1 /*
2  * Cantata
3  *
4  * Copyright (c) 2011-2020 Craig Drummond <craig.p.drummond@gmail.com>
5  *
6  * ----
7  *
8  * This program is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation; either version 2 of the License, or
11  * (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16  * General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program; see the file COPYING.  If not, write to
20  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21  * Boston, MA 02110-1301, USA.
22  */
23 
24 #include "podcastwidget.h"
25 #include "podcastsearchdialog.h"
26 #include "podcastsettingsdialog.h"
27 #include "widgets/itemview.h"
28 #include "widgets/toolbutton.h"
29 #include "widgets/icons.h"
30 #include "widgets/menubutton.h"
31 #include "support/action.h"
32 #include "support/messagebox.h"
33 #include "support/configuration.h"
34 #include "support/monoicon.h"
35 #include "support/utils.h"
36 #include <QTimer>
37 #include <QFileDialog>
38 
PodcastWidget(PodcastService * s,QWidget * p)39 PodcastWidget::PodcastWidget(PodcastService *s, QWidget *p)
40     : SinglePageWidget(p)
41     , srv(s)
42     , proxy(this)
43 {
44     QIcon newIcon = MonoIcon::icon(FontAwesome::asterisk, Utils::monoIconColor());
45     subscribeAction = new Action(Icons::self()->addNewItemIcon, tr("Add Subscription"), this);
46     unSubscribeAction = new Action(Icons::self()->removeIcon, tr("Remove Subscription"), this);
47     downloadAction = new Action(Icons::self()->downloadIcon, tr("Download Episodes"), this);
48     deleteAction = new Action(MonoIcon::icon(FontAwesome::trash, MonoIcon::constRed, MonoIcon::constRed), tr("Delete Downloaded Episodes"), this);
49     cancelDownloadAction = new Action(Icons::self()->cancelIcon, tr("Cancel Download"), this);
50     markAsNewAction = new Action(newIcon, tr("Mark Episodes As New"), this);
51     markAsListenedAction = new Action(tr("Mark Episodes As Listened"), this);
52     //unplayedOnlyAction = new Action(newIcon, tr("Show Unplayed Only"), this);
53     //unplayedOnlyAction->setCheckable(true);
54     exportAction = new Action(tr("Export Current Subscriptions"), this);
55 
56     proxy.setSourceModel(srv);
57     view->setModel(&proxy);
58 
59     view->alwaysShowHeader();
60     connect(view, SIGNAL(headerClicked(int)), SLOT(headerClicked(int)));
61 
62     connect(subscribeAction, SIGNAL(triggered()), this, SLOT(subscribe()));
63     connect(unSubscribeAction, SIGNAL(triggered()), this, SLOT(unSubscribe()));
64     connect(downloadAction, SIGNAL(triggered()), this, SLOT(download()));
65     connect(deleteAction, SIGNAL(triggered()), this, SLOT(deleteDownload()));
66     connect(cancelDownloadAction, SIGNAL(triggered()), this, SLOT(cancelDownload()));
67     connect(markAsNewAction, SIGNAL(triggered()), this, SLOT(markAsNew()));
68     connect(markAsListenedAction, SIGNAL(triggered()), this, SLOT(markAsListened()));
69     //connect(unplayedOnlyAction, SIGNAL(toggled(bool)), this, SLOT(showUnplayedOnly(bool)));
70     connect(exportAction, SIGNAL(triggered()), SLOT(exportSubscriptions()));
71 
72     view->setMode(ItemView::Mode_DetailedTree);
73     Configuration config(metaObject()->className());
74     view->load(config);
75     MenuButton *menu=new MenuButton(this);
76     ToolButton *addSub=new ToolButton(this);
77     //ToolButton *unplayedOnlyBtn=new ToolButton(this);
78     addSub->setDefaultAction(subscribeAction);
79     //unplayedOnlyBtn->setDefaultAction(unplayedOnlyAction);
80     menu->addAction(createViewMenu(QList<ItemView::Mode>() << ItemView::Mode_BasicTree << ItemView::Mode_SimpleTree
81                                                            << ItemView::Mode_DetailedTree << ItemView::Mode_List));
82 
83     Action *configureAction=new Action(Icons::self()->configureIcon, tr("Configure"), this);
84     connect(configureAction, SIGNAL(triggered()), SLOT(configure()));
85     menu->addAction(configureAction);
86     menu->addAction(exportAction);
87     init(ReplacePlayQueue|AppendToPlayQueue|Refresh, QList<QWidget *>() << menu/* << unplayedOnlyBtn*/, QList<QWidget *>() << addSub);
88 
89     view->addAction(subscribeAction);
90     view->addAction(unSubscribeAction);
91     view->addSeparator();
92     view->addAction(downloadAction);
93     view->addAction(deleteAction);
94     view->addAction(cancelDownloadAction);
95     view->addSeparator();
96     view->addAction(markAsNewAction);
97     view->addAction(markAsListenedAction);
98     view->setInfoText(tr("Use the + icon (below) to add podcast subscriptions."));
99 }
100 
~PodcastWidget()101 PodcastWidget::~PodcastWidget()
102 {
103     Configuration config(metaObject()->className());
104     view->save(config);
105 }
106 
selectedFiles(bool allowPlaylists) const107 QStringList PodcastWidget::selectedFiles(bool allowPlaylists) const
108 {
109     QModelIndexList selected = view->selectedIndexes();
110     if (selected.isEmpty()) {
111         return QStringList();
112     }
113     return srv->filenames(proxy.mapToSource(selected), allowPlaylists);
114 }
115 
selectedSongs(bool allowPlaylists) const116 QList<Song> PodcastWidget::selectedSongs(bool allowPlaylists) const
117 {
118     QModelIndexList selected = view->selectedIndexes();
119     if (selected.isEmpty()) {
120         return QList<Song>();
121     }
122     return srv->songs(proxy.mapToSource(selected), allowPlaylists);
123 }
124 
headerClicked(int level)125 void PodcastWidget::headerClicked(int level)
126 {
127     if (0==level) {
128         emit close();
129     }
130 }
131 
subscribe()132 void PodcastWidget::subscribe()
133 {
134     if (0==PodcastSearchDialog::instanceCount()) {
135         PodcastSearchDialog *dlg=new PodcastSearchDialog(srv, this);
136         dlg->show();
137     }
138 }
139 
unSubscribe()140 void PodcastWidget::unSubscribe()
141 {
142     const QModelIndexList selected = view->selectedIndexes(false); // Dont need sorted selection here...
143     if (1!=selected.size()) {
144         return;
145     }
146 
147     PodcastService::Item *item=static_cast<PodcastService::Item *>(proxy.mapToSource(selected.first()).internalPointer());
148     if (!item->isPodcast()) {
149         return;
150     }
151 
152     if (MessageBox::No==MessageBox::warningYesNo(this, tr("Unsubscribe from '%1'?").arg(item->name))) {
153         return;
154     }
155 
156     srv->unSubscribe(static_cast<PodcastService::Podcast *>(item));
157 }
158 
159 enum GetEp {
160     GetEp_Downloaded     = 0x01,
161     GetEp_NotDownloaded  = 0x02,
162     GetEp_Listened       = 0x04,
163     GetEp_NotListened    = 0x08
164 };
165 
useEpisode(const PodcastService::Episode * episode,int get)166 static bool useEpisode(const PodcastService::Episode *episode, int get)
167 {
168     return ! ((get&GetEp_Downloaded && episode->localFile.isEmpty()) ||
169               (get&GetEp_NotDownloaded && !episode->localFile.isEmpty()) ||
170               (get&GetEp_Listened && !episode->played) ||
171               (get&GetEp_NotListened && episode->played) );
172 }
173 
getEpisodes(PodcastService::Proxy & proxy,const QModelIndexList & selected,int get)174 static QMap<PodcastService::Podcast *, QSet<PodcastService::Episode *> > getEpisodes(PodcastService::Proxy &proxy, const QModelIndexList &selected, int get)
175 {
176     QMap<PodcastService::Podcast *, QSet<PodcastService::Episode *> > urls;
177     for (const QModelIndex &idx: selected) {
178         PodcastService::Item *item=static_cast<PodcastService::Item *>(proxy.mapToSource(idx).internalPointer());
179         if (item->isPodcast()) {
180             for (PodcastService::Episode *episode: static_cast<PodcastService::Podcast *>(item)->episodes) {
181                 if (useEpisode(episode, get)) {
182                     urls[static_cast<PodcastService::Podcast *>(item)].insert(episode);
183                 }
184             }
185         } else {
186             PodcastService::Episode *episode=static_cast<PodcastService::Episode *>(item);
187             if (useEpisode(episode, get)) {
188                 urls[episode->parent].insert(episode);
189             }
190         }
191     }
192     return urls;
193 }
194 
download()195 void PodcastWidget::download()
196 {
197     QMap<PodcastService::Podcast *, QSet<PodcastService::Episode *> > urls=getEpisodes(proxy, view->selectedIndexes(true), GetEp_NotDownloaded/*|(unplayedOnlyAction->isChecked() ? GetEp_NotListened : 0)*/);
198 
199     if (!urls.isEmpty()) {
200         QMap<PodcastService::Podcast *, QSet<PodcastService::Episode *> >::ConstIterator it(urls.constBegin());
201         QMap<PodcastService::Podcast *, QSet<PodcastService::Episode *> >::ConstIterator end(urls.constEnd());
202         for (; it!=end; ++it) {
203             srv->downloadPodcasts(it.key(), it.value().toList());
204         }
205     }
206 }
207 
cancelDownload()208 void PodcastWidget::cancelDownload()
209 {
210     if (srv->isDownloading()) {
211         srv->cancelAll();
212     }
213 }
214 
deleteDownload()215 void PodcastWidget::deleteDownload()
216 {
217     QMap<PodcastService::Podcast *, QSet<PodcastService::Episode *> > urls=getEpisodes(proxy, view->selectedIndexes(false), GetEp_Downloaded);
218 
219     if (!urls.isEmpty()) {
220         if (MessageBox::No==MessageBox::questionYesNo(this, tr("Do you wish to the delete downloaded files of the selected podcast episodes?"))) {
221             return;
222         }
223         QMap<PodcastService::Podcast *, QSet<PodcastService::Episode *> >::ConstIterator it(urls.constBegin());
224         QMap<PodcastService::Podcast *, QSet<PodcastService::Episode *> >::ConstIterator end(urls.constEnd());
225         for (; it!=end; ++it) {
226             srv->deleteDownloadedPodcasts(it.key(), it.value().toList());
227         }
228     }
229 }
230 
markAsNew()231 void PodcastWidget::markAsNew()
232 {
233     QMap<PodcastService::Podcast *, QSet<PodcastService::Episode *> > urls=getEpisodes(proxy, view->selectedIndexes(false), GetEp_Listened);
234 
235     if (!urls.isEmpty()) {
236         QMap<PodcastService::Podcast *, QSet<PodcastService::Episode *> >::ConstIterator it(urls.constBegin());
237         QMap<PodcastService::Podcast *, QSet<PodcastService::Episode *> >::ConstIterator end(urls.constEnd());
238         for (; it!=end; ++it) {
239             srv->setPodcastsAsListened(it.key(), it.value().toList(), false);
240         }
241     }
242 }
243 
markAsListened()244 void PodcastWidget::markAsListened()
245 {
246     QMap<PodcastService::Podcast *, QSet<PodcastService::Episode *> > urls=getEpisodes(proxy, view->selectedIndexes(false), GetEp_NotListened);
247 
248     if (!urls.isEmpty()) {
249         QMap<PodcastService::Podcast *, QSet<PodcastService::Episode *> >::ConstIterator it(urls.constBegin());
250         QMap<PodcastService::Podcast *, QSet<PodcastService::Episode *> >::ConstIterator end(urls.constEnd());
251         for (; it!=end; ++it) {
252             srv->setPodcastsAsListened(it.key(), it.value().toList(), true);
253         }
254     }
255 }
256 
257 /*
258 void PodcastWidget::showUnplayedOnly(bool on)
259 {
260     view->goToTop();
261     proxy.showUnplayedOnly(on);
262 }
263 */
264 
doSearch()265 void PodcastWidget::doSearch()
266 {
267     QString text=view->searchText().trimmed();
268     proxy.update(text);
269     if (proxy.enabled() && !text.isEmpty()) {
270         view->expandAll();
271     }
272 }
273 
refresh()274 void PodcastWidget::refresh()
275 {
276     QModelIndexList sel=view->selectedIndexes(false);
277     QModelIndexList selected;
278     for (const QModelIndex &i: sel) {
279         if (!i.parent().isValid()) {
280             selected+=proxy.mapToSource(i);
281         }
282     }
283 
284     if (selected.isEmpty() || selected.count()==srv->podcastCount()) {
285         srv->refreshAll();
286         return;
287     }
288     switch (MessageBox::questionYesNoCancel(this, tr("Refresh all subscriptions, or only those selected?"), tr("Refresh"), GuiItem(tr("Refresh All")), GuiItem(tr("Refresh Selected")))) {
289     case MessageBox::Yes:
290         srv->refreshAll();
291         break;
292     case MessageBox::No:
293         srv->refresh(selected);
294         break;
295     default:
296         break;
297     }
298 }
299 
configure()300 void PodcastWidget::configure()
301 {
302     srv->configure(this);
303 }
304 
exportSubscriptions()305 void PodcastWidget::exportSubscriptions()
306 {
307     if (0==srv->podcastCount()) {
308         return;
309     }
310 
311     QString filename = QFileDialog::getSaveFileName(this, tr("Export Podcast Subscriptions"), QDir::homePath(), QString(".opml"));
312     if (filename.isEmpty()) {
313         return;
314     }
315 
316     if (!srv->exportSubscriptions(filename)) {
317         MessageBox::error(this, tr("Export failed!"));
318     }
319 }
320 
controlActions()321 void PodcastWidget::controlActions()
322 {
323     SinglePageWidget::controlActions();
324 
325     QModelIndexList selected=view->selectedIndexes(false); // Dont need sorted selection here...
326     downloadAction->setEnabled(!selected.isEmpty());
327     unSubscribeAction->setEnabled(1==selected.count() && static_cast<PodcastService::Item *>(proxy.mapToSource(selected.first()).internalPointer())->isPodcast());
328     downloadAction->setEnabled(!selected.isEmpty());
329     deleteAction->setEnabled(!selected.isEmpty());
330     cancelDownloadAction->setEnabled(!selected.isEmpty());
331     markAsNewAction->setEnabled(!selected.isEmpty());
332     markAsListenedAction->setEnabled(!selected.isEmpty());
333 }
334 
335 #include "moc_podcastwidget.cpp"
336