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 }