/*************************************************************************** * copyright : (c) 2004 Pierpaolo Di Panfilo * * (c) 2004 Mark Kretschmann * * (c) 2005-2006 Seb Ruiz * * (c) 2005 Gábor Lehel * * (c) 2005 Christian Muehlhaeuser * * (c) 2006 Alexandre Oliveira * * (c) 2006 Adam Pigg * * See COPYING file for licensing information * ***************************************************************************/ #define DEBUG_PREFIX "PlaylistBrowser" #include "amarok.h" //actionCollection() #include "browserToolBar.h" #include "collectiondb.h" //smart playlists #include "debug.h" #include "htmlview.h" #include "k3bexporter.h" #include "mediabrowser.h" #include "dynamicmode.h" #include "lastfm.h" #include "playlist.h" #include "playlistbrowser.h" #include "playlistbrowseritem.h" #include "playlistselection.h" #include "podcastbundle.h" #include "podcastsettings.h" #include "scancontroller.h" #include "smartplaylisteditor.h" #include "tagdialog.h" //showContextMenu() #include "threadmanager.h" #include "statusbar.h" #include "contextbrowser.h" #include "xspfplaylist.h" #include //customEvent() #include //mousePressed() #include #include //paintCell() #include //paintCell() #include //loadPlaylists(), saveM3U(), savePLS() #include #include #include #include #include //openPlaylist() #include //deleteSelectedPlaylists() #include //smallIcon #include #include //rename() #include #include //renamePlaylist(), deleteSelectedPlaylist() #include #include //dragObject() #include #include #include //TDEGlobal::dirs() #include //dragObject() #include //rename() in renamePlaylist() namespace Amarok { TQListViewItem* findItemByPath( TQListView *view, TQString name ) { const static TQString escaped( "\\/" ); const static TQChar sep( '/' ); debug() << "Searching " << name << endl; TQStringList path = splitPath( name ); TQListViewItem *prox = view->firstChild(); TQListViewItem *item = 0; foreach( path ) { item = prox; TQString text( *it ); text.replace( escaped, sep ); for ( ; item; item = item->nextSibling() ) { if ( text == item->text(0) ) { break; } } if ( !item ) return 0; prox = item->firstChild(); } return item; } TQStringList splitPath( TQString path ) { TQStringList list; const static TQChar sep( '/' ); int bOffset = 0, sOffset = 0; int pos = path.find( sep, bOffset ); while ( pos != -1 ) { if ( pos > sOffset && pos <= (int)path.length() ) { if ( pos > 0 && path[pos-1] != '\\' ) { list << path.mid( sOffset, pos - sOffset ); sOffset = pos + 1; } } bOffset = pos + 1; pos = path.find( sep, bOffset ); } int length = path.length() - 1; if ( path.mid( sOffset, length - sOffset + 1 ).length() > 0 ) list << path.mid( sOffset, length - sOffset + 1 ); return list; } } inline TQString fileExtension( const TQString &fileName ) { return Amarok::extension( fileName ); } PlaylistBrowser *PlaylistBrowser::s_instance = 0; PlaylistBrowser::PlaylistBrowser( const char *name ) : TQVBox( 0, name ) , m_polished( false ) , m_playlistCategory( 0 ) , m_streamsCategory( 0 ) , m_smartCategory( 0 ) , m_dynamicCategory( 0 ) , m_podcastCategory( 0 ) , m_coolStreams( 0 ) , m_smartDefaults( 0 ) , m_lastfmCategory( 0 ) , m_shoutcastCategory( 0 ) , m_lastPlaylist( 0 ) , m_coolStreamsOpen( false ) , m_smartDefaultsOpen( false ) , m_lastfmOpen( false ) , m_ac( new TDEActionCollection( this ) ) , m_podcastTimer( new TQTimer( this ) ) { s_instance = this; TQVBox *browserBox = new TQVBox( this ); browserBox->setSpacing( 3 ); // addMenuButton = new TDEActionMenu( i18n("Add"), Amarok::icon( "add_playlist" ), m_ac ); addMenuButton->setDelayed( false ); TDEPopupMenu *playlistMenu = new TDEPopupMenu( this ); playlistMenu->insertItem( i18n("New..."), PLAYLIST ); playlistMenu->insertItem( i18n("Import Existing..."), PLAYLIST_IMPORT ); connect( playlistMenu, TQT_SIGNAL( activated(int) ), TQT_SLOT( slotAddPlaylistMenu(int) ) ); TDEPopupMenu *addMenu = addMenuButton->popupMenu(); addMenu->insertItem( i18n("Playlist"), playlistMenu ); addMenu->insertItem( i18n("Smart Playlist..."), SMARTPLAYLIST ); addMenu->insertItem( i18n("Dynamic Playlist..."), ADDDYNAMIC); addMenu->insertItem( i18n("Radio Stream..."), STREAM ); addMenu->insertItem( i18n("Podcast..."), PODCAST ); connect( addMenu, TQT_SIGNAL( activated(int) ), TQT_SLOT( slotAddMenu(int) ) ); renameButton = new TDEAction( i18n("Rename"), "edit-clear", 0, TQT_TQOBJECT(this), TQT_SLOT( renameSelectedItem() ), m_ac ); removeButton = new TDEAction( i18n("Delete"), Amarok::icon( "remove" ), 0, TQT_TQOBJECT(this), TQT_SLOT( removeSelectedItems() ), m_ac ); m_toolbar = new Browser::ToolBar( browserBox ); m_toolbar->setIconText( TDEToolBar::IconTextRight, false ); //we want the open button to have text on right addMenuButton->plug( m_toolbar ); m_toolbar->setIconText( TDEToolBar::IconOnly, false ); //default appearance m_toolbar->insertLineSeparator(); renameButton->plug( m_toolbar); removeButton->plug( m_toolbar ); renameButton->setEnabled( false ); removeButton->setEnabled( false ); // m_splitter = new TQSplitter( Qt::Vertical, browserBox ); m_splitter->setChildrenCollapsible( false ); // hiding the InfoPane entirely can only be confusing m_listview = new PlaylistBrowserView( m_splitter ); int sort = Amarok::config( "PlaylistBrowser" )->readNumEntry( "Sorting", TQt::Ascending ); m_listview->setSorting( 0, sort == TQt::Ascending ? true : false ); m_podcastTimerInterval = Amarok::config( "PlaylistBrowser" )->readNumEntry( "Podcast Interval", 14400000 ); connect( m_podcastTimer, TQT_SIGNAL(timeout()), this, TQT_SLOT(scanPodcasts()) ); // signals and slots connections connect( m_listview, TQT_SIGNAL( contextMenuRequested( TQListViewItem *, const TQPoint &, int ) ), this, TQT_SLOT( showContextMenu( TQListViewItem *, const TQPoint &, int ) ) ); connect( m_listview, TQT_SIGNAL( doubleClicked( TQListViewItem *, const TQPoint &, int ) ), this, TQT_SLOT( invokeItem( TQListViewItem *, const TQPoint &, int ) ) ); connect( m_listview, TQT_SIGNAL( itemRenamed( TQListViewItem*, const TQString&, int ) ), this, TQT_SLOT( renamePlaylist( TQListViewItem*, const TQString&, int ) ) ); connect( m_listview, TQT_SIGNAL( currentChanged( TQListViewItem * ) ), this, TQT_SLOT( currentItemChanged( TQListViewItem * ) ) ); connect( CollectionDB::instance(), TQT_SIGNAL( scanDone( bool ) ), TQT_SLOT( collectionScanDone() ) ); setMinimumWidth( m_toolbar->sizeHint().width() ); m_infoPane = new InfoPane( m_splitter ); m_podcastCategory = loadPodcasts(); setSpacing( 4 ); setFocusProxy( m_listview ); } void PlaylistBrowser::polish() { // we make startup faster by doing the slow bits for this // only when we are shown on screen DEBUG_FUNC_INFO Amarok::OverrideCursor cursor; // blockSignals( true ); // BrowserBar::instance()->restoreWidth(); // blockSignals( false ); TQVBox::polish(); /// Podcasting is always initialised in the ctor because of autoscanning m_polished = true; m_playlistCategory = loadPlaylists(); if( !CollectionDB::instance()->isEmpty() ) { m_smartCategory = loadSmartPlaylists(); loadDefaultSmartPlaylists(); } #define config Amarok::config( "PlaylistBrowser" ) m_dynamicCategory = loadDynamics(); m_randomDynamic = new DynamicEntry( m_dynamicCategory, 0, i18n("Random Mix") ); m_randomDynamic->setKept( false ); //don't save it m_randomDynamic->setCycleTracks( config->readBoolEntry( "Dynamic Random Remove Played", true ) ); m_randomDynamic->setUpcomingCount( config->readNumEntry ( "Dynamic Random Upcoming Count", 15 ) ); m_randomDynamic->setPreviousCount( config->readNumEntry ( "Dynamic Random Previous Count", 5 ) ); m_suggestedDynamic = new DynamicEntry( m_dynamicCategory, m_randomDynamic, i18n("Suggested Songs" ) ); m_suggestedDynamic->setKept( false ); //don't save it m_suggestedDynamic->setAppendType( DynamicMode::SUGGESTION ); m_suggestedDynamic->setCycleTracks( config->readBoolEntry( "Dynamic Suggest Remove Played", true ) ); m_suggestedDynamic->setUpcomingCount( config->readNumEntry ( "Dynamic Suggest Upcoming Count", 15 ) ); m_suggestedDynamic->setPreviousCount( config->readNumEntry ( "Dynamic Suggest Previous Count", 5 ) ); #undef config m_streamsCategory = loadStreams(); loadCoolStreams(); m_shoutcastCategory = new ShoutcastBrowser( m_streamsCategory ); if( !AmarokConfig::scrobblerUsername().isEmpty() ) { const bool subscriber = Amarok::config( "Scrobbler" )->readBoolEntry( "Subscriber", false ); loadLastfmStreams( subscriber ); } markDynamicEntries(); // ListView item state restoration: // First we check if the number of items in the listview is the same as it was on last // application exit. If true, we iterate over all items and restore their open/closed state. // Note: We ignore podcast items, because they are added dynamically added to the ListView. TQValueList stateList = Amarok::config( "PlaylistBrowser" )->readIntListEntry( "Item State" ); TQListViewItemIterator it( m_listview ); uint count = 0; while ( it.current() ) { if( !isPodcastEpisode( it.current() ) ) ++count; ++it; } if ( count == stateList.count() ) { uint index = 0; it = TQListViewItemIterator( m_listview ); while ( it.current() ) { if( !isPodcastEpisode( it.current() ) ) { it.current()->setOpen( stateList[index] ); ++index; } ++it; } } // Set height of InfoPane m_infoPane->setStoredHeight( Amarok::config( "PlaylistBrowser" )->readNumEntry( "InfoPane Height", 200 ) ); } PlaylistBrowser::~PlaylistBrowser() { DEBUG_BLOCK s_instance = 0; if( m_polished ) { savePlaylists(); saveSmartPlaylists(); saveDynamics(); saveStreams(); saveLastFm(); savePodcastFolderStates( m_podcastCategory ); TQStringList list; for( uint i=0; i < m_dynamicEntries.count(); i++ ) { TQListViewItem *item = m_dynamicEntries.at( i ); list.append( item->text(0) ); } Amarok::config( "PlaylistBrowser" )->writeEntry( "Sorting", m_listview->sortOrder() ); Amarok::config( "PlaylistBrowser" )->writeEntry( "Podcast Interval", m_podcastTimerInterval ); Amarok::config( "PlaylistBrowser" )->writeEntry( "Podcast Folder Open", m_podcastCategory->isOpen() ); Amarok::config( "PlaylistBrowser" )->writeEntry( "InfoPane Height", m_infoPane->getHeight() ); } } void PlaylistBrowser::setInfo( const TQString &title, const TQString &info ) { m_infoPane->setInfo( title, info ); } void PlaylistBrowser::resizeEvent( TQResizeEvent * ) { if( TQT_TQWIDGET( m_infoPane->child( "container" ) )->isShown() ) m_infoPane->setMaximumHeight( ( int )( m_splitter->height() / 1.5 ) ); } void PlaylistBrowser::markDynamicEntries() { if( Amarok::dynamicMode() ) { TQStringList playlists = Amarok::dynamicMode()->items(); for( uint i=0; i < playlists.count(); i++ ) { PlaylistBrowserEntry *item = dynamic_cast( Amarok::findItemByPath( m_listview, playlists[i] ) ); if( item ) { m_dynamicEntries.append( item ); if( item->rtti() == PlaylistEntry::RTTI ) static_cast( item )->setDynamic( true ); if( item->rtti() == SmartPlaylist::RTTI ) static_cast( item )->setDynamic( true ); } } } } /** ************************************************************************* * STREAMS ************************************************************************* **/ TQString PlaylistBrowser::streamBrowserCache() const { return Amarok::saveLocation() + "streambrowser_save.xml"; } PlaylistCategory* PlaylistBrowser::loadStreams() { TQFile file( streamBrowserCache() ); TQTextStream stream( &file ); stream.setEncoding( TQTextStream::UnicodeUTF8 ); TQDomDocument d; TQDomElement e; TQListViewItem *after = m_dynamicCategory; if( !file.open( IO_ReadOnly ) || !d.setContent( stream.read() ) ) { /*Couldn't open the file or it had invalid content, so let's create an empty element*/ return new PlaylistCategory( m_listview, after , i18n("Radio Streams") ); } else { e = d.namedItem( "category" ).toElement(); if ( e.attribute("formatversion") =="1.1" ) { PlaylistCategory* p = new PlaylistCategory( m_listview, after, e ); p->setText(0, i18n("Radio Streams") ); return p; } else { // Old unversioned format PlaylistCategory* p = new PlaylistCategory( m_listview, after, i18n("Radio Streams") ); TQListViewItem *last = 0; TQDomNode n = d.namedItem( "streambrowser" ).namedItem("stream"); for( ; !n.isNull(); n = n.nextSibling() ) { last = new StreamEntry( p, last, n.toElement() ); } return p; } } } void PlaylistBrowser::loadCoolStreams() { TQFile file( locate( "data","amarok/data/Cool-Streams.xml" ) ); if( !file.open( IO_ReadOnly ) ) return; TQTextStream stream( &file ); stream.setEncoding( TQTextStream::UnicodeUTF8 ); TQDomDocument d; if( !d.setContent( stream.read() ) ) { error() << "Bad Cool Streams XML file" << endl; return; } m_coolStreams = new PlaylistCategory( m_streamsCategory, 0, i18n("Cool-Streams") ); m_coolStreams->setOpen( m_coolStreamsOpen ); m_coolStreams->setKept( false ); StreamEntry *last = 0; TQDomNode n = d.namedItem( "coolstreams" ).firstChild(); for( ; !n.isNull(); n = n.nextSibling() ) { TQDomElement e = n.toElement(); TQString name = e.attribute( "name" ); e = n.namedItem( "url" ).toElement(); KURL url( e.text() ); last = new StreamEntry( m_coolStreams, last, url, name ); last->setKept( false ); } } void PlaylistBrowser::addStream( TQListViewItem *parent ) { StreamEditor dialog( this, i18n( "Radio Stream" ), TQString() ); dialog.setCaption( i18n( "Add Radio Stream" ) ); if( !parent ) parent = static_cast(m_streamsCategory); if( dialog.exec() == TQDialog::Accepted ) { new StreamEntry( parent, 0, dialog.url(), dialog.name() ); parent->sortChildItems( 0, true ); parent->setOpen( true ); saveStreams(); } } void PlaylistBrowser::editStreamURL( StreamEntry *item, const bool readonly ) { StreamEditor dialog( this, item->title(), item->url().prettyURL(), readonly ); dialog.setCaption( readonly ? i18n( "Radio Stream" ) : i18n( "Edit Radio Stream" ) ); if( dialog.exec() == TQDialog::Accepted ) { item->setTitle( dialog.name() ); item->setURL( dialog.url() ); item->setText(0, dialog.name() ); } } void PlaylistBrowser::saveStreams() { TQFile file( streamBrowserCache() ); TQDomDocument doc; TQDomElement streamB = m_streamsCategory->xml(); streamB.setAttribute( "product", "Amarok" ); streamB.setAttribute( "version", APP_VERSION ); streamB.setAttribute( "formatversion", "1.1" ); TQDomNode streamsNode = doc.importNode( streamB, true ); doc.appendChild( streamsNode ); TQString temp( doc.toString() ); // Only open the file after all data is ready. If it crashes, data is not lost! if ( !file.open( IO_WriteOnly ) ) return; TQTextStream stream( &file ); stream.setEncoding( TQTextStream::UnicodeUTF8 ); stream << "\n"; stream << temp; } /** ************************************************************************* * LAST.FM ************************************************************************* **/ void PlaylistBrowser::loadLastfmStreams( const bool subscriber /*false*/ ) { TQFile file( Amarok::saveLocation() + "lastfmbrowser_save.xml" ); TQTextStream stream( &file ); stream.setEncoding( TQTextStream::UnicodeUTF8 ); TQDomDocument d; TQDomElement e; TQListViewItem *after = m_streamsCategory; if( !file.open( IO_ReadOnly ) || !d.setContent( stream.read() ) ) { /*Couldn't open the file or it had invalid content, so let's create an empty element*/ m_lastfmCategory = new PlaylistCategory( m_listview, after , i18n("Last.fm Radio") ); } else { e = d.namedItem( "category" ).toElement(); m_lastfmCategory = new PlaylistCategory( m_listview, after, e ); m_lastfmCategory->setText( 0, i18n("Last.fm Radio") ); } /// Load the default items TQStringList globaltags; globaltags << "Alternative" << "Ambient" << "Chill Out" << "Classical" << "Dance" << "Electronica" << "Favorites" << "Heavy Metal" << "Hip Hop" << "Indie Rock" << "Industrial" << "Japanese" << "Pop" << "Psytrance" << "Rap" << "Rock" << "Soundtrack" << "Techno" << "Trance"; PlaylistCategory *tagsFolder = new PlaylistCategory( m_lastfmCategory, 0, i18n("Global Tags") ); tagsFolder->setKept( false ); LastFmEntry *last = 0; foreach( globaltags ) { const KURL url( "lastfm://globaltags/" + *it ); last = new LastFmEntry( tagsFolder, last, url, *it ); last->setKept( false ); } TQString user = AmarokConfig::scrobblerUsername(); KURL url( TQString("lastfm://user/%1/neighbours").arg( user ) ); last = new LastFmEntry( m_lastfmCategory, tagsFolder, url, i18n( "Neighbor Radio" ) ); last->setKept( false ); url = KURL::fromPathOrURL( TQString("lastfm://user/%1/recommended/100").arg( user ) ); last = new LastFmEntry( m_lastfmCategory, last, url, i18n( "Recommended Radio" ) ); last->setKept( false ); if( subscriber ) { url = KURL::fromPathOrURL( TQString("lastfm://user/%1/personal").arg( user ) ); last = new LastFmEntry( m_lastfmCategory, last, url, i18n( "Personal Radio" ) ); last->setKept( false ); url = KURL::fromPathOrURL( TQString("lastfm://user/%1/loved").arg( user ) ); last = new LastFmEntry( m_lastfmCategory, last, url, i18n( "Loved Radio" ) ); last->setKept( false ); } } void PlaylistBrowser::addLastFmRadio( TQListViewItem *parent ) { StreamEditor dialog( this, i18n( "Last.fm Radio" ), TQString() ); dialog.setCaption( i18n( "Add Last.fm Radio" ) ); if( !parent ) parent = static_cast(m_lastfmCategory); if( dialog.exec() == TQDialog::Accepted ) { new LastFmEntry( parent, 0, dialog.url(), dialog.name() ); parent->sortChildItems( 0, true ); parent->setOpen( true ); saveLastFm(); } } void PlaylistBrowser::addLastFmCustomRadio( TQListViewItem *parent ) { TQString token = LastFm::Controller::createCustomStation(); if( token.isEmpty() ) return; token.replace( "/", "%252" ); const TQString text = "lastfm://artistnames/" + token; const KURL url( text ); TQString name = LastFm::Controller::stationDescription( text ); name.replace( "%252", "/" ); new LastFmEntry( parent, 0, url, name ); saveLastFm(); } void PlaylistBrowser::saveLastFm() { if ( !m_lastfmCategory ) return; TQFile file( Amarok::saveLocation() + "lastfmbrowser_save.xml" ); TQDomDocument doc; TQDomElement lastfmB = m_lastfmCategory->xml(); lastfmB.setAttribute( "product", "Amarok" ); lastfmB.setAttribute( "version", APP_VERSION ); lastfmB.setAttribute( "formatversion", "1.1" ); TQDomNode lastfmNode = doc.importNode( lastfmB, true ); doc.appendChild( lastfmNode ); TQString temp( doc.toString() ); // Only open the file after all data is ready. If it crashes, data is not lost! if ( !file.open( IO_WriteOnly ) ) return; TQTextStream stream( &file ); stream.setEncoding( TQTextStream::UnicodeUTF8 ); stream << "\n"; stream << temp; } /** ************************************************************************* * SMART-PLAYLISTS ************************************************************************* **/ TQString PlaylistBrowser::smartplaylistBrowserCache() const { return Amarok::saveLocation() + "smartplaylistbrowser_save.xml"; } void PlaylistBrowser::addSmartPlaylist( TQListViewItem *parent ) //SLOT { if( CollectionDB::instance()->isEmpty() || !m_smartCategory ) return; if( !parent ) parent = static_cast(m_smartCategory); SmartPlaylistEditor dialog( i18n("Untitled"), this ); if( dialog.exec() == TQDialog::Accepted ) { PlaylistCategory *category = dynamic_cast(parent); for( TQListViewItem *item = category->firstChild(); item; item = item->nextSibling() ) { SmartPlaylist *sp = dynamic_cast(item); if ( sp && sp->title() == dialog.name() ) { if( KMessageBox::warningContinueCancel( PlaylistWindow::self(), i18n( "A Smart Playlist named \"%1\" already exists. Do you want to overwrite it?" ).arg( dialog.name() ), i18n( "Overwrite Playlist?" ), i18n( "Overwrite" ) ) == KMessageBox::Continue ) { delete item; break; } else return; } } new SmartPlaylist( parent, 0, dialog.result() ); parent->sortChildItems( 0, true ); parent->setOpen( true ); saveSmartPlaylists(); } } PlaylistCategory* PlaylistBrowser::loadSmartPlaylists() { TQFile file( smartplaylistBrowserCache() ); TQTextStream stream( &file ); stream.setEncoding( TQTextStream::UnicodeUTF8 ); TQListViewItem *after = m_playlistCategory; TQDomDocument d; TQDomElement e; if( !file.open( IO_ReadOnly ) || !d.setContent( stream.read() ) ) { /*Couldn't open the file or it had invalid content, so let's create an empty element*/ return new PlaylistCategory(m_listview, after, i18n("Smart Playlists") ); } else { e = d.namedItem( "category" ).toElement(); TQString version = e.attribute("formatversion"); float fversion = e.attribute("formatversion").toFloat(); if ( version == "1.8" ) { PlaylistCategory* p = new PlaylistCategory(m_listview, after, e ); p->setText( 0, i18n("Smart Playlists") ); return p; } else if ( fversion > 1.0f ) { PlaylistCategory* p = new PlaylistCategory(m_listview, after, e ); p->setText( 0, i18n("Smart Playlists") ); debug() << "loading old format smart playlists, converted to new format" << endl; updateSmartPlaylists( p ); saveSmartPlaylists( p ); return p; } else { // Old unversioned format PlaylistCategory* p = new PlaylistCategory(m_listview, after , i18n("Smart Playlists") ); TQListViewItem *last = 0; TQDomNode n = d.namedItem( "smartplaylists" ).namedItem("smartplaylist"); for( ; !n.isNull(); n = n.nextSibling() ) { last = new SmartPlaylist( p, last, n.toElement() ); } return p; } } } void PlaylistBrowser::updateSmartPlaylists( TQListViewItem *p ) { if( !p ) return; for( TQListViewItem *it = p->firstChild(); it; it = it->nextSibling() ) { SmartPlaylist *spl = dynamic_cast( it ); if( spl ) { TQDomElement xml = spl->xml(); TQDomElement query = xml.namedItem( "sqlquery" ).toElement(); TQDomElement expandBy = xml.namedItem( "expandby" ).toElement(); updateSmartPlaylistElement( query ); updateSmartPlaylistElement( expandBy ); spl->setXml( xml ); } else updateSmartPlaylists( it ); } } void PlaylistBrowser::updateSmartPlaylistElement( TQDomElement& query ) { TQRegExp limitSearch( "LIMIT.*(\\d+)\\s*,\\s*(\\d+)" ); TQRegExp selectFromSearch( "SELECT[^'\"]*FROM" ); for(TQDomNode child = query.firstChild(); !child.isNull(); child = child.nextSibling() ) { if( child.isText() ) { //HACK this should be refactored to just regenerate the SQL from the 's TQDomText text = child.toText(); TQString sql = text.data(); if ( selectFromSearch.search( sql ) != -1 ) sql.replace( selectFromSearch, "SELECT (*ListOfFields*) FROM" ); if ( limitSearch.search( sql ) != -1 ) sql.replace( limitSearch, TQString( "LIMIT %1 OFFSET %2").arg( limitSearch.capturedTexts()[2].toInt() ).arg( limitSearch.capturedTexts()[1].toInt() ) ); text.setData( sql ); break; } } } void PlaylistBrowser::loadDefaultSmartPlaylists() { DEBUG_BLOCK const TQStringList genres = CollectionDB::instance()->query( "SELECT DISTINCT name FROM genre;" ); const TQStringList artists = CollectionDB::instance()->artistList(); SmartPlaylist *item; QueryBuilder qb; SmartPlaylist *last = 0; m_smartDefaults = new PlaylistCategory( m_smartCategory, 0, i18n("Collection") ); m_smartDefaults->setOpen( m_smartDefaultsOpen ); m_smartDefaults->setKept( false ); /********** All Collection **************/ qb.initSQLDrag(); qb.sortBy( QueryBuilder::tabArtist, QueryBuilder::valName ); qb.sortBy( QueryBuilder::tabAlbum, QueryBuilder::valName ); qb.sortBy( QueryBuilder::tabSong, QueryBuilder::valTrack ); item = new SmartPlaylist( m_smartDefaults, 0, i18n( "All Collection" ), qb.query() ); item->setPixmap( 0, SmallIcon( Amarok::icon( "collection" ) ) ); item->setKept( false ); /********** Favorite Tracks **************/ qb.initSQLDrag(); qb.sortByFavorite(); qb.setLimit( 0, 15 ); item = new SmartPlaylist( m_smartDefaults, item, i18n( "Favorite Tracks" ), qb.query() ); item->setKept( false ); last = 0; qb.initSQLDrag(); qb.sortByFavorite(); qb.setLimit( 0, 15 ); foreach( artists ) { QueryBuilder qbTemp( qb ); qbTemp.addMatch( QueryBuilder::tabArtist, *it ); last = new SmartPlaylist( item, last, i18n( "By %1" ).arg( *it ), qbTemp.query() ); last->setKept( false ); } /********** Most Played **************/ qb.initSQLDrag(); qb.sortBy( QueryBuilder::tabStats, QueryBuilder::valPlayCounter, true ); qb.setLimit( 0, 15 ); item = new SmartPlaylist( m_smartDefaults, item, i18n( "Most Played" ), qb.query() ); item->setKept( false ); last = 0; qb.initSQLDrag(); qb.sortBy( QueryBuilder::tabStats, QueryBuilder::valPlayCounter, true ); qb.setLimit( 0, 15 ); foreach( artists ) { QueryBuilder qbTemp( qb ); qbTemp.addMatch( QueryBuilder::tabArtist, *it ); last = new SmartPlaylist( item, last, i18n( "By %1" ).arg( *it ), qbTemp.query() ); last->setKept( false ); } /********** Newest Tracks **************/ qb.initSQLDrag(); qb.sortBy( QueryBuilder::tabSong, QueryBuilder::valCreateDate, true ); qb.setLimit( 0, 15 ); item = new SmartPlaylist( m_smartDefaults, item, i18n( "Newest Tracks" ), qb.query() ); item->setKept( false ); last = 0; qb.initSQLDrag(); qb.sortBy( QueryBuilder::tabSong, QueryBuilder::valCreateDate, true ); qb.setLimit( 0, 15 ); foreach( artists ) { QueryBuilder qbTemp( qb ); qbTemp.addMatch( QueryBuilder::tabArtist, *it ); last = new SmartPlaylist( item, last, i18n( "By %1" ).arg( *it ), qbTemp.query( true ) ); last->setKept( false ); } /********** Last Played **************/ qb.initSQLDrag(); qb.sortBy( QueryBuilder::tabStats, QueryBuilder::valAccessDate, true ); qb.setLimit( 0, 15 ); item = new SmartPlaylist( m_smartDefaults, item, i18n( "Last Played" ), qb.query( true ) ); item->setKept( false ); /********** Never Played **************/ qb.initSQLDrag(); qb.addNumericFilter( QueryBuilder::tabStats, QueryBuilder::valPlayCounter, "0" ); qb.sortBy( QueryBuilder::tabArtist, QueryBuilder::valName ); qb.sortBy( QueryBuilder::tabAlbum, QueryBuilder::valName ); qb.sortBy( QueryBuilder::tabSong, QueryBuilder::valTrack ); item = new SmartPlaylist( m_smartDefaults, item, i18n( "Never Played" ), qb.query( true ) ); item->setKept( false ); /********** Ever Played **************/ qb.initSQLDrag(); qb.excludeFilter( QueryBuilder::tabStats, QueryBuilder::valPlayCounter, "1", QueryBuilder::modeLess ); qb.sortBy( QueryBuilder::tabArtist, QueryBuilder::valName ); qb.sortBy( QueryBuilder::tabAlbum, QueryBuilder::valName ); qb.sortBy( QueryBuilder::tabSong, QueryBuilder::valTrack ); qb.sortBy( QueryBuilder::tabStats, QueryBuilder::valScore ); item = new SmartPlaylist( m_smartDefaults, item, i18n( "Ever Played" ), qb.query( true ) ); item->setKept( false ); /********** Genres **************/ item = new SmartPlaylist( m_smartDefaults, item, i18n( "Genres" ), TQString() ); item->setKept( false ); last = 0; qb.initSQLDrag(); qb.sortBy( QueryBuilder::tabArtist, QueryBuilder::valName ); qb.sortBy( QueryBuilder::tabAlbum, QueryBuilder::valName ); qb.sortBy( QueryBuilder::tabSong, QueryBuilder::valTrack ); foreach( genres ) { QueryBuilder qbTemp( qb ); qbTemp.addMatch( QueryBuilder::tabGenre, *it ); last = new SmartPlaylist( item, last, i18n( "%1" ).arg( *it ), qbTemp.query( true ) ); last->setKept( false ); } /********** 50 Random Tracks **************/ qb.initSQLDrag(); qb.setOptions( QueryBuilder::optRandomize ); qb.setLimit( 0, 50 ); item = new SmartPlaylist( m_smartDefaults, item, i18n( "50 Random Tracks" ), qb.query( true ) ); item->setKept( false ); } void PlaylistBrowser::editSmartPlaylist( SmartPlaylist* item ) { SmartPlaylistEditor dialog( this, item->xml() ); if( dialog.exec() == TQDialog::Accepted ) { item->setXml ( dialog.result() ); item->setText( 0, dialog.name() ); if( item->isDynamic() ) // rebuild the cache if the smart playlist has changed Playlist::instance()->rebuildDynamicModeCache(); } } void PlaylistBrowser::saveSmartPlaylists( PlaylistCategory *smartCategory ) { TQFile file( smartplaylistBrowserCache() ); if( !smartCategory ) smartCategory = m_smartCategory; // If the user hadn't set a collection, we didn't create the Smart Playlist Item if( !smartCategory ) return; TQDomDocument doc; TQDomElement smartB = smartCategory->xml(); smartB.setAttribute( "product", "Amarok" ); smartB.setAttribute( "version", APP_VERSION ); smartB.setAttribute( "formatversion", "1.8" ); TQDomNode smartplaylistsNode = doc.importNode( smartB, true ); doc.appendChild( smartplaylistsNode ); TQString temp( doc.toString() ); // Only open the file after all data is ready. If it crashes, data is not lost! if ( !file.open( IO_WriteOnly ) ) return; TQTextStream smart( &file ); smart.setEncoding( TQTextStream::UnicodeUTF8 ); smart << "\n"; smart << temp; } /** ************************************************************************* * PARTIES ************************************************************************* **/ TQString PlaylistBrowser::dynamicBrowserCache() const { return Amarok::saveLocation() + "dynamicbrowser_save.xml"; } PlaylistCategory* PlaylistBrowser::loadDynamics() { TQFile file( dynamicBrowserCache() ); TQTextStream stream( &file ); stream.setEncoding( TQTextStream::UnicodeUTF8 ); TQDomDocument d; TQDomElement e; PlaylistCategory *after = m_smartCategory; if( CollectionDB::instance()->isEmpty() || !m_smartCategory ) // incase of no collection after = m_playlistCategory; if( !file.open( IO_ReadOnly ) || !d.setContent( stream.read() ) ) { /*Couldn't open the file or it had invalid content, so let's create some defaults*/ PlaylistCategory *p = new PlaylistCategory( m_listview, after, i18n("Dynamic Playlists") ); return p; } else { e = d.namedItem( "category" ).toElement(); TQString version = e.attribute("formatversion"); if ( version == "1.2" ) { PlaylistCategory* p = new PlaylistCategory( m_listview, after, e ); p->setText( 0, i18n("Dynamic Playlists") ); return p; } else if ( version == "1.1" ) { // In 1.1, playlists would be referred only by its name. // TODO: We can *try* to convert by using findItem PlaylistCategory* p = new PlaylistCategory( m_listview, after, e ); p->setText( 0, i18n("Dynamic Playlists") ); fixDynamicPlaylistPath( p ); return p; } else { // Old unversioned format PlaylistCategory* p = new PlaylistCategory( m_listview, after, i18n("Dynamic Playlists") ); TQListViewItem *last = 0; TQDomNode n = d.namedItem( "dynamicbrowser" ).namedItem("dynamic"); for( ; !n.isNull(); n = n.nextSibling() ) { last = new DynamicEntry( p, last, n.toElement() ); } return p; } } } void PlaylistBrowser::fixDynamicPlaylistPath( TQListViewItem *item ) { DynamicEntry *entry = dynamic_cast( item ); if ( entry ) { TQStringList names = entry->items(); TQStringList paths; foreach( names ) { TQString path = guessPathFromPlaylistName( *it ); if ( !path.isNull() ) paths+=path; } entry->setItems( paths ); } PlaylistCategory *cat = dynamic_cast( item ); if ( cat ) { TQListViewItem *it = cat->firstChild(); for( ; it; it = it->nextSibling() ) { fixDynamicPlaylistPath( it ); } } } TQString PlaylistBrowser::guessPathFromPlaylistName( TQString name ) { TQListViewItem *item = m_listview->findItem( name, 0, TQt::ExactMatch ); PlaylistBrowserEntry *entry = dynamic_cast( item ); if ( entry ) return entry->name(); return TQString(); } void PlaylistBrowser::saveDynamics() { Amarok::config( "PlaylistBrowser" )->writeEntry( "Dynamic Random Remove Played", m_randomDynamic->cycleTracks() ); Amarok::config( "PlaylistBrowser" )->writeEntry( "Dynamic Random Upcoming Count", m_randomDynamic->upcomingCount() ); Amarok::config( "PlaylistBrowser" )->writeEntry( "Dynamic Random Previous Count", m_randomDynamic->previousCount() ); Amarok::config( "PlaylistBrowser" )->writeEntry( "Dynamic Suggest Remove Played", m_suggestedDynamic->cycleTracks() ); Amarok::config( "PlaylistBrowser" )->writeEntry( "Dynamic Suggest Upcoming Count", m_suggestedDynamic->upcomingCount() ); Amarok::config( "PlaylistBrowser" )->writeEntry( "Dynamic Suggest Previous Count", m_suggestedDynamic->previousCount() ); TQFile file( dynamicBrowserCache() ); TQTextStream stream( &file ); TQDomDocument doc; TQDomElement dynamicB = m_dynamicCategory->xml(); dynamicB.setAttribute( "product", "Amarok" ); dynamicB.setAttribute( "version", APP_VERSION ); dynamicB.setAttribute( "formatversion", "1.2" ); TQDomNode dynamicsNode = doc.importNode( dynamicB, true ); doc.appendChild( dynamicsNode ); TQString temp( doc.toString() ); // Only open the file after all data is ready. If it crashes, data is not lost! if ( !file.open( IO_WriteOnly ) ) return; stream.setEncoding( TQTextStream::UnicodeUTF8 ); stream << "\n"; stream << temp; } void PlaylistBrowser::loadDynamicItems() { // Make sure all items are unmarked for( uint i=0; i < m_dynamicEntries.count(); i++ ) { TQListViewItem *it = m_dynamicEntries.at( i ); if( it ) static_cast(it)->setDynamic( false ); } m_dynamicEntries.clear(); // Don't use remove(), since we do i++, which would cause skip overs!!! // Mark appropriate items as used markDynamicEntries(); } /** ************************************************************************* * PODCASTS ************************************************************************* **/ TQString PlaylistBrowser::podcastBrowserCache() const { //returns the playlists stats cache file return Amarok::saveLocation() + "podcastbrowser_save.xml"; } PlaylistCategory* PlaylistBrowser::loadPodcasts() { DEBUG_BLOCK TQFile file( podcastBrowserCache() ); TQTextStream stream( &file ); stream.setEncoding( TQTextStream::UnicodeUTF8 ); TQDomDocument d; TQDomElement e; TQListViewItem *after = 0; if( !file.open( IO_ReadOnly ) || !d.setContent( stream.read() ) ) { /*Couldn't open the file or it had invalid content, so let's create an empty element*/ PlaylistCategory *p = new PlaylistCategory( m_listview, after, i18n("Podcasts") ); p->setId( 0 ); loadPodcastsFromDatabase( p ); return p; } else { e = d.namedItem( "category" ).toElement(); if ( e.attribute("formatversion") == "1.1" ) { debug() << "Podcasts are being moved to the database..." << endl; m_podcastItemsToScan.clear(); PlaylistCategory *p = new PlaylistCategory( m_listview, after, e ); p->setId( 0 ); //delete the file, it is deprecated TDEIO::del( KURL::fromPathOrURL( podcastBrowserCache() ) ); if( !m_podcastItemsToScan.isEmpty() ) m_podcastTimer->start( m_podcastTimerInterval ); return p; } } PlaylistCategory *p = new PlaylistCategory( m_listview, after, i18n("Podcasts") ); p->setId( 0 ); return p; } void PlaylistBrowser::loadPodcastsFromDatabase( PlaylistCategory *p ) { DEBUG_BLOCK if( !p ) p = m_podcastCategory; m_podcastItemsToScan.clear(); while( p->firstChild() ) delete p->firstChild(); TQMap folderMap = loadPodcastFolders( p ); TQValueList channels; channels = CollectionDB::instance()->getPodcastChannels(); PodcastChannel *channel = 0; foreachType( TQValueList, channels ) { PlaylistCategory *parent = p; const int parentId = (*it).parentId(); if( parentId > 0 && folderMap.find( parentId ) != folderMap.end() ) parent = folderMap[parentId]; channel = new PodcastChannel( parent, channel, *it ); bool hasNew = CollectionDB::instance()->query( TQString("SELECT COUNT(parent) FROM podcastepisodes WHERE ( parent='%1' AND isNew=%2 ) LIMIT 1" ) .arg( (*it).url().url(), CollectionDB::instance()->boolT() ) ) .first().toInt() > 0; channel->setNew( hasNew ); if( channel->autoscan() ) m_podcastItemsToScan.append( channel ); } if( !m_podcastItemsToScan.isEmpty() ) m_podcastTimer->start( m_podcastTimerInterval ); } TQMap PlaylistBrowser::loadPodcastFolders( PlaylistCategory *p ) { DEBUG_BLOCK TQString sql = "SELECT * FROM podcastfolders ORDER BY parent ASC;"; TQStringList values = CollectionDB::instance()->query( sql ); // store the folder and IDs so finding a parent is fast TQMap folderMap; PlaylistCategory *folder = 0; foreach( values ) { const int id = (*it).toInt(); const TQString t = *++it; const int parentId = (*++it).toInt(); const bool isOpen = ( (*++it) == CollectionDB::instance()->boolT() ? true : false ); PlaylistCategory *parent = p; if( parentId > 0 && folderMap.find( parentId ) != folderMap.end() ) parent = folderMap[parentId]; folder = new PlaylistCategory( parent, folder, t, id ); folder->setOpen( isOpen ); folderMap[id] = folder; } // check if the base folder exists p->setOpen( Amarok::config( "PlaylistBrowser" )->readBoolEntry( "Podcast Folder Open", true ) ); return folderMap; } void PlaylistBrowser::savePodcastFolderStates( PlaylistCategory *folder ) { if( !folder ) return; PlaylistCategory *child = static_cast(folder->firstChild()); while( child ) { if( isCategory( child ) ) savePodcastFolderStates( child ); else break; child = static_cast(child->nextSibling()); } if( folder != m_podcastCategory ) { if( folder->id() < 0 ) // probably due to a 1.3->1.4 migration { // we add the folder to the db, set the id and then update all the children int parentId = static_cast(folder->parent())->id(); int newId = CollectionDB::instance()->addPodcastFolder( folder->text(0), parentId, folder->isOpen() ); folder->setId( newId ); PodcastChannel *chan = static_cast(folder->firstChild()); while( chan ) { if( isPodcastChannel( chan ) ) // will update the database so child has correct parentId. chan->setParent( folder ); chan = static_cast(chan->nextSibling()); } } else { CollectionDB::instance()->updatePodcastFolder( folder->id(), folder->text(0), static_cast(folder->parent())->id(), folder->isOpen() ); } } } void PlaylistBrowser::scanPodcasts() { //don't want to restart timer unnecessarily. addPodcast will start it if it is necessary if( m_podcastItemsToScan.isEmpty() ) return; for( uint i=0; i < m_podcastItemsToScan.count(); i++ ) { TQListViewItem *item = m_podcastItemsToScan.at( i ); PodcastChannel *pc = static_cast(item); pc->rescan(); } //restart timer m_podcastTimer->start( m_podcastTimerInterval ); } void PlaylistBrowser::refreshPodcasts( TQListViewItem *parent ) { for( TQListViewItem *child = parent->firstChild(); child; child = child->nextSibling() ) { if( isPodcastChannel( child ) ) static_cast( child )->rescan(); else if( isCategory( child ) ) refreshPodcasts( child ); } } void PlaylistBrowser::addPodcast( TQListViewItem *parent ) { bool ok; const TQString name = KInputDialog::getText(i18n("Add Podcast"), i18n("Enter Podcast URL:"), TQString(), &ok, this); if( ok && !name.isEmpty() ) { addPodcast( KURL::fromPathOrURL( name ), parent ); } } void PlaylistBrowser::configurePodcasts( TQListViewItem *parent ) { TQPtrList podcastChannelList; for( TQListViewItem *child = parent->firstChild(); child; child = child->nextSibling() ) { if( isPodcastChannel( child ) ) { podcastChannelList.append( static_cast( child ) ); } } if( !podcastChannelList.isEmpty() ) configurePodcasts( podcastChannelList, i18n( "Podcasts contained in %1", "All in %1").arg( parent->text( 0 ) ) ); } void PlaylistBrowser::configureSelectedPodcasts() { TQPtrList selected; TQListViewItemIterator it( m_listview, TQListViewItemIterator::Selected); for( ; it.current(); ++it ) { if( isPodcastChannel( (*it) ) ) selected.append( static_cast(*it) ); } if (selected.isEmpty() ) return; //shouldn't happen if( selected.count() == 1 ) selected.getFirst()->configure(); else configurePodcasts( selected, i18n("1 Podcast", "%n Podcasts", selected.count() ) ); if( m_podcastItemsToScan.isEmpty() ) m_podcastTimer->stop(); else if( m_podcastItemsToScan.count() == 1 ) m_podcastTimer->start( m_podcastTimerInterval ); // else timer is already running } void PlaylistBrowser::configurePodcasts( TQPtrList &podcastChannelList, const TQString &caption ) { if( podcastChannelList.isEmpty() ) { debug() << "BUG: podcastChannelList is empty" << endl; return; } TQPtrList podcastSettingsList; foreachType( TQPtrList, podcastChannelList) { podcastSettingsList.append( (*it)->getSettings() ); } PodcastSettingsDialog *dialog = new PodcastSettingsDialog( podcastSettingsList, caption ); if( dialog->configure() ) { PodcastChannel *channel = podcastChannelList.first(); foreachType( TQPtrList, podcastSettingsList ) { if ( (*it)->title() == channel->title() ) { channel->setSettings( *it ); } else debug() << " BUG in playlistbrowser.cpp:configurePodcasts( )" << endl; channel = podcastChannelList.next(); } } } PodcastChannel * PlaylistBrowser::findPodcastChannel( const KURL &feed, TQListViewItem *parent ) const { if( !parent ) parent = static_cast(m_podcastCategory); for( TQListViewItem *it = parent->firstChild(); it; it = it->nextSibling() ) { if( isPodcastChannel( it ) ) { PodcastChannel *channel = static_cast( it ); if( channel->url().prettyURL() == feed.prettyURL() ) { return channel; } } else if( isCategory( it ) ) { PodcastChannel *channel = findPodcastChannel( feed, it ); if( channel ) return channel; } } return 0; } PodcastEpisode * PlaylistBrowser::findPodcastEpisode( const KURL &episode, const KURL &feed ) const { PodcastChannel *channel = findPodcastChannel( feed ); if( !channel ) return 0; if( !channel->isPolished() ) channel->load(); TQListViewItem *child = channel->firstChild(); while( child ) { #define child static_cast(child) if( child->url() == episode ) return child; #undef child child = child->nextSibling(); } return 0; } void PlaylistBrowser::addPodcast( const KURL& origUrl, TQListViewItem *parent ) { if( !parent ) parent = static_cast(m_podcastCategory); KURL url( origUrl ); if( url.protocol() == "itpc" || url.protocol() == "pcast" ) url.setProtocol( "http" ); PodcastChannel *channel = findPodcastChannel( url ); if( channel ) { Amarok::StatusBar::instance()->longMessage( i18n( "Already subscribed to feed %1 as %2" ) .arg( url.prettyURL(), channel->title() ), KDE::StatusBar::Sorry ); return; } PodcastChannel *pc = new PodcastChannel( parent, 0, url ); if( m_podcastItemsToScan.isEmpty() ) { m_podcastItemsToScan.append( pc ); m_podcastTimer->start( m_podcastTimerInterval ); } else { m_podcastItemsToScan.append( pc ); } parent->sortChildItems( 0, true ); parent->setOpen( true ); } void PlaylistBrowser::changePodcastInterval() { double time = static_cast(m_podcastTimerInterval / ( 60 * 60 * 1000 )); bool ok; double interval = KInputDialog::getDouble( i18n("Download Interval"), i18n("Scan interval (hours):"), time, 0.5, 100.0, .5, 1, // min, max, step, base &ok, this); int milliseconds = static_cast(interval*60.0*60.0*1000.0); if( ok ) { if( milliseconds != m_podcastTimerInterval ) { m_podcastTimerInterval = milliseconds; m_podcastTimer->changeInterval( m_podcastTimerInterval ); } } } bool PlaylistBrowser::deleteSelectedPodcastItems( const bool removeItem, const bool silent ) { KURL::List urls; TQListViewItemIterator it( m_podcastCategory, TQListViewItemIterator::Selected ); TQPtrList erasedItems; for( ; it.current(); ++it ) { if( isPodcastEpisode( *it ) ) { #define item static_cast(*it) if( item->isOnDisk() ) { urls.append( item->localUrl() ); erasedItems.append( item ); } #undef item } } if( urls.isEmpty() ) return false; int button; if( !silent ) button = KMessageBox::warningContinueCancel( this, i18n( "

