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