1 /****************************************************************************************
2  * Copyright (c) 2010 Bart Cerneels <bart.cerneels@kde.org                              *
3  *                                                                                      *
4  * This program is free software; you can redistribute it and/or modify it under        *
5  * the terms of the GNU General Public License as published by the Free Software        *
6  * Foundation; either version 2 of the License, or (at your option) any later           *
7  * version.                                                                             *
8  *                                                                                      *
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
10  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
11  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
12  *                                                                                      *
13  * You should have received a copy of the GNU General Public License along with         *
14  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
15  ****************************************************************************************/
16 
17 #include "OpmlDirectoryModel.h"
18 
19 #include "core/support/Amarok.h"
20 #include "MainWindow.h"
21 #include "OpmlParser.h"
22 #include "OpmlWriter.h"
23 #include "core/support/Debug.h"
24 //included to access defaultPodcasts()
25 #include "playlistmanager/PlaylistManager.h"
26 #include "core/podcasts/PodcastProvider.h"
27 
28 #include "ui_AddOpmlWidget.h"
29 
30 #include <QAction>
31 #include <QDialog>
32 #include <QDialogButtonBox>
33 
OpmlDirectoryModel(QUrl outlineUrl,QObject * parent)34 OpmlDirectoryModel::OpmlDirectoryModel( QUrl outlineUrl, QObject *parent )
35     : QAbstractItemModel( parent )
36     , m_rootOpmlUrl( outlineUrl )
37 {
38     //fetchMore will be called by the view
39     m_addOpmlAction = new QAction( QIcon::fromTheme( "list-add" ), i18n( "Add OPML" ), this );
40     connect( m_addOpmlAction, &QAction::triggered, this, &OpmlDirectoryModel::slotAddOpmlAction );
41 
42     m_addFolderAction = new QAction( QIcon::fromTheme( "folder-add" ), i18n( "Add Folder"), this );
43     connect( m_addFolderAction, &QAction::triggered, this, &OpmlDirectoryModel::slotAddFolderAction );
44 }
45 
~OpmlDirectoryModel()46 OpmlDirectoryModel::~OpmlDirectoryModel()
47 {
48 }
49 
50 QModelIndex
index(int row,int column,const QModelIndex & parent) const51 OpmlDirectoryModel::index( int row, int column, const QModelIndex &parent ) const
52 {
53     if( !parent.isValid() )
54     {
55         if( m_rootOutlines.isEmpty() || m_rootOutlines.count() <= row )
56             return QModelIndex();
57         else
58             return createIndex( row, column, m_rootOutlines[row] );
59     }
60 
61     OpmlOutline *parentOutline = static_cast<OpmlOutline *>( parent.internalPointer() );
62     if( !parentOutline )
63         return QModelIndex();
64 
65     if( !parentOutline->hasChildren() || parentOutline->children().count() <= row )
66         return QModelIndex();
67 
68     return createIndex( row, column, parentOutline->children().at(row) );
69 }
70 
71 Qt::ItemFlags
flags(const QModelIndex & idx) const72 OpmlDirectoryModel::flags( const QModelIndex &idx ) const
73 {
74     if( !idx.isValid() )
75         return Qt::ItemIsDropEnabled;
76 
77     OpmlOutline *outline = static_cast<OpmlOutline *>( idx.internalPointer() );
78     if( outline && !outline->attributes().contains( "type" ) ) //probably a folder
79         return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsDragEnabled
80                 | Qt::ItemIsDropEnabled;
81 
82     return QAbstractItemModel::flags( idx );
83 }
84 
85 QModelIndex
parent(const QModelIndex & idx) const86 OpmlDirectoryModel::parent( const QModelIndex &idx ) const
87 {
88     if( !idx.isValid() )
89         return QModelIndex();
90 //     debug() << idx;
91     OpmlOutline *outline = static_cast<OpmlOutline *>( idx.internalPointer() );
92     if( outline->isRootItem() )
93         return QModelIndex();
94 
95     OpmlOutline *parentOutline = outline->parent();
96     int childIndex;
97     if( parentOutline->isRootItem() )
98         childIndex = m_rootOutlines.indexOf( parentOutline );
99     else
100         childIndex = parentOutline->parent()->children().indexOf( parentOutline );
101     return createIndex( childIndex, 0, parentOutline );
102 }
103 
104 int
rowCount(const QModelIndex & parent) const105 OpmlDirectoryModel::rowCount( const QModelIndex &parent ) const
106 {
107     if( !parent.isValid() )
108         return m_rootOutlines.count();
109 
110     OpmlOutline *outline = static_cast<OpmlOutline *>( parent.internalPointer() );
111 
112     if( !outline || !outline->hasChildren() )
113         return 0;
114     else
115         return outline->children().count();
116 }
117 
118 bool
hasChildren(const QModelIndex & parent) const119 OpmlDirectoryModel::hasChildren( const QModelIndex &parent ) const
120 {
121     debug() << parent;
122     if( !parent.isValid() )
123         return !m_rootOutlines.isEmpty();
124 
125     OpmlOutline *outline = static_cast<OpmlOutline *>( parent.internalPointer() );
126 
127     if( !outline )
128         return false;
129 
130     if( outline->hasChildren() )
131         return true;
132 
133     return outline->attributes().value( "type" ) == "include";
134 }
135 
136 int
columnCount(const QModelIndex & parent) const137 OpmlDirectoryModel::columnCount( const QModelIndex &parent ) const
138 {
139     Q_UNUSED(parent)
140     return 1;
141 }
142 
143 QVariant
data(const QModelIndex & idx,int role) const144 OpmlDirectoryModel::data( const QModelIndex &idx, int role ) const
145 {
146     if( !idx.isValid() )
147     {
148         if( role == ActionRole )
149         {
150             QList<QAction *> actions;
151             actions << m_addOpmlAction << m_addFolderAction;
152             return QVariant::fromValue( actions );
153         }
154         return QVariant();
155     }
156 
157     OpmlOutline *outline = static_cast<OpmlOutline *>( idx.internalPointer() );
158     if( !outline )
159         return QVariant();
160 
161     switch( role )
162     {
163         case Qt::DisplayRole:
164             return outline->attributes().value("text");
165         case Qt::DecorationRole:
166             return m_imageMap.contains( outline ) ? m_imageMap.value( outline ) : QVariant();
167         case ActionRole:
168             if( outline->opmlNodeType() == RegularNode ) //probably a folder
169             {
170                 //store the index the new item should get added to
171                 m_addOpmlAction->setData( QVariant::fromValue( idx ) );
172                 m_addFolderAction->setData( QVariant::fromValue( idx ) );
173                 return QVariant::fromValue( QActionList() << m_addOpmlAction << m_addFolderAction );
174             }
175             debug() << outline->opmlNodeType();
176             return QVariant();
177         default:
178             return QVariant();
179     }
180 
181     return QVariant();
182 }
183 
184 bool
setData(const QModelIndex & idx,const QVariant & value,int role)185 OpmlDirectoryModel::setData( const QModelIndex &idx, const QVariant &value, int role )
186 {
187     Q_UNUSED(role);
188 
189     if( !idx.isValid() )
190         return false;
191 
192     OpmlOutline *outline = static_cast<OpmlOutline *>( idx.internalPointer() );
193     if( !outline )
194         return false;
195 
196     outline->mutableAttributes()["text"] = value.toString();
197 
198     saveOpml( m_rootOpmlUrl );
199 
200     return true;
201 }
202 
203 bool
removeRows(int row,int count,const QModelIndex & parent)204 OpmlDirectoryModel::removeRows( int row, int count, const QModelIndex &parent )
205 {
206     if( !parent.isValid() )
207     {
208         if( m_rootOutlines.count() >= ( row + count ) )
209         {
210             beginRemoveRows( parent, row, row + count - 1 );
211             for( int i = 0; i < count; i++ )
212                 m_rootOutlines.removeAt( row );
213             endRemoveRows();
214             saveOpml( m_rootOpmlUrl );
215             return true;
216         }
217 
218         return false;
219     }
220 
221     OpmlOutline *outline = static_cast<OpmlOutline *>( parent.internalPointer() );
222     if( !outline )
223         return false;
224 
225     if( !outline->hasChildren() || outline->children().count() < ( row + count ) )
226         return false;
227 
228     beginRemoveRows( parent, row, row + count -1 );
229     for( int i = 0; i < count - 1; i++ )
230             outline->mutableChildren().removeAt( row );
231     endRemoveRows();
232 
233     saveOpml( m_rootOpmlUrl );
234 
235     return true;
236 }
237 
238 void
saveOpml(const QUrl & saveLocation)239 OpmlDirectoryModel::saveOpml( const QUrl &saveLocation )
240 {
241     if( !saveLocation.isLocalFile() )
242     {
243         //TODO:implement
244         error() << "can not save OPML to remote location";
245         return;
246     }
247 
248     QFile *opmlFile = new QFile( saveLocation.toLocalFile(), this );
249     if( !opmlFile->open( QIODevice::WriteOnly | QIODevice::Truncate ) )
250     {
251         error() << "could not open OPML file for writing " << saveLocation.url();
252         return;
253     }
254 
255     QMap<QString,QString> headerData;
256     //TODO: set header data such as date
257 
258     OpmlWriter *opmlWriter = new OpmlWriter( m_rootOutlines, headerData, opmlFile );
259     connect( opmlWriter, &OpmlWriter::result, this, &OpmlDirectoryModel::slotOpmlWriterDone );
260     opmlWriter->run();
261 }
262 
263 void
slotOpmlWriterDone(int result)264 OpmlDirectoryModel::slotOpmlWriterDone( int result )
265 {
266     Q_UNUSED( result )
267 
268     OpmlWriter *writer = qobject_cast<OpmlWriter *>( QObject::sender() );
269     Q_ASSERT( writer );
270     writer->device()->close();
271     delete writer;
272 }
273 
274 OpmlNodeType
opmlNodeType(const QModelIndex & idx) const275 OpmlDirectoryModel::opmlNodeType( const QModelIndex &idx ) const
276 {
277     OpmlOutline *outline = static_cast<OpmlOutline *>( idx.internalPointer() );
278     return outline->opmlNodeType();
279 }
280 
281 void
slotAddOpmlAction()282 OpmlDirectoryModel::slotAddOpmlAction()
283 {
284     QModelIndex parentIdx = QModelIndex();
285     QAction *action = qobject_cast<QAction *>( sender() );
286     if( action )
287     {
288         parentIdx = action->data().value<QModelIndex>();
289     }
290 
291     QDialog *dialog = new QDialog( The::mainWindow() );
292     dialog->setLayout( new QVBoxLayout );
293     dialog->setWindowTitle( i18nc( "Heading of Add OPML dialog", "Add OPML" ) );
294     QWidget *opmlAddWidget = new QWidget( dialog );
295     dialog->layout()->addWidget( opmlAddWidget );
296     Ui::AddOpmlWidget widget;
297     widget.setupUi( opmlAddWidget );
298     widget.urlEdit->setMode( KFile::File );
299     auto buttonBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel, dialog );
300     dialog->layout()->addWidget( buttonBox );
301     connect( buttonBox, &QDialogButtonBox::accepted, dialog, &QDialog::accept );
302     connect( buttonBox, &QDialogButtonBox::rejected, dialog, &QDialog::reject );
303 
304     if( dialog->exec() != QDialog::Accepted ) {
305         delete dialog;
306         return;
307     }
308 
309     QString url = widget.urlEdit->url().url();
310     QString title = widget.titleEdit->text();
311     debug() << QString( "creating a new OPML outline with url = %1 and title \"%2\"." ).arg( url, title );
312     OpmlOutline *outline = new OpmlOutline();
313     outline->addAttribute( "type", "include" );
314     outline->addAttribute( "url", url );
315     if( !title.isEmpty() )
316         outline->addAttribute( "text", title );
317 
318     //Folder icon with down-arrow emblem
319     m_imageMap.insert( outline, QIcon::fromTheme( "folder-download", QIcon::fromTheme( "go-down" ) ).pixmap( 24, 24 ) );
320 
321     QModelIndex newIdx = addOutlineToModel( parentIdx, outline );
322     //TODO: force the view to expand the folder (parentIdx) so the new node is shown
323 
324     //if the title is missing, start parsing the OPML so we can get it from the feed
325     if( outline->attributes().contains( "text" ) )
326         saveOpml( m_rootOpmlUrl );
327     else
328         fetchMore( newIdx ); //saves OPML after receiving the title.
329 
330     delete dialog;
331 }
332 
333 void
slotAddFolderAction()334 OpmlDirectoryModel::slotAddFolderAction()
335 {
336     QModelIndex parentIdx = QModelIndex();
337     QAction *action = qobject_cast<QAction *>( sender() );
338     if( action )
339     {
340         parentIdx = action->data().value<QModelIndex>();
341     }
342 
343     OpmlOutline *outline = new OpmlOutline();
344     outline->addAttribute( "text", i18n( "New Folder" ) );
345     m_imageMap.insert( outline, QIcon::fromTheme( "folder" ).pixmap( 24, 24 ) );
346 
347     addOutlineToModel( parentIdx, outline );
348     //TODO: trigger edit of the new folder
349 
350     saveOpml( m_rootOpmlUrl );
351 }
352 
353 bool
canFetchMore(const QModelIndex & parent) const354 OpmlDirectoryModel::canFetchMore( const QModelIndex &parent ) const
355 {
356     debug() << parent;
357     //already fetched or just started?
358     if( rowCount( parent ) || m_currentFetchingMap.values().contains( parent ) )
359         return false;
360     if( !parent.isValid() )
361         return m_rootOutlines.isEmpty();
362 
363     OpmlOutline *outline = static_cast<OpmlOutline *>( parent.internalPointer() );
364 
365     return outline && ( outline->attributes().value( "type" ) == "include" );
366 }
367 
368 void
fetchMore(const QModelIndex & parent)369 OpmlDirectoryModel::fetchMore( const QModelIndex &parent )
370 {
371     debug() << parent;
372     if( m_currentFetchingMap.values().contains( parent ) )
373     {
374         error() << "trying to start second fetch job for same item";
375         return;
376     }
377     QUrl urlToFetch;
378     if( !parent.isValid() )
379     {
380         urlToFetch = m_rootOpmlUrl;
381     }
382     else
383     {
384         OpmlOutline *outline = static_cast<OpmlOutline *>( parent.internalPointer() );
385         if( !outline )
386             return;
387         if( outline->attributes().value( "type" ) != "include" )
388             return;
389         urlToFetch = QUrl( outline->attributes().value("url") );
390     }
391 
392     if( !urlToFetch.isValid() )
393         return;
394 
395     OpmlParser *parser = new OpmlParser( urlToFetch );
396     connect( parser, &OpmlParser::headerDone, this, &OpmlDirectoryModel::slotOpmlHeaderDone );
397     connect( parser, &OpmlParser::outlineParsed, this, &OpmlDirectoryModel::slotOpmlOutlineParsed );
398     connect( parser, &OpmlParser::doneParsing, this, &OpmlDirectoryModel::slotOpmlParsingDone );
399 
400     m_currentFetchingMap.insert( parser, parent );
401 
402 //    ThreadWeaver::Weaver::instance()->enqueue( parser );
403     parser->run();
404 }
405 
406 void
slotOpmlHeaderDone()407 OpmlDirectoryModel::slotOpmlHeaderDone()
408 {
409     OpmlParser *parser = qobject_cast<OpmlParser *>( QObject::sender() );
410     QModelIndex idx = m_currentFetchingMap.value( parser );
411 
412     if( !idx.isValid() ) //header data of the root not required.
413         return;
414 
415     OpmlOutline *outline = static_cast<OpmlOutline *>( idx.internalPointer() );
416 
417     if( !outline->attributes().contains("text") )
418     {
419         if( parser->headerData().contains( "title" ) )
420             outline->addAttribute( "text", parser->headerData().value("title") );
421         else
422             outline->addAttribute( "text", parser->url().fileName() );
423 
424         //force a view update
425         Q_EMIT dataChanged( idx, idx );
426 
427         saveOpml( m_rootOpmlUrl );
428     }
429 
430 }
431 
432 void
slotOpmlOutlineParsed(OpmlOutline * outline)433 OpmlDirectoryModel::slotOpmlOutlineParsed( OpmlOutline *outline )
434 {
435     OpmlParser *parser = qobject_cast<OpmlParser *>( QObject::sender() );
436     QModelIndex idx = m_currentFetchingMap.value( parser );
437 
438     addOutlineToModel( idx, outline );
439 
440     //TODO: begin image fetch
441     switch( outline->opmlNodeType() )
442     {
443         case RegularNode:
444             m_imageMap.insert( outline, QIcon::fromTheme( "folder" ).pixmap( 24, 24 ) ); break;
445         case IncludeNode:
446         {
447             m_imageMap.insert( outline,
448                                QIcon::fromTheme( "folder-download", QIcon::fromTheme( "go-down" ) ).pixmap( 24, 24 )
449                              );
450             break;
451         }
452         case RssUrlNode:
453         default: break;
454     }
455 }
456 
457 void
slotOpmlParsingDone()458 OpmlDirectoryModel::slotOpmlParsingDone()
459 {
460     OpmlParser *parser = qobject_cast<OpmlParser *>( QObject::sender() );
461     m_currentFetchingMap.remove( parser );
462     parser->deleteLater();
463 }
464 
465 void
subscribe(const QModelIndexList & indexes) const466 OpmlDirectoryModel::subscribe( const QModelIndexList &indexes ) const
467 {
468     QList<OpmlOutline *> outlines;
469 
470     foreach( const QModelIndex &idx, indexes )
471         outlines << static_cast<OpmlOutline *>( idx.internalPointer() );
472 
473     foreach( const OpmlOutline *outline, outlines )
474     {
475         if( !outline )
476             continue;
477 
478         QUrl url;
479         if( outline->attributes().contains( "xmlUrl" ) )
480             url = QUrl( outline->attributes().value("xmlUrl") );
481         else if( outline->attributes().contains( "url" ) )
482             url = QUrl( outline->attributes().value("url") );
483 
484         if( url.isEmpty() )
485             continue;
486 
487         The::playlistManager()->defaultPodcasts()->addPodcast( url );
488     }
489 }
490 
491 QModelIndex
addOutlineToModel(const QModelIndex & parentIdx,OpmlOutline * outline)492 OpmlDirectoryModel::addOutlineToModel(const QModelIndex &parentIdx, OpmlOutline *outline )
493 {
494     int newRow = rowCount( parentIdx );
495     beginInsertRows( parentIdx, newRow, newRow );
496 
497     //no reparenting required when the item is already parented.
498     if( outline->isRootItem() )
499     {
500         if( parentIdx.isValid() )
501         {
502             OpmlOutline * parentOutline = static_cast<OpmlOutline *>( parentIdx.internalPointer() );
503             Q_ASSERT(parentOutline);
504 
505             outline->setParent( parentOutline );
506             parentOutline->addChild( outline );
507             parentOutline->setHasChildren( true );
508         }
509         else
510         {
511             m_rootOutlines << outline;
512         }
513     }
514     endInsertRows();
515 
516     return index( newRow, 0, parentIdx );
517 }
518