You have selected 1 podcast episode to be irreversibly deleted. ", "

You have selected %n podcast episodes to be irreversibly deleted. ", urls.count() ), TQString(), KStdGuiItem::del() ); if( silent || button != KMessageBox::Continue ) return false; TDEIO::Job *job = TDEIO::del( urls ); PodcastEpisode *item; for ( item = erasedItems.first(); item; item = erasedItems.next() ) { if( removeItem ) { CollectionDB::instance()->removePodcastEpisode( item->dBId() ); delete item; } else connect( job, TQT_SIGNAL( result( TDEIO::Job* ) ), item, TQT_SLOT( isOnDisk() ) );; } return true; } bool PlaylistBrowser::deletePodcasts( TQPtrList items ) { if( items.isEmpty() ) return false; KURL::List urls; foreachType( TQPtrList, items ) { for( TQListViewItem *ch = (*it)->firstChild(); ch; ch = ch->nextSibling() ) { #define ch static_cast(ch) if( ch->isOnDisk() ) { //delete downloaded media urls.append( ch->localUrl() ); } #undef ch /// we don't need to delete from the database, because removing the channel from the database /// automatically removes the children as well. m_podcastItemsToScan.remove( static_cast(*it) ); } CollectionDB::instance()->removePodcastChannel( static_cast(*it)->url() ); } // TODO We need to check which files have been deleted successfully if ( urls.count() ) TDEIO::del( urls ); return true; } void PlaylistBrowser::downloadSelectedPodcasts() { TQListViewItemIterator it( m_listview, TQListViewItemIterator::Selected ); for( ; it.current(); ++it ) { if( isPodcastEpisode( *it ) ) { #define item static_cast(*it) if( !item->isOnDisk() ) m_podcastDownloadQueue.append( item ); #undef item } } downloadPodcastQueue(); } void PlaylistBrowser::downloadPodcastQueue() //SLOT { if( m_podcastDownloadQueue.isEmpty() ) return; PodcastEpisode *first = m_podcastDownloadQueue.first(); first->downloadMedia(); m_podcastDownloadQueue.removeFirst(); connect( first, TQT_SIGNAL( downloadFinished() ), this, TQT_SLOT( downloadPodcastQueue() ) ); connect( first, TQT_SIGNAL( downloadAborted() ), this, TQT_SLOT( abortPodcastQueue() ) ); } void PlaylistBrowser::abortPodcastQueue() //SLOT { m_podcastDownloadQueue.clear(); } void PlaylistBrowser::registerPodcastSettings( const TQString &title, const PodcastSettings *settings ) { m_podcastSettings.insert( title, settings ); } /** ************************************************************************* * PLAYLISTS ************************************************************************* **/ TQString PlaylistBrowser::playlistBrowserCache() const { //returns the playlists stats cache file return Amarok::saveLocation() + "playlistbrowser_save.xml"; } PlaylistCategory* PlaylistBrowser::loadPlaylists() { TQFile file( playlistBrowserCache() ); TQTextStream stream( &file ); stream.setEncoding( TQTextStream::UnicodeUTF8 ); TQDomDocument d; TQDomElement e; if( !file.open( IO_ReadOnly ) || !d.setContent( stream.read() ) ) { /*Couldn't open the file or it had invalid content, so let's create an empty element*/ return new PlaylistCategory(m_listview, 0 , i18n("Playlists") ); } else { e = d.namedItem( "category" ).toElement(); if ( e.attribute("formatversion") =="1.1" ) { PlaylistCategory* p = new PlaylistCategory( m_listview, 0 , e ); p->setText( 0, i18n("Playlists") ); return p; } else { // Old unversioned format PlaylistCategory* p = new PlaylistCategory( m_listview, 0 , i18n("Playlists") ); TQListViewItem *last = 0; TQDomNode n = d.namedItem( "playlistbrowser" ).namedItem("playlist"); for ( ; !n.isNull(); n = n.nextSibling() ) last = new PlaylistEntry( p, last, n.toElement() ); return p; } } } TQListViewItem * PlaylistBrowser::findItemInTree( const TQString &searchstring, int c ) const { TQStringList list = TQStringList::split( "/", searchstring, true ); // select the 1st level TQStringList::Iterator it = list.begin(); TQListViewItem *pli = findItem (*it, c); if ( !pli ) return pli; for ( ++it ; it != list.end(); ++it ) { TQListViewItemIterator it2( pli ); for( ++it2 ; it2.current(); ++it2 ) { if ( *it == (*it2)->text(0) ) { pli = *it2; break; } // test, to not go over into the next category if ( isCategory( *it2 ) && (pli->nextSibling() == *it2) ) return 0; } if ( ! it2.current() ) return 0; } return pli; } DynamicMode *PlaylistBrowser::findDynamicModeByTitle( const TQString &title ) { if( !m_polished ) polish(); for ( TQListViewItem *item = m_dynamicCategory->firstChild(); item; item = item->nextSibling() ) { DynamicEntry *entry = dynamic_cast( item ); if ( entry && entry->title() == title ) return entry; } return 0; } PlaylistEntry * PlaylistBrowser::findPlaylistEntry( const TQString &url, TQListViewItem *parent ) const { if( !parent ) parent = static_cast(m_playlistCategory); for( TQListViewItem *it = parent->firstChild(); it; it = it->nextSibling() ) { if( isPlaylist( it ) ) { PlaylistEntry *pl = static_cast( it ); debug() << pl->url().path() << " == " << url << endl; if( pl->url().path() == url ) { debug() << "ok!" << endl; return pl; } } else if( isCategory( it ) ) { PlaylistEntry *pl = findPlaylistEntry( url, it ); if( pl ) return pl; } } return 0; } int PlaylistBrowser::loadPlaylist( const TQString &playlist, bool /*force*/ ) { // roland DEBUG_BLOCK TQListViewItem *pli = findItemInTree( playlist, 0 ); if ( ! pli ) return -1; slotDoubleClicked( pli ); return 0; // roland } void PlaylistBrowser::addPlaylist( const TQString &path, TQListViewItem *parent, bool force, bool imported ) { // this function adds a playlist to the playlist browser if( !m_polished ) polish(); TQFile file( path ); if( !file.exists() ) return; PlaylistEntry *playlist = findPlaylistEntry( path ); if( playlist && force ) playlist->load(); //reload the playlist if( imported ) { TQListViewItem *playlistImports = 0; //First try and find the imported folder for ( TQListViewItem *it = m_playlistCategory->firstChild(); it; it = it->nextSibling() ) { if ( dynamic_cast( it ) && static_cast( it )->isFolder() && it->text( 0 ) == i18n( "Imported" ) ) { playlistImports = it; break; } } if ( !playlistImports ) //We didn't find the Imported folder, so create it. playlistImports = new PlaylistCategory( m_playlistCategory, 0, i18n("Imported") ); parent = playlistImports; } else if( !parent ) parent = static_cast(m_playlistCategory); if( !playlist ) { if( !m_playlistCategory || !m_playlistCategory->childCount() ) { //first child removeButton->setEnabled( true ); renameButton->setEnabled( true ); } KURL auxKURL; auxKURL.setPath(path); m_lastPlaylist = playlist = new PlaylistEntry( parent, 0, auxKURL ); } parent->setOpen( true ); parent->sortChildItems( 0, true ); m_listview->clearSelection(); playlist->setSelected( true ); } bool PlaylistBrowser::savePlaylist( const TQString &path, const TQValueList &in_urls, const TQValueList &titles, const TQValueList &lengths, bool relative ) { if( path.isEmpty() ) return false; TQFile file( path ); if( !file.open( IO_WriteOnly ) ) { KMessageBox::sorry( PlaylistWindow::self(), i18n( "Cannot write playlist (%1).").arg(path) ); return false; } TQTextStream stream( &file ); stream << "#EXTM3U\n"; KURL::List urls; for( int i = 0, n = in_urls.count(); i < n; ++i ) { const KURL &url = in_urls[i]; if( url.isLocalFile() && TQFileInfo( url.path() ).isDir() ) urls += recurse( url ); else urls += url; } for( int i = 0, n = urls.count(); i < n; ++i ) { const KURL &url = urls[i]; if( !titles.isEmpty() && !lengths.isEmpty() ) { stream << "#EXTINF:"; stream << TQString::number( lengths[i] ); stream << ','; stream << titles[i]; stream << '\n'; } if (url.protocol() == "file" ) { if ( relative ) { const TQFileInfo fi(file); stream << KURL::relativePath(fi.dirPath(), url.path()); } else stream << url.path(); } else { stream << url.url(); } stream << "\n"; } file.close(); // Flushes the file, before we read it PlaylistBrowser::instance()->addPlaylist( path, 0, true ); return true; } void PlaylistBrowser::openPlaylist( TQListViewItem *parent ) //SLOT { // open a file selector to add playlists to the playlist browser TQStringList files; files = KFileDialog::getOpenFileNames( TQString(), "*.m3u *.pls *.xspf|" + i18n("Playlist Files"), this, i18n("Import Playlists") ); const TQStringList::ConstIterator end = files.constEnd(); for( TQStringList::ConstIterator it = files.constBegin(); it != end; ++it ) addPlaylist( *it, parent ); savePlaylists(); } void PlaylistBrowser::savePlaylists() { TQFile file( playlistBrowserCache() ); TQDomDocument doc; TQDomElement playlistsB = m_playlistCategory->xml(); playlistsB.setAttribute( "product", "Amarok" ); playlistsB.setAttribute( "version", APP_VERSION ); playlistsB.setAttribute( "formatversion", "1.1" ); TQDomNode playlistsNode = doc.importNode( playlistsB, true ); doc.appendChild( playlistsNode ); TQString temp( doc.toString() ); // Only open the file after all data is ready. If it crashes, data is not lost! if ( !file.open( IO_WriteOnly ) ) return; TQTextStream stream( &file ); stream.setEncoding( TQTextStream::UnicodeUTF8 ); stream << "\n"; stream << temp; } bool PlaylistBrowser::deletePlaylists( TQPtrList items ) { KURL::List urls; foreachType( TQPtrList, items ) { urls.append( (*it)->url() ); } if( !urls.isEmpty() ) return deletePlaylists( urls ); return false; } bool PlaylistBrowser::deletePlaylists( KURL::List items ) { if ( items.isEmpty() ) return false; // TODO We need to check which files have been deleted successfully // Avoid deleting dirs. See bug #122480 for ( KURL::List::iterator it = items.begin(), end = items.end(); it != end; ++it ) { if ( TQFileInfo( (*it).path() ).isDir() ) { it = items.remove( it ); continue; } } TDEIO::del( items ); return true; } void PlaylistBrowser::savePlaylist( PlaylistEntry *item ) { bool append = false; if( item->trackList().count() == 0 ) //the playlist hasn't been loaded so we append the dropped tracks append = true; //save the modified playlist in m3u or pls format const TQString ext = fileExtension( item->url().path() ); if( ext.lower() == "m3u" ) saveM3U( item, append ); else if ( ext.lower() == "xspf" ) saveXSPF( item, append ); else savePLS( item, append ); } /** ************************************************************************* * General Methods ************************************************************************* **/ PlaylistBrowserEntry * PlaylistBrowser::findItem( TQString &t, int c ) const { return static_cast( m_listview->findItem( t, c, TQt::ExactMatch ) ); } bool PlaylistBrowser::createPlaylist( TQListViewItem *parent, bool current, TQString title ) { if( title.isEmpty() ) title = i18n("Untitled"); const TQString path = PlaylistDialog::getSaveFileName( title ); if( path.isEmpty() ) return false; if( !parent ) parent = static_cast( m_playlistCategory ); if( current ) { if ( !Playlist::instance()->saveM3U( path ) ) { return false; } } else { //Remove any items in Listview that have the same path as this one // Should only happen when overwriting a playlist TQListViewItem *item = parent->firstChild(); while( item ) { if( static_cast( item )->url() == path ) { TQListViewItem *todelete = item; item = item->nextSibling(); delete todelete; } else item = item->nextSibling(); } //Remove existing playlist if it exists if ( TQFileInfo( path ).exists() ) TQFileInfo( path ).dir().remove( path ); m_lastPlaylist = new PlaylistEntry( parent, 0, path ); parent->sortChildItems( 0, true ); } savePlaylists(); return true; } void PlaylistBrowser::addSelectedToPlaylist( int options ) { if ( options == -1 ) options = Playlist::Unique | Playlist::Append; KURL::List list; TQListViewItemIterator it( m_listview, TQListViewItemIterator::Selected ); for( ; it.current(); ++it ) { #define item (*it) if ( isPlaylist( item ) ) list << static_cast(item)->url(); else if( isLastFm( item ) ) list << static_cast(item)->url(); else if ( isStream( item ) ) list << static_cast(item)->url(); else if ( isPodcastChannel( item ) ) { #define channel static_cast(item) if( !channel->isPolished() ) channel->load(); #undef channel KURL::List _list; TQListViewItem *child = item->firstChild(); while( child ) { #define child static_cast(child) child->isOnDisk() ? _list.prepend( child->localUrl() ): _list.prepend( child->url() ); #undef child child = child->nextSibling(); } list += _list ; } else if ( isPodcastEpisode( item ) ) { #define pod static_cast(item) if( pod->isOnDisk() ) list << pod->localUrl(); else list << pod->url(); #undef pod } else if ( isPlaylistTrackItem( item ) ) list << static_cast(item)->url(); #undef item } if( !list.isEmpty() ) Playlist::instance()->insertMedia( list, options ); } void PlaylistBrowser::invokeItem( TQListViewItem* i, const TQPoint& point, int column ) //SLOT { if( column == -1 ) return; PlaylistBrowserView *view = getListView(); TQPoint p = mapFromGlobal( point ); if ( p.x() > view->header()->sectionPos( view->header()->mapToIndex( 0 ) ) + view->treeStepSize() * ( i->depth() + ( view->rootIsDecorated() ? 1 : 0) ) + view->itemMargin() || p.x() < view->header()->sectionPos( view->header()->mapToIndex( 0 ) ) ) slotDoubleClicked( i ); } void PlaylistBrowser::slotDoubleClicked( TQListViewItem *item ) //SLOT { if( !item ) return; PlaylistBrowserEntry *entry = dynamic_cast(item); if ( entry ) entry->slotDoubleClicked(); } void PlaylistBrowser::collectionScanDone() { if( !m_polished || CollectionDB::instance()->isEmpty() ) { return; } else if( !m_smartCategory ) { m_smartCategory = loadSmartPlaylists(); loadDefaultSmartPlaylists(); m_smartCategory->setOpen( true ); } } void PlaylistBrowser::removeSelectedItems() //SLOT { // this function remove selected playlists and tracks int playlistCount = 0; int trackCount = 0; int streamCount = 0; int smartyCount = 0; int dynamicCount = 0; int podcastCount = 0; int folderCount = 0; int lastfmCount = 0; TQPtrList playlistsToDelete; TQPtrList podcastsToDelete; TQPtrList playlistFoldersToDelete; TQPtrList podcastFoldersToDelete; //remove currentItem, no matter if selected or not m_listview->setSelected( m_listview->currentItem(), true ); TQPtrList selected; TQListViewItemIterator it( m_listview, TQListViewItemIterator::Selected ); for( ; it.current(); ++it ) { if( !static_cast(*it)->isKept() ) continue; if( isCategory( *it ) && !static_cast(*it)->isFolder() ) //its a base category continue; // if the playlist containing this item is already selected the current item will be skipped // it will be deleted from the parent TQListViewItem *parent = it.current()->parent(); if( parent && parent->isSelected() ) //parent will remove children continue; if (parent) { while( parent->parent() && static_cast(parent)->isKept() ) parent = parent->parent(); } if( parent && !static_cast(parent)->isKept() ) continue; switch( (*it)->rtti() ) { case PlaylistEntry::RTTI: playlistsToDelete.append( static_cast(*it) ); playlistCount++; continue; // don't add the folder to selected, else it will be deleted twice case PlaylistTrackItem::RTTI: trackCount++; break; case LastFmEntry::RTTI: lastfmCount++; break; case StreamEntry::RTTI: streamCount++; break; case DynamicEntry::RTTI: dynamicCount++; break; case SmartPlaylist::RTTI: smartyCount++; break; case PodcastChannel::RTTI: podcastCount++; podcastsToDelete.append( static_cast(*it) ); case PodcastEpisode::RTTI: //episodes can't be removed continue; // don't add the folder to selected, else it will be deleted twice case PlaylistCategory::RTTI: folderCount++; if( parent == m_playlistCategory ) { for( TQListViewItem *ch = (*it)->firstChild(); ch; ch = ch->nextSibling() ) { if( isCategory( ch ) ) { folderCount++; playlistFoldersToDelete.append( static_cast(ch) ); } else { playlistCount++; playlistsToDelete.append( static_cast(ch) ); } } playlistFoldersToDelete.append( static_cast(*it) ); continue; // don't add the folder to selected, else it will be deleted twice } else if( parent == m_podcastCategory ) { for( TQListViewItem *ch = (*it)->firstChild(); ch; ch = ch->nextSibling() ) { if( isCategory( ch ) ) { folderCount++; podcastFoldersToDelete.append( static_cast(ch) ); } else { podcastCount++; podcastsToDelete.append( static_cast(ch) ); } } podcastFoldersToDelete.append( static_cast(*it) ); continue; // don't add the folder to selected, else it will be deleted twice } default: break; } selected.append( it.current() ); } int totalCount = playlistCount + smartyCount + dynamicCount + streamCount + podcastCount + folderCount + lastfmCount; if( selected.isEmpty() && !totalCount ) return; TQString message = i18n( "

