File indexing completed on 2024-05-19 04:50:24

0001 /****************************************************************************************
0002  * Copyright (c) 2010 Bart Cerneels <bart.cerneels@kde.org                              *
0003  *                                                                                      *
0004  * This program is free software; you can redistribute it and/or modify it under        *
0005  * the terms of the GNU General Public License as published by the Free Software        *
0006  * Foundation; either version 2 of the License, or (at your option) any later           *
0007  * version.                                                                             *
0008  *                                                                                      *
0009  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
0010  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
0011  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
0012  *                                                                                      *
0013  * You should have received a copy of the GNU General Public License along with         *
0014  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
0015  ****************************************************************************************/
0016 
0017 #include "OpmlDirectoryModel.h"
0018 
0019 #include "core/support/Amarok.h"
0020 #include "MainWindow.h"
0021 #include "OpmlParser.h"
0022 #include "OpmlWriter.h"
0023 #include "core/support/Debug.h"
0024 //included to access defaultPodcasts()
0025 #include "playlistmanager/PlaylistManager.h"
0026 #include "core/podcasts/PodcastProvider.h"
0027 
0028 #include "ui_AddOpmlWidget.h"
0029 
0030 #include <QAction>
0031 #include <QDialog>
0032 #include <QDialogButtonBox>
0033 
0034 OpmlDirectoryModel::OpmlDirectoryModel( QUrl outlineUrl, QObject *parent )
0035     : QAbstractItemModel( parent )
0036     , m_rootOpmlUrl( outlineUrl )
0037 {
0038     //fetchMore will be called by the view
0039     m_addOpmlAction = new QAction( QIcon::fromTheme( "list-add" ), i18n( "Add OPML" ), this );
0040     connect( m_addOpmlAction, &QAction::triggered, this, &OpmlDirectoryModel::slotAddOpmlAction );
0041 
0042     m_addFolderAction = new QAction( QIcon::fromTheme( "folder-add" ), i18n( "Add Folder"), this );
0043     connect( m_addFolderAction, &QAction::triggered, this, &OpmlDirectoryModel::slotAddFolderAction );
0044 }
0045 
0046 OpmlDirectoryModel::~OpmlDirectoryModel()
0047 {
0048 }
0049 
0050 QModelIndex
0051 OpmlDirectoryModel::index( int row, int column, const QModelIndex &parent ) const
0052 {
0053     if( !parent.isValid() )
0054     {
0055         if( m_rootOutlines.isEmpty() || m_rootOutlines.count() <= row )
0056             return QModelIndex();
0057         else
0058             return createIndex( row, column, m_rootOutlines[row] );
0059     }
0060 
0061     OpmlOutline *parentOutline = static_cast<OpmlOutline *>( parent.internalPointer() );
0062     if( !parentOutline )
0063         return QModelIndex();
0064 
0065     if( !parentOutline->hasChildren() || parentOutline->children().count() <= row )
0066         return QModelIndex();
0067 
0068     return createIndex( row, column, parentOutline->children().at(row) );
0069 }
0070 
0071 Qt::ItemFlags
0072 OpmlDirectoryModel::flags( const QModelIndex &idx ) const
0073 {
0074     if( !idx.isValid() )
0075         return Qt::ItemIsDropEnabled;
0076 
0077     OpmlOutline *outline = static_cast<OpmlOutline *>( idx.internalPointer() );
0078     if( outline && !outline->attributes().contains( "type" ) ) //probably a folder
0079         return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsDragEnabled
0080                 | Qt::ItemIsDropEnabled;
0081 
0082     return QAbstractItemModel::flags( idx );
0083 }
0084 
0085 QModelIndex
0086 OpmlDirectoryModel::parent( const QModelIndex &idx ) const
0087 {
0088     if( !idx.isValid() )
0089         return QModelIndex();
0090 //     debug() << idx;
0091     OpmlOutline *outline = static_cast<OpmlOutline *>( idx.internalPointer() );
0092     if( outline->isRootItem() )
0093         return QModelIndex();
0094 
0095     OpmlOutline *parentOutline = outline->parent();
0096     int childIndex;
0097     if( parentOutline->isRootItem() )
0098         childIndex = m_rootOutlines.indexOf( parentOutline );
0099     else
0100         childIndex = parentOutline->parent()->children().indexOf( parentOutline );
0101     return createIndex( childIndex, 0, parentOutline );
0102 }
0103 
0104 int
0105 OpmlDirectoryModel::rowCount( const QModelIndex &parent ) const
0106 {
0107     if( !parent.isValid() )
0108         return m_rootOutlines.count();
0109 
0110     OpmlOutline *outline = static_cast<OpmlOutline *>( parent.internalPointer() );
0111 
0112     if( !outline || !outline->hasChildren() )
0113         return 0;
0114     else
0115         return outline->children().count();
0116 }
0117 
0118 bool
0119 OpmlDirectoryModel::hasChildren( const QModelIndex &parent ) const
0120 {
0121     debug() << parent;
0122     if( !parent.isValid() )
0123         return !m_rootOutlines.isEmpty();
0124 
0125     OpmlOutline *outline = static_cast<OpmlOutline *>( parent.internalPointer() );
0126 
0127     if( !outline )
0128         return false;
0129 
0130     if( outline->hasChildren() )
0131         return true;
0132 
0133     return outline->attributes().value( "type" ) == "include";
0134 }
0135 
0136 int
0137 OpmlDirectoryModel::columnCount( const QModelIndex &parent ) const
0138 {
0139     Q_UNUSED(parent)
0140     return 1;
0141 }
0142 
0143 QVariant
0144 OpmlDirectoryModel::data( const QModelIndex &idx, int role ) const
0145 {
0146     if( !idx.isValid() )
0147     {
0148         if( role == ActionRole )
0149         {
0150             QList<QAction *> actions;
0151             actions << m_addOpmlAction << m_addFolderAction;
0152             return QVariant::fromValue( actions );
0153         }
0154         return QVariant();
0155     }
0156 
0157     OpmlOutline *outline = static_cast<OpmlOutline *>( idx.internalPointer() );
0158     if( !outline )
0159         return QVariant();
0160 
0161     switch( role )
0162     {
0163         case Qt::DisplayRole:
0164             return outline->attributes().value("text");
0165         case Qt::DecorationRole:
0166             return m_imageMap.contains( outline ) ? m_imageMap.value( outline ) : QVariant();
0167         case ActionRole:
0168             if( outline->opmlNodeType() == RegularNode ) //probably a folder
0169             {
0170                 //store the index the new item should get added to
0171                 m_addOpmlAction->setData( QVariant::fromValue( idx ) );
0172                 m_addFolderAction->setData( QVariant::fromValue( idx ) );
0173                 return QVariant::fromValue( QActionList() << m_addOpmlAction << m_addFolderAction );
0174             }
0175             debug() << outline->opmlNodeType();
0176             return QVariant();
0177         default:
0178             return QVariant();
0179     }
0180 
0181     return QVariant();
0182 }
0183 
0184 bool
0185 OpmlDirectoryModel::setData( const QModelIndex &idx, const QVariant &value, int role )
0186 {
0187     Q_UNUSED(role);
0188 
0189     if( !idx.isValid() )
0190         return false;
0191 
0192     OpmlOutline *outline = static_cast<OpmlOutline *>( idx.internalPointer() );
0193     if( !outline )
0194         return false;
0195 
0196     outline->mutableAttributes()["text"] = value.toString();
0197 
0198     saveOpml( m_rootOpmlUrl );
0199 
0200     return true;
0201 }
0202 
0203 bool
0204 OpmlDirectoryModel::removeRows( int row, int count, const QModelIndex &parent )
0205 {
0206     if( !parent.isValid() )
0207     {
0208         if( m_rootOutlines.count() >= ( row + count ) )
0209         {
0210             beginRemoveRows( parent, row, row + count - 1 );
0211             for( int i = 0; i < count; i++ )
0212                 m_rootOutlines.removeAt( row );
0213             endRemoveRows();
0214             saveOpml( m_rootOpmlUrl );
0215             return true;
0216         }
0217 
0218         return false;
0219     }
0220 
0221     OpmlOutline *outline = static_cast<OpmlOutline *>( parent.internalPointer() );
0222     if( !outline )
0223         return false;
0224 
0225     if( !outline->hasChildren() || outline->children().count() < ( row + count ) )
0226         return false;
0227 
0228     beginRemoveRows( parent, row, row + count -1 );
0229     for( int i = 0; i < count - 1; i++ )
0230             outline->mutableChildren().removeAt( row );
0231     endRemoveRows();
0232 
0233     saveOpml( m_rootOpmlUrl );
0234 
0235     return true;
0236 }
0237 
0238 void
0239 OpmlDirectoryModel::saveOpml( const QUrl &saveLocation )
0240 {
0241     if( !saveLocation.isLocalFile() )
0242     {
0243         //TODO:implement
0244         error() << "can not save OPML to remote location";
0245         return;
0246     }
0247 
0248     QFile *opmlFile = new QFile( saveLocation.toLocalFile(), this );
0249     if( !opmlFile->open( QIODevice::WriteOnly | QIODevice::Truncate ) )
0250     {
0251         error() << "could not open OPML file for writing " << saveLocation.url();
0252         return;
0253     }
0254 
0255     QMap<QString,QString> headerData;
0256     //TODO: set header data such as date
0257 
0258     OpmlWriter *opmlWriter = new OpmlWriter( m_rootOutlines, headerData, opmlFile );
0259     connect( opmlWriter, &OpmlWriter::result, this, &OpmlDirectoryModel::slotOpmlWriterDone );
0260     opmlWriter->run();
0261 }
0262 
0263 void
0264 OpmlDirectoryModel::slotOpmlWriterDone( int result )
0265 {
0266     Q_UNUSED( result )
0267 
0268     OpmlWriter *writer = qobject_cast<OpmlWriter *>( QObject::sender() );
0269     Q_ASSERT( writer );
0270     writer->device()->close();
0271     delete writer;
0272 }
0273 
0274 OpmlNodeType
0275 OpmlDirectoryModel::opmlNodeType( const QModelIndex &idx ) const
0276 {
0277     OpmlOutline *outline = static_cast<OpmlOutline *>( idx.internalPointer() );
0278     return outline->opmlNodeType();
0279 }
0280 
0281 void
0282 OpmlDirectoryModel::slotAddOpmlAction()
0283 {
0284     QModelIndex parentIdx = QModelIndex();
0285     QAction *action = qobject_cast<QAction *>( sender() );
0286     if( action )
0287     {
0288         parentIdx = action->data().value<QModelIndex>();
0289     }
0290 
0291     QDialog *dialog = new QDialog( The::mainWindow() );
0292     dialog->setLayout( new QVBoxLayout );
0293     dialog->setWindowTitle( i18nc( "Heading of Add OPML dialog", "Add OPML" ) );
0294     QWidget *opmlAddWidget = new QWidget( dialog );
0295     dialog->layout()->addWidget( opmlAddWidget );
0296     Ui::AddOpmlWidget widget;
0297     widget.setupUi( opmlAddWidget );
0298     widget.urlEdit->setMode( KFile::File );
0299     auto buttonBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel, dialog );
0300     dialog->layout()->addWidget( buttonBox );
0301     connect( buttonBox, &QDialogButtonBox::accepted, dialog, &QDialog::accept );
0302     connect( buttonBox, &QDialogButtonBox::rejected, dialog, &QDialog::reject );
0303 
0304     if( dialog->exec() != QDialog::Accepted ) {
0305         delete dialog;
0306         return;
0307     }
0308 
0309     QString url = widget.urlEdit->url().url();
0310     QString title = widget.titleEdit->text();
0311     debug() << QString( "creating a new OPML outline with url = %1 and title \"%2\"." ).arg( url, title );
0312     OpmlOutline *outline = new OpmlOutline();
0313     outline->addAttribute( "type", "include" );
0314     outline->addAttribute( "url", url );
0315     if( !title.isEmpty() )
0316         outline->addAttribute( "text", title );
0317 
0318     //Folder icon with down-arrow emblem
0319     m_imageMap.insert( outline, QIcon::fromTheme( "folder-download", QIcon::fromTheme( "go-down" ) ).pixmap( 24, 24 ) );
0320 
0321     QModelIndex newIdx = addOutlineToModel( parentIdx, outline );
0322     //TODO: force the view to expand the folder (parentIdx) so the new node is shown
0323 
0324     //if the title is missing, start parsing the OPML so we can get it from the feed
0325     if( outline->attributes().contains( "text" ) )
0326         saveOpml( m_rootOpmlUrl );
0327     else
0328         fetchMore( newIdx ); //saves OPML after receiving the title.
0329 
0330     delete dialog;
0331 }
0332 
0333 void
0334 OpmlDirectoryModel::slotAddFolderAction()
0335 {
0336     QModelIndex parentIdx = QModelIndex();
0337     QAction *action = qobject_cast<QAction *>( sender() );
0338     if( action )
0339     {
0340         parentIdx = action->data().value<QModelIndex>();
0341     }
0342 
0343     OpmlOutline *outline = new OpmlOutline();
0344     outline->addAttribute( "text", i18n( "New Folder" ) );
0345     m_imageMap.insert( outline, QIcon::fromTheme( "folder" ).pixmap( 24, 24 ) );
0346 
0347     addOutlineToModel( parentIdx, outline );
0348     //TODO: trigger edit of the new folder
0349 
0350     saveOpml( m_rootOpmlUrl );
0351 }
0352 
0353 bool
0354 OpmlDirectoryModel::canFetchMore( const QModelIndex &parent ) const
0355 {
0356     debug() << parent;
0357     //already fetched or just started?
0358     if( rowCount( parent ) || m_currentFetchingMap.values().contains( parent ) )
0359         return false;
0360     if( !parent.isValid() )
0361         return m_rootOutlines.isEmpty();
0362 
0363     OpmlOutline *outline = static_cast<OpmlOutline *>( parent.internalPointer() );
0364 
0365     return outline && ( outline->attributes().value( "type" ) == "include" );
0366 }
0367 
0368 void
0369 OpmlDirectoryModel::fetchMore( const QModelIndex &parent )
0370 {
0371     debug() << parent;
0372     if( m_currentFetchingMap.values().contains( parent ) )
0373     {
0374         error() << "trying to start second fetch job for same item";
0375         return;
0376     }
0377     QUrl urlToFetch;
0378     if( !parent.isValid() )
0379     {
0380         urlToFetch = m_rootOpmlUrl;
0381     }
0382     else
0383     {
0384         OpmlOutline *outline = static_cast<OpmlOutline *>( parent.internalPointer() );
0385         if( !outline )
0386             return;
0387         if( outline->attributes().value( "type" ) != "include" )
0388             return;
0389         urlToFetch = QUrl( outline->attributes().value("url") );
0390     }
0391 
0392     if( !urlToFetch.isValid() )
0393         return;
0394 
0395     OpmlParser *parser = new OpmlParser( urlToFetch );
0396     connect( parser, &OpmlParser::headerDone, this, &OpmlDirectoryModel::slotOpmlHeaderDone );
0397     connect( parser, &OpmlParser::outlineParsed, this, &OpmlDirectoryModel::slotOpmlOutlineParsed );
0398     connect( parser, &OpmlParser::doneParsing, this, &OpmlDirectoryModel::slotOpmlParsingDone );
0399 
0400     m_currentFetchingMap.insert( parser, parent );
0401 
0402 //    ThreadWeaver::Weaver::instance()->enqueue( parser );
0403     parser->run();
0404 }
0405 
0406 void
0407 OpmlDirectoryModel::slotOpmlHeaderDone()
0408 {
0409     OpmlParser *parser = qobject_cast<OpmlParser *>( QObject::sender() );
0410     QModelIndex idx = m_currentFetchingMap.value( parser );
0411 
0412     if( !idx.isValid() ) //header data of the root not required.
0413         return;
0414 
0415     OpmlOutline *outline = static_cast<OpmlOutline *>( idx.internalPointer() );
0416 
0417     if( !outline->attributes().contains("text") )
0418     {
0419         if( parser->headerData().contains( "title" ) )
0420             outline->addAttribute( "text", parser->headerData().value("title") );
0421         else
0422             outline->addAttribute( "text", parser->url().fileName() );
0423 
0424         //force a view update
0425         Q_EMIT dataChanged( idx, idx );
0426 
0427         saveOpml( m_rootOpmlUrl );
0428     }
0429 
0430 }
0431 
0432 void
0433 OpmlDirectoryModel::slotOpmlOutlineParsed( OpmlOutline *outline )
0434 {
0435     OpmlParser *parser = qobject_cast<OpmlParser *>( QObject::sender() );
0436     QModelIndex idx = m_currentFetchingMap.value( parser );
0437 
0438     addOutlineToModel( idx, outline );
0439 
0440     //TODO: begin image fetch
0441     switch( outline->opmlNodeType() )
0442     {
0443         case RegularNode:
0444             m_imageMap.insert( outline, QIcon::fromTheme( "folder" ).pixmap( 24, 24 ) ); break;
0445         case IncludeNode:
0446         {
0447             m_imageMap.insert( outline,
0448                                QIcon::fromTheme( "folder-download", QIcon::fromTheme( "go-down" ) ).pixmap( 24, 24 )
0449                              );
0450             break;
0451         }
0452         case RssUrlNode:
0453         default: break;
0454     }
0455 }
0456 
0457 void
0458 OpmlDirectoryModel::slotOpmlParsingDone()
0459 {
0460     OpmlParser *parser = qobject_cast<OpmlParser *>( QObject::sender() );
0461     m_currentFetchingMap.remove( parser );
0462     parser->deleteLater();
0463 }
0464 
0465 void
0466 OpmlDirectoryModel::subscribe( const QModelIndexList &indexes ) const
0467 {
0468     QList<OpmlOutline *> outlines;
0469 
0470     foreach( const QModelIndex &idx, indexes )
0471         outlines << static_cast<OpmlOutline *>( idx.internalPointer() );
0472 
0473     foreach( const OpmlOutline *outline, outlines )
0474     {
0475         if( !outline )
0476             continue;
0477 
0478         QUrl url;
0479         if( outline->attributes().contains( "xmlUrl" ) )
0480             url = QUrl( outline->attributes().value("xmlUrl") );
0481         else if( outline->attributes().contains( "url" ) )
0482             url = QUrl( outline->attributes().value("url") );
0483 
0484         if( url.isEmpty() )
0485             continue;
0486 
0487         The::playlistManager()->defaultPodcasts()->addPodcast( url );
0488     }
0489 }
0490 
0491 QModelIndex
0492 OpmlDirectoryModel::addOutlineToModel(const QModelIndex &parentIdx, OpmlOutline *outline )
0493 {
0494     int newRow = rowCount( parentIdx );
0495     beginInsertRows( parentIdx, newRow, newRow );
0496 
0497     //no reparenting required when the item is already parented.
0498     if( outline->isRootItem() )
0499     {
0500         if( parentIdx.isValid() )
0501         {
0502             OpmlOutline * parentOutline = static_cast<OpmlOutline *>( parentIdx.internalPointer() );
0503             Q_ASSERT(parentOutline);
0504 
0505             outline->setParent( parentOutline );
0506             parentOutline->addChild( outline );
0507             parentOutline->setHasChildren( true );
0508         }
0509         else
0510         {
0511             m_rootOutlines << outline;
0512         }
0513     }
0514     endInsertRows();
0515 
0516     return index( newRow, 0, parentIdx );
0517 }