You have selected:

    " ); if( playlistCount ) message += "
  • " + i18n( "1 playlist", "%n playlists", playlistCount ) + "
  • "; if( smartyCount ) message += "
  • " + i18n( "1 smart playlist", "%n smart playlists", smartyCount ) + "
  • "; if( dynamicCount ) message += "
  • " + i18n( "1 dynamic playlist", "%n dynamic playlists", dynamicCount ) + "
  • "; if( streamCount ) message += "
  • " + i18n( "1 stream", "%n streams", streamCount ) + "
  • "; if( podcastCount ) message += "
  • " + i18n( "1 podcast", "%n podcasts", podcastCount ) + "
  • "; if( folderCount ) message += "
  • " + i18n( "1 folder", "%n folders", folderCount ) + "
  • "; if( lastfmCount ) message += "
  • " + i18n( "1 last.fm stream", "%n last.fm streams", lastfmCount ) + "
  • "; message += i18n( "

to be irreversibly deleted.

" ); if( podcastCount ) message += i18n( "

All downloaded podcast episodes will also be deleted.

" ); if( totalCount > 0 ) { int button = KMessageBox::warningContinueCancel( this, message, TQString(), KStdGuiItem::del() ); if( button != KMessageBox::Continue ) return; } foreachType( TQPtrList, selected ) { if ( isPlaylistTrackItem( *it ) ) { static_cast( (*it)->parent() )->removeTrack( (*it) ); continue; } if ( isDynamic( *it ) ) static_cast( *it )->deleting(); delete (*it); } // used for deleting playlists first, then folders. if( playlistCount ) { if( deletePlaylists( playlistsToDelete ) ) { foreachType( TQPtrList, playlistsToDelete ) { m_dynamicEntries.remove(*it); delete (*it); } } } if( podcastCount ) { if( deletePodcasts( podcastsToDelete ) ) foreachType( TQPtrList, podcastsToDelete ) delete (*it); } foreachType( TQPtrList, playlistFoldersToDelete ) delete (*it); foreachType( TQPtrList, podcastFoldersToDelete ) removePodcastFolder( *it ); if( playlistCount || trackCount ) savePlaylists(); if( streamCount ) saveStreams(); if( smartyCount ) saveSmartPlaylists(); if( dynamicCount ) saveDynamics(); if( lastfmCount ) saveLastFm(); } // remove podcast folders. we need to do this recursively to ensure all children are removed from the db void PlaylistBrowser::removePodcastFolder( PlaylistCategory *item ) { if( !item ) return; if( !item->childCount() ) { CollectionDB::instance()->removePodcastFolder( item->id() ); delete item; return; } TQListViewItem *child = item->firstChild(); while( child ) { TQListViewItem *nextChild = 0; if( isPodcastChannel( child ) ) { #define child static_cast(child) nextChild = child->nextSibling(); CollectionDB::instance()->removePodcastChannel( child->url() ); m_podcastItemsToScan.remove( child ); #undef child } else if( isCategory( child ) ) { nextChild = child->nextSibling(); removePodcastFolder( static_cast(child) ); } child = nextChild; } CollectionDB::instance()->removePodcastFolder( item->id() ); delete item; } void PlaylistBrowser::renameSelectedItem() //SLOT { TQListViewItem *item = m_listview->currentItem(); if( !item ) return; if( item == m_randomDynamic || item == m_suggestedDynamic ) return; PlaylistBrowserEntry *entry = dynamic_cast( item ); if ( entry ) entry->slotRenameItem(); } void PlaylistBrowser::renamePlaylist( TQListViewItem* item, const TQString& newName, int ) //SLOT { PlaylistBrowserEntry *entry = dynamic_cast( item ); if ( entry ) entry->slotPostRenameItem( newName ); } void PlaylistBrowser::saveM3U( PlaylistEntry *item, bool append ) { TQFile file( item->url().path() ); if( append ? file.open( IO_WriteOnly | IO_Append ) : file.open( IO_WriteOnly ) ) { TQTextStream stream( &file ); if( !append ) stream << "#EXTM3U\n"; TQPtrList trackList = append ? item->droppedTracks() : item->trackList(); for( TrackItemInfo *info = trackList.first(); info; info = trackList.next() ) { stream << "#EXTINF:"; stream << info->length(); stream << ','; stream << info->title(); stream << '\n'; stream << (info->url().protocol() == "file" ? info->url().path() : info->url().url()); stream << "\n"; } file.close(); } } void PlaylistBrowser::saveXSPF( PlaylistEntry *item, bool append ) { XSPFPlaylist* playlist = new XSPFPlaylist(); playlist->setCreator( "Amarok" ); playlist->setTitle( item->text(0) ); XSPFtrackList list; TQPtrList trackList = append ? item->droppedTracks() : item->trackList(); for( TrackItemInfo *info = trackList.first(); info; info = trackList.next() ) { XSPFtrack track; MetaBundle b( info->url() ); track.creator = b.artist(); track.title = b.title(); track.location = b.url().url(); list.append( track ); } playlist->setTrackList( list, append ); TQFile file( item->url().path() ); if ( !file.open( IO_WriteOnly ) ) warning() << "Could not open file " << file.name() << " write-only" << endl; else { TQTextStream stream ( &file ); playlist->save( stream, 2 ); file.close(); } } void PlaylistBrowser::savePLS( PlaylistEntry *item, bool append ) { TQFile file( item->url().path() ); if( append ? file.open( IO_WriteOnly | IO_Append ) : file.open( IO_WriteOnly ) ) { TQTextStream stream( &file ); TQPtrList trackList = append ? item->droppedTracks() : item->trackList(); stream << "NumberOfEntries=" << trackList.count() << endl; int c=1; for( TrackItemInfo *info = trackList.first(); info; info = trackList.next(), ++c ) { stream << "File" << c << "="; stream << (info->url().protocol() == "file" ? info->url().path() : info->url().url()); stream << "\nTitle" << c << "="; stream << info->title(); stream << "\nLength" << c << "="; stream << info->length(); stream << "\n"; } stream << "Version=2\n"; file.close(); } } #include #include #include "playlistloader.h" //this function (C) Copyright 2003-4 Max Howell, (C) Copyright 2004 Mark Kretschmann KURL::List PlaylistBrowser::recurse( const KURL &url ) { typedef TQMap FileMap; KDirLister lister( false ); lister.setAutoUpdate( false ); lister.setAutoErrorHandlingEnabled( false, 0 ); lister.openURL( url ); while( !lister.isFinished() ) kapp->eventLoop()->processEvents( TQEventLoop::ExcludeUserInput ); KFileItemList items = lister.items(); //returns TQPtrList, so we MUST only do it once! KURL::List urls; FileMap files; for( KFileItem *item = items.first(); item; item = items.next() ) { if( item->isFile() ) { files[item->name()] = item->url(); continue; } if( item->isDir() ) urls += recurse( item->url() ); } foreachType( FileMap, files ) // users often have playlist files that reflect directories // higher up, or stuff in this directory. Don't add them as // it produces double entries if( !PlaylistFile::isPlaylistFile( (*it).fileName() ) ) urls += *it; return urls; } void PlaylistBrowser::currentItemChanged( TQListViewItem *item ) //SLOT { // rename remove and delete buttons are disabled if there are no playlists // rename and delete buttons are disabled for track items bool enable_remove = false; bool enable_rename = false; if ( !item ) goto enable_buttons; if ( isCategory( item ) ) { if( static_cast(item)->isFolder() && static_cast(item)->isKept() ) enable_remove = enable_rename = true; } else if ( isPodcastChannel( item ) ) { enable_remove = true; enable_rename = false; } else if ( !isPodcastEpisode( item ) ) enable_remove = enable_rename = static_cast(item)->isKept(); static_cast(item)->updateInfo(); enable_buttons: removeButton->setEnabled( enable_remove ); renameButton->setEnabled( enable_rename ); } void PlaylistBrowser::customEvent( TQCustomEvent *e ) { // If a playlist is found in collection folders it will be automatically added to the playlist browser // The ScanController sends a PlaylistFoundEvent when a playlist is found. ScanController::PlaylistFoundEvent* p = static_cast( e ); addPlaylist( p->path(), 0, false, true ); } void PlaylistBrowser::slotAddMenu( int id ) //SLOT { switch( id ) { case STREAM: addStream(); break; case SMARTPLAYLIST: addSmartPlaylist(); break; case PODCAST: addPodcast(); break; case ADDDYNAMIC: ConfigDynamic::dynamicDialog(this); break; } } void PlaylistBrowser::slotAddPlaylistMenu( int id ) //SLOT { switch( id ) { case PLAYLIST: createPlaylist( 0/*base cat*/, false/*make empty*/ ); break; case PLAYLIST_IMPORT: openPlaylist(); break; } } /** ************************ * Context Menu Entries ************************ **/ void PlaylistBrowser::showContextMenu( TQListViewItem *item, const TQPoint &p, int ) //SLOT { if( !item ) return; PlaylistBrowserEntry *entry = dynamic_cast( item ); if ( entry ) entry->showContextMenu( p ); } ///////////////////////////////////////////////////////////////////////////// // CLASS PlaylistBrowserView //////////////////////////////////////////////////////////////////////////// PlaylistBrowserView::PlaylistBrowserView( TQWidget *parent, const char *name ) : TDEListView( parent, name ) , m_marker( 0 ) { addColumn( i18n("Playlists") ); setSelectionMode( TQListView::Extended ); setResizeMode( TQListView::AllColumns ); setShowSortIndicator( true ); setRootIsDecorated( true ); setDropVisualizer( true ); //the visualizer (a line marker) is drawn when dragging over tracks setDropHighlighter( true ); //and the highligther (a focus rect) is drawn when dragging over playlists setDropVisualizerWidth( 3 ); setAcceptDrops( true ); setTreeStepSize( 20 ); connect( this, TQT_SIGNAL( mouseButtonPressed ( int, TQListViewItem *, const TQPoint &, int ) ), this, TQT_SLOT( mousePressed( int, TQListViewItem *, const TQPoint &, int ) ) ); //TODO moving tracks //connect( this, TQT_SIGNAL( moved(TQListViewItem *, TQListViewItem *, TQListViewItem * )), // this, TQT_SLOT( itemMoved(TQListViewItem *, TQListViewItem *, TQListViewItem * ))); } PlaylistBrowserView::~PlaylistBrowserView() { } void PlaylistBrowserView::contentsDragEnterEvent( TQDragEnterEvent *e ) { e->accept( e->source() == viewport() || KURLDrag::canDecode( e ) ); } void PlaylistBrowserView::contentsDragMoveEvent( TQDragMoveEvent* e ) { //Get the closest item _before_ the cursor const TQPoint p = contentsToViewport( e->pos() ); TQListViewItem *item = itemAt( p ); if( !item ) { eraseMarker(); return; } //only for track items (for playlist items we draw the highlighter) if( isPlaylistTrackItem( item ) ) item = item->itemAbove(); if( item != m_marker ) { eraseMarker(); m_marker = item; viewportPaintEvent( 0 ); } } void PlaylistBrowserView::contentsDragLeaveEvent( TQDragLeaveEvent* ) { eraseMarker(); } void PlaylistBrowserView::contentsDropEvent( TQDropEvent *e ) { TQListViewItem *parent = 0; TQListViewItem *after; const TQPoint p = contentsToViewport( e->pos() ); TQListViewItem *item = itemAt( p ); if( !item ) { eraseMarker(); return; } if( !isPlaylist( item ) ) findDrop( e->pos(), parent, after ); eraseMarker(); if( e->source() == this ) { moveSelectedItems( item ); // D&D sucks, do it ourselves } else { KURL::List decodedList; TQValueList bundles; if( KURLDrag::decode( e, decodedList ) ) { KURL::List::ConstIterator it = decodedList.begin(); MetaBundle first( *it ); const TQString album = first.album(); const TQString artist = first.artist(); int suggestion = !album.stripWhiteSpace().isEmpty() ? 1 : !artist.stripWhiteSpace().isEmpty() ? 2 : 3; for ( ; it != decodedList.end(); ++it ) { if( isCategory(item) ) { // check if it is podcast category TQListViewItem *cat = item; while( isCategory(cat) && cat!=PlaylistBrowser::instance()->podcastCategory() ) cat = cat->parent(); if( cat == PlaylistBrowser::instance()->podcastCategory() ) PlaylistBrowser::instance()->addPodcast(*it, item); continue; } TQString filename = (*it).fileName(); if( filename.endsWith("m3u") || filename.endsWith("pls") ) PlaylistBrowser::instance()->addPlaylist( (*it).path() ); else if( ContextBrowser::hasContextProtocol( *it ) ) { KURL::List urls = ContextBrowser::expandURL( *it ); for( KURL::List::iterator i = urls.begin(); i != urls.end(); i++ ) { MetaBundle mb(*i); bundles.append( mb ); } } else //TODO: check canDecode ? { MetaBundle mb(*it); bundles.append( mb ); if( suggestion == 1 && mb.album()->lower().stripWhiteSpace() != album.lower().stripWhiteSpace() ) suggestion = 2; if( suggestion == 2 && mb.artist()->lower().stripWhiteSpace() != artist.lower().stripWhiteSpace() ) suggestion = 3; } } if( bundles.isEmpty() ) return; if( parent && isPlaylist( parent ) ) { //insert the dropped tracks PlaylistEntry *playlist = static_cast( parent ); playlist->insertTracks( after, bundles ); } else //dropped on a playlist item { TQListViewItem *parent = item; bool isPlaylistFolder = false; while( parent ) { if( parent == PlaylistBrowser::instance()->m_playlistCategory ) { isPlaylistFolder = true; break; } parent = parent->parent(); } if( isPlaylist( item ) ) { PlaylistEntry *playlist = static_cast( item ); //append the dropped tracks playlist->insertTracks( 0, bundles ); } else if( isCategory( item ) && isPlaylistFolder ) { PlaylistBrowser *pb = PlaylistBrowser::instance(); TQString title = suggestion == 1 ? album : suggestion == 2 ? artist : TQString(); if ( pb->createPlaylist( item, false, title ) ) pb->m_lastPlaylist->insertTracks( 0, bundles ); } } } else e->ignore(); } } void PlaylistBrowserView::eraseMarker() //SLOT { if( m_marker ) { TQRect spot; if( isPlaylist( m_marker ) ) spot = drawItemHighlighter( 0, m_marker ); else spot = drawDropVisualizer( 0, 0, m_marker ); m_marker = 0; viewport()->repaint( spot, false ); } } void PlaylistBrowserView::viewportPaintEvent( TQPaintEvent *e ) { if( e ) TDEListView::viewportPaintEvent( e ); //we call with 0 in contentsDropEvent() if( m_marker ) { TQPainter painter( viewport() ); if( isPlaylist( m_marker ) ) //when dragging on a playlist we draw a focus rect drawItemHighlighter( &painter, m_marker ); else //when dragging on a track we draw a line marker painter.fillRect( drawDropVisualizer( 0, 0, m_marker ), TQBrush( colorGroup().highlight(), TQBrush::Dense4Pattern ) ); } } void PlaylistBrowserView::mousePressed( int button, TQListViewItem *item, const TQPoint &pnt, int ) //SLOT { // this function expande/collapse the playlist if the +/- symbol has been pressed // and show the save menu if the save icon has been pressed if( !item || button != Qt::LeftButton ) return; if( isPlaylist( item ) ) { TQPoint p = mapFromGlobal( pnt ); p.setY( p.y() - header()->height() ); TQRect itemrect = itemRect( item ); TQRect expandRect = TQRect( 4, itemrect.y() + (item->height()/2) - 5, 15, 15 ); if( expandRect.contains( p ) ) { //expand symbol clicked setOpen( item, !item->isOpen() ); return; } } } void PlaylistBrowserView::moveSelectedItems( TQListViewItem *newParent ) { if( !newParent ) return; TQListViewItem *after=0; if( isDynamic( newParent ) || isPodcastChannel( newParent ) || isSmartPlaylist( newParent ) || isPodcastEpisode( newParent ) || isStream( newParent ) ) { after = newParent; newParent = newParent->parent(); } #define newParent static_cast(newParent) if( !newParent->isKept() ) return; #undef newParent TQPtrList selected; TQListViewItemIterator it( this, TQListViewItemIterator::Selected ); for( ; it.current(); ++it ) { if( !(*it)->parent() ) //must be a base category we are draggin' continue; selected.append( *it ); } for( TQListViewItem *item = selected.first(); item; item = selected.next() ) { TQListViewItem *itemParent = item->parent(); if( isPlaylistTrackItem( item ) ) { if( isPlaylistTrackItem( newParent ) ) { if( !after && newParent != newParent->parent()->firstChild() ) after = newParent->itemAbove(); newParent = static_cast(newParent->parent()); } else if( !isPlaylist( newParent ) ) continue; #define newParent static_cast(newParent) newParent->insertTracks( after, KURL::List( static_cast(item)->url() )); #undef newParent #define itemParent static_cast(itemParent) itemParent->removeTrack( static_cast(item) ); #undef itemParent continue; } else if( !isCategory( newParent ) ) continue; TQListViewItem *base = newParent; while( base->parent() ) base = base->parent(); if( base == PlaylistBrowser::instance()->m_playlistCategory && isPlaylist( item ) || base == PlaylistBrowser::instance()->m_streamsCategory && isStream( item ) || base == PlaylistBrowser::instance()->m_smartCategory && isSmartPlaylist( item ) || base == PlaylistBrowser::instance()->m_dynamicCategory && isDynamic( item ) ) { // if the item is from the cool streams dir, copy it. if( item->parent() == PlaylistBrowser::instance()->m_coolStreams ) { #define item static_cast(item) new StreamEntry( newParent, after, item->url(), item->title() ); #undef item } else // otherwise, we move it { itemParent->takeItem( item ); newParent->insertItem( item ); } newParent->sortChildItems( 0, true ); } else if( base == PlaylistBrowser::instance()->m_podcastCategory && isPodcastChannel( item ) ) { #define item static_cast(item) item->setParent( static_cast(newParent) ); #undef item } } } void PlaylistBrowserView::rename( TQListViewItem *item, int c ) { TDEListView::rename( item, c ); TQRect rect( itemRect( item ) ); int fieldX = rect.x() + treeStepSize() + 2; int fieldW = rect.width() - treeStepSize() - 2; KLineEdit *renameEdit = renameLineEdit(); renameEdit->setGeometry( fieldX, rect.y(), fieldW, rect.height() ); renameEdit->show(); } void PlaylistBrowserView::keyPressEvent( TQKeyEvent *e ) { switch( e->key() ) { case Key_Enter: case Key_Return: { if (e->state() & TQt::ShiftButton) { // load and play PlaylistBrowser::instance()->slotDoubleClicked( currentItem() ); } else { // load TQListViewItem *item = currentItem(); if (item) { Playlist::instance()->clear(); Playlist::instance()->setPlaylistName( item->text(0), true ); PlaylistBrowser::instance()->addSelectedToPlaylist( Playlist::Append ); } } break; } case Key_Delete: // delete if (e->state() & TQt::ShiftButton) { PlaylistBrowser::instance()->removeSelectedItems(); break; } case Key_F2: if (e->state() & TQt::ShiftButton) { //rename PlaylistBrowser::instance()->renameSelectedItem(); break; } default: TDEListView::keyPressEvent( e ); break; } } void PlaylistBrowserView::startDrag() { KURL::List urls; KURL::List itemList; // this is for CollectionDB::createDragPixmap() KURL::List podList; // used to add podcast episodes of the same channel in reverse order (usability) PodcastEpisode *lastPodcastEpisode = 0; // keep track of the last podcastepisode we visited. KMultipleDrag *drag = new KMultipleDrag( this ); TQListViewItemIterator it( this, TQListViewItemIterator::Selected ); TQString pixText = TQString(); uint count = 0; for( ; it.current(); ++it ) { if( !isPodcastEpisode( *it ) && !podList.isEmpty() ) { // we left the podcast channel, so append those items we iterated over urls += podList; podList.clear(); } if( isPlaylist( *it ) ) { urls += static_cast(*it)->url(); itemList += static_cast(*it)->url(); pixText = (*it)->text(0); } else if( isStream( *it ) ) { urls += static_cast(*it)->url(); itemList += KURL::fromPathOrURL( "stream://" ); pixText = (*it)->text(0); } else if( isLastFm( *it ) ) { urls += static_cast(*it)->url(); itemList += static_cast(*it)->url(); pixText = (*it)->text(0); } else if( isPodcastEpisode( *it ) ) { if( (*it)->parent()->isSelected() ) continue; if( !podList.isEmpty() && lastPodcastEpisode && lastPodcastEpisode->TQListViewItem::parent() != (*it)->parent() ) { // we moved onto a new podcast channel urls += podList; podList.clear(); } #define item static_cast(*it) if( item->isOnDisk() ) { podList.prepend( item->localUrl() ); itemList += item->url(); } else { podList.prepend( item->url() ); itemList += item->url(); } lastPodcastEpisode = item; pixText = (*it)->text(0); #undef item } else if( isPodcastChannel( *it ) ) { #define item static_cast(*it) if( !item->isPolished() ) item->load(); TQListViewItem *child = item->firstChild(); KURL::List tmp; // we add the podcasts in reverse, its much nicer to add them chronologically :) while( child ) { PodcastEpisode *pe = static_cast( child ); if( pe->isOnDisk() ) tmp.prepend( pe->localUrl() ); else tmp.prepend( pe->url() ); child = child->nextSibling(); } urls += tmp; itemList += KURL::fromPathOrURL( item->url().url() ); pixText = (*it)->text(0); #undef item } else if( isSmartPlaylist( *it ) ) { SmartPlaylist *item = static_cast( *it ); if( !item->query().isEmpty() ) { TQTextDrag *textdrag = new TQTextDrag( item->text(0) + '\n' + item->query(), 0 ); textdrag->setSubtype( "amarok-sql" ); drag->addDragObject( textdrag ); } itemList += KURL::fromPathOrURL( TQString("smartplaylist://%1").arg( item->text(0) ) ); pixText = (*it)->text(0); } else if( isDynamic( *it ) ) { DynamicEntry *item = static_cast( *it ); // Serialize pointer to string const TQString str = TQString::number( reinterpret_cast( item ) ); TQTextDrag *textdrag = new TQTextDrag( str, 0 ); textdrag->setSubtype( "dynamic" ); drag->addDragObject( textdrag ); itemList += KURL::fromPathOrURL( TQString("dynamic://%1").arg( item->text(0) ) ); pixText = (*it)->text(0); } else if( isPlaylistTrackItem( *it ) ) { if( (*it)->parent()->isSelected() ) continue; urls += static_cast(*it)->url(); itemList += static_cast(*it)->url(); } count++; } if( !podList.isEmpty() ) urls += podList; if( count > 1 ) pixText = TQString(); drag->addDragObject( new KURLDrag( urls, viewport() ) ); drag->setPixmap( CollectionDB::createDragPixmap( itemList, pixText ), TQPoint( CollectionDB::DRAGPIXMAP_OFFSET_X, CollectionDB::DRAGPIXMAP_OFFSET_Y ) ); drag->dragCopy(); } ///////////////////////////////////////////////////////////////////////////// // CLASS PlaylistDialog //////////////////////////////////////////////////////////////////////////// TQString PlaylistDialog::getSaveFileName( const TQString &suggestion, bool proposeOverwriting ) //static { PlaylistDialog dialog; if( !suggestion.isEmpty() ) { TQString path = Amarok::saveLocation("playlists/") + "%1" + ".m3u"; if( TQFileInfo( path.arg( suggestion ) ).exists() && !proposeOverwriting ) { int n = 2; while( TQFileInfo( path.arg( i18n( "%1 (%2)" ).arg( suggestion, TQString::number( n ) ) ) ).exists() ) n++; dialog.edit->setText( i18n( "%1 (%2)" ).arg( suggestion, TQString::number( n ) ) ); } else dialog.edit->setText( suggestion ); } if( dialog.exec() == Accepted ) return dialog.result; return TQString(); } PlaylistDialog::PlaylistDialog() : KDialogBase( PlaylistWindow::self(), "saveplaylist", true /*modal*/, i18n( "Save Playlist" ), Ok | Cancel | User1, Ok, false /*separator*/, KGuiItem( i18n( "Save to location..." ), SmallIconSet( Amarok::icon( "files" ) ) ) ) , customChosen( false ) { TQVBox *vbox = makeVBoxMainWidget(); TQLabel *label = new TQLabel( i18n( "&Enter a name for the playlist:" ), vbox ); edit = new KLineEdit( vbox ); edit->setFocus(); label->setBuddy( edit ); enableButtonOK( false ); connect( edit, TQT_SIGNAL( textChanged( const TQString & ) ), this, TQT_SLOT( slotTextChanged( const TQString& ) ) ); connect( this, TQT_SIGNAL( user1Clicked() ), TQT_SLOT( slotCustomPath() ) ); } void PlaylistDialog::slotOk() { // TODO Remove this hack for 1.2. It's needed because playlists was a file once. TQString folder = Amarok::saveLocation( "playlists" ); TQFileInfo info( folder ); if ( !info.isDir() ) TQFile::remove( folder ); if( !customChosen && !edit->text().isEmpty() ) result = Amarok::saveLocation( "playlists/" ) + edit->text() + ".m3u"; if( !TQFileInfo( result ).exists() || KMessageBox::warningContinueCancel( PlaylistWindow::self(), i18n( "A playlist named \"%1\" already exists. Do you want to overwrite it?" ).arg( edit->text() ), i18n( "Overwrite Playlist?" ), i18n( "Overwrite" ) ) == KMessageBox::Continue ) { KDialogBase::slotOk(); } } void PlaylistDialog::slotTextChanged( const TQString &s ) { enableButtonOK( !s.isEmpty() ); } void PlaylistDialog::slotCustomPath() { result = KFileDialog::getSaveFileName( ":saveplaylists", "*.m3u" ); if( !result.isNull() ) { edit->setText( result ); edit->setReadOnly( true ); enableButtonOK( true ); customChosen = true; } } InfoPane::InfoPane( TQWidget *parent ) : TQVBox( parent ), m_enable( false ), m_storedHeight( 100 ) { TQFrame *container = new TQVBox( this, "container" ); container->hide(); { TQFrame *box = new TQHBox( container ); box->setMargin( 3 ); box->setBackgroundMode( TQt::PaletteBase ); m_infoBrowser = new HTMLView( box, "extended_info", false /*DNDEnabled*/, false /*JS enabled*/ ); container->setFrameStyle( TQFrame::StyledPanel ); container->setMargin( 3 ); container->setBackgroundMode( TQt::PaletteBase ); } m_pushButton = new KPushButton( KGuiItem( i18n("&Show Extended Info"), "info" ), this ); m_pushButton->setToggleButton( true ); m_pushButton->setEnabled( m_enable ); connect( m_pushButton, TQT_SIGNAL(toggled( bool )), TQT_SLOT(toggle( bool )) ); //Set the height to fixed. The button shouldn't be resized. setFixedHeight( m_pushButton->sizeHint().height() ); } InfoPane::~InfoPane() { // Ensure the TDEHTMLPart dies before its TDEHTMLView dies, // because TDEHTMLPart's dtoring relies on its TDEHTMLView still being alive // (see bug 130494). delete m_infoBrowser; } int InfoPane::getHeight() { if( TQT_TQWIDGET( child( "container" ) )->isShown() ) { //If the InfoPane is shown, return true height. return static_cast( parentWidget() )->sizes().last(); } return m_storedHeight; } void InfoPane::setStoredHeight( const int newHeight ) { m_storedHeight = newHeight; } void InfoPane::toggle( bool toggled ) { TQSplitter *splitter = static_cast( parentWidget() ); if ( !toggled ) { //Save the height for later setStoredHeight( splitter->sizes().last() ); //Set the height to fixed. The button shouldn't be resized. setFixedHeight( m_pushButton->sizeHint().height() ); //Now the info pane is not shown, we can disable the button if necessary m_pushButton->setEnabled( m_enable ); } else { setMaximumHeight( ( int )( parentWidget()->height() / 1.5 ) ); //Restore the height of the InfoPane (change the splitter properties) //Done every time since the pane forgets its height if you try to resize it while the info is hidden. TQValueList sizes = splitter->sizes(); const int sizeOffset = getHeight() - sizes.last(); sizes.first() -= sizeOffset; sizes.last() += sizeOffset; splitter->setSizes( sizes ); setMinimumHeight( 150 ); } TQT_TQWIDGET( child( "container" ) )->setShown( toggled ); } void InfoPane::setInfo( const TQString &title, const TQString &info ) { //If the info pane is not shown, we can enable or disable the button depending on //whether there is content to show. Otherwise, just remember what we wanted to do //so we can do it later, when the user does hide the pane. m_enable = !( info.isEmpty() && title.isEmpty() ); if ( !TQT_TQWIDGET(child("container"))->isShown() ) m_pushButton->setEnabled( m_enable ); if( m_pushButton->isOn() ) toggle( !(info.isEmpty() && title.isEmpty()) ); TQString info_ = info; info_.replace( "\n", "
" ); m_infoBrowser->set( m_enable ? TQString( "
" "
" "" " %1 " "" "
" "" "" "" "" "
" " %2 " "
" "
" ).arg( title, info_ ) : TQString() ); } #include "playlistbrowser.moc"