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 "streamssettings.h"
25 #include "models/streamsmodel.h"
26 #include "streamproviderlistdialog.h"
27 #include "widgets/basicitemdelegate.h"
28 #include "widgets/icons.h"
29 #include "support/icon.h"
30 #include "tar.h"
31 #include "support/messagebox.h"
32 #include "support/utils.h"
33 #include "digitallyimportedsettings.h"
34 #include <QListWidget>
35 #include <QMenu>
36 #include <QFileInfo>
37 #include <QFileDialog>
38 #include <QTimer>
39 
40 enum Roles {
41     KeyRole = Qt::UserRole,
42     BuiltInRole,
43     ConfigurableRole
44 };
45 
removeDir(const QString & d,const QStringList & types)46 static bool removeDir(const QString &d, const QStringList &types)
47 {
48     QDir dir(d);
49     if (dir.exists()) {
50         QFileInfoList files=dir.entryInfoList(types, QDir::Files|QDir::NoDotAndDotDot);
51         for (const QFileInfo &file: files) {
52             if (!QFile::remove(file.absoluteFilePath())) {
53                 return false;
54             }
55         }
56         return dir.rmdir(d);
57     }
58     return true; // Does not exist...
59 }
60 
StreamsSettings(QWidget * p)61 StreamsSettings::StreamsSettings(QWidget *p)
62     : Dialog(p, "StreamsDialog", QSize(400, 500))
63     , providerDialog(nullptr)
64 {
65     setCaption(tr("Configure Streams"));
66     QWidget *mw=new QWidget(this);
67     setupUi(mw);
68     setMainWidget(mw);
69     categories->setItemDelegate(new BasicItemDelegate(categories));
70     categories->setSortingEnabled(true);
71     int iSize=Icon::stdSize(QApplication::fontMetrics().height()*1.25);
72     QMenu *installMenu=new QMenu(this);
73     QAction *installFromFileAct=installMenu->addAction(tr("From File..."));
74     QAction *installFromWebAct=installMenu->addAction(tr("Download..."));
75     categories->setIconSize(QSize(iSize, iSize));
76     connect(categories, SIGNAL(currentRowChanged(int)), SLOT(currentCategoryChanged(int)));
77     connect(installFromFileAct, SIGNAL(triggered()), this, SLOT(installFromFile()));
78     connect(installFromWebAct, SIGNAL(triggered()), this, SLOT(installFromWeb()));
79 
80     setButtons(Close|User1|User2|User3);
81     setButtonGuiItem(User1, GuiItem(tr("Configure Provider")));
82     setButtonGuiItem(User2, GuiItem(tr("Install")));
83     setButtonGuiItem(User3, GuiItem(tr("Remove")));
84     setButtonMenu(User2, installMenu, InstantPopup);
85     enableButton(User3, false);
86     enableButton(User1, false);
87 }
88 
load()89 void StreamsSettings::load()
90 {
91     QList<StreamsModel::Category> cats=StreamsModel::self()->getCategories();
92     QFont f(font());
93     f.setItalic(true);
94     categories->clear();
95     for (const StreamsModel::Category &cat: cats) {
96         QListWidgetItem *item=new QListWidgetItem(cat.name, categories);
97         item->setCheckState(cat.hidden ? Qt::Unchecked : Qt::Checked);
98         item->setData(KeyRole, cat.key);
99         item->setData(BuiltInRole, cat.builtin);
100         item->setData(ConfigurableRole, cat.configurable);
101         item->setIcon(cat.icon);
102         if (cat.builtin) {
103             item->setFont(f);
104         }
105     }
106 }
107 
save()108 void StreamsSettings::save()
109 {
110     QSet<QString> disabled;
111     for (int i=0; i<categories->count(); ++i) {
112         QListWidgetItem *item=categories->item(i);
113         if (Qt::Unchecked==item->checkState()) {
114             disabled.insert(item->data(Qt::UserRole).toString());
115         }
116     }
117     StreamsModel::self()->setHiddenCategories(disabled);
118 }
119 
currentCategoryChanged(int row)120 void StreamsSettings::currentCategoryChanged(int row)
121 {
122     bool enableRemove=false;
123     bool enableConfigure=false;
124 
125     if (row>=0) {
126         QListWidgetItem *item=categories->item(row);
127         enableRemove=!item->data(BuiltInRole).toBool();
128         enableConfigure=item->data(ConfigurableRole).toBool();
129     }
130     enableButton(User3, enableRemove);
131     enableButton(User1, enableConfigure);
132 }
133 
installFromFile()134 void StreamsSettings::installFromFile()
135 {
136     QString fileName=QFileDialog::getOpenFileName(this, tr("Install Streams"), QDir::homePath(), tr("Cantata Streams (*.streams)"));
137     if (fileName.isEmpty()) {
138         return;
139     }
140 
141     QString name=QFileInfo(fileName).baseName();
142     if (name.isEmpty()) {
143         return;
144     }
145     name=name.replace(Utils::constDirSep, "_");
146     #ifdef Q_OS_WIN
147     name=name.replace("\\", "_");
148     #endif
149 
150     if (get(name) && MessageBox::No==MessageBox::warningYesNo(this, tr("A category named '%1' already exists!\n\nOverwrite?").arg(name))) {
151         return;
152     }
153     install(fileName, name);
154 }
155 
installFromWeb()156 void StreamsSettings::installFromWeb()
157 {
158     if (!providerDialog) {
159         providerDialog=new StreamProviderListDialog(this);
160     }
161 
162     QSet<QString> installed;
163     for (int i=0; i<categories->count(); ++i) {
164         QListWidgetItem *item=categories->item(i);
165         installed.insert(item->text());
166     }
167 
168     providerDialog->show(installed);
169     #ifdef Q_OS_MAC
170     // Under OSX when stream providers are installed/updated, and the dialog closed, it
171     // puts the pref dialog below the main window! This hack fixes this...
172     QTimer::singleShot(0, this, SLOT(raiseWindow()));
173     #endif
174 }
175 
install(const QString & fileName,const QString & name,bool showErrors)176 bool StreamsSettings::install(const QString &fileName, const QString &name, bool showErrors)
177 {
178     Tar tar(fileName);
179     if (!tar.open()) {
180         if (showErrors) {
181             MessageBox::error(this, tr("Failed to open package file."));
182         }
183         return false;
184     }
185 
186     QMap<QString, QByteArray> files=tar.extract(QStringList() << StreamsModel::constXmlFile << StreamsModel::constCompressedXmlFile
187                                                               << StreamsModel::constSettingsFile
188                                                               << StreamsModel::constPngIcon << StreamsModel::constSvgIcon
189                                                               << ".png" << ".svg");
190     QString streamsName=files.contains(StreamsModel::constCompressedXmlFile)
191                             ? StreamsModel::constCompressedXmlFile
192                             : files.contains(StreamsModel::constXmlFile)
193                                 ? StreamsModel::constXmlFile
194                                 : StreamsModel::constSettingsFile;
195     QString iconName=files.contains(StreamsModel::constSvgIcon) ? StreamsModel::constSvgIcon : StreamsModel::constPngIcon;
196     QByteArray streamFile=files[streamsName];
197     QByteArray icon=files[iconName];
198 
199     if (streamFile.isEmpty()) {
200         if (showErrors) {
201             MessageBox::error(this, tr("Invalid file format!"));
202         }
203         return false;
204     }
205 
206     QString streamsDir=Utils::dataDir(StreamsModel::constSubDir, true);
207     QString dir=streamsDir+name;
208     if (!QDir(dir).exists() && !QDir(dir).mkpath(dir)) {
209         if (showErrors) {
210             MessageBox::error(this, tr("Failed to create stream category folder!"));
211         }
212         return false;
213     }
214 
215     QFile streamsFile(dir+Utils::constDirSep+streamsName);
216     if (!streamsFile.open(QIODevice::WriteOnly)) {
217         if (showErrors) {
218             MessageBox::error(this, tr("Failed to save stream list!"));
219         }
220         return false;
221     }
222     streamsFile.write(streamFile);
223     streamsFile.close();
224 
225     QIcon icn;
226     if (!icon.isEmpty()) {
227         QFile iconFile(dir+Utils::constDirSep+iconName);
228         if (iconFile.open(QIODevice::WriteOnly)) {
229             iconFile.write(icon);
230             iconFile.close();
231             icn.addFile(dir+Utils::constDirSep+iconName);
232         }
233     }
234 
235     // Write all other png and svg files...
236     QMap<QString, QByteArray>::ConstIterator it=files.constBegin();
237     QMap<QString, QByteArray>::ConstIterator end=files.constEnd();
238     for (; it!=end; ++it) {
239         if (it.key()!=iconName && (it.key().endsWith(".png") || it.key().endsWith(".svg"))) {
240             QFile f(dir+Utils::constDirSep+it.key());
241             if (f.open(QIODevice::WriteOnly)) {
242                 f.write(it.value());
243             }
244         }
245     }
246 
247     StreamsModel::CategoryItem *cat=StreamsModel::self()->addInstalledProvider(name, icn, dir+Utils::constDirSep+streamsName, true);
248     if (!cat) {
249         if (showErrors) {
250             MessageBox::error(this, tr("Invalid file format!"));
251         }
252         return false;
253     }
254     QListWidgetItem *existing=get(name);
255     if (existing) {
256         delete existing;
257     }
258 
259     QListWidgetItem *item=new QListWidgetItem(name, categories);
260     item->setCheckState(Qt::Checked);
261     item->setData(KeyRole, cat->configName);
262     item->setData(BuiltInRole, false);
263     item->setData(ConfigurableRole, cat->isDi());
264     item->setIcon(icn);
265     return true;
266 }
267 
remove()268 void StreamsSettings::remove()
269 {
270     int row=categories->currentRow();
271     if (row<0) {
272         return;
273     }
274 
275     QListWidgetItem *item=categories->item(row);
276     if (!item->data(BuiltInRole).toBool() && MessageBox::No==MessageBox::warningYesNo(this, tr("Are you sure you wish to remove '%1'?").arg(item->text()))) {
277         return;
278     }
279 
280     QString dir=Utils::dataDir(StreamsModel::constSubDir);
281     if (!dir.isEmpty() && !removeDir(dir+item->text(), QStringList() << StreamsModel::constXmlFile << StreamsModel::constCompressedXmlFile
282                                                                      << StreamsModel::constSettingsFile << "*.png" << "*.svg")) {
283         MessageBox::error(this, tr("Failed to remove streams folder!"));
284         return;
285     }
286 
287     StreamsModel::self()->removeInstalledProvider(item->data(KeyRole).toString());
288     delete item;
289 }
290 
configure()291 void StreamsSettings::configure()
292 {
293     int row=categories->currentRow();
294     if (row<0) {
295         return;
296     }
297 
298     QListWidgetItem *item=categories->item(row);
299     if (!item->data(ConfigurableRole).toBool()) {
300         return;
301     }
302 
303     // TODO: Currently only digitally imported can be configured...
304     DigitallyImportedSettings(this).show();
305 }
306 
slotButtonClicked(int button)307 void StreamsSettings::slotButtonClicked(int button)
308 {
309     switch (button) {
310     case User1:
311         configure();
312         break;
313     case User3:
314         remove();
315         break;
316     case Close:
317         save();
318         reject();
319         // Need to call this - if not, when dialog is closed by window X control, it is not deleted!!!!
320         Dialog::slotButtonClicked(button);
321         break;
322     default:
323         break;
324     }
325 }
326 
raiseWindow()327 void StreamsSettings::raiseWindow()
328 {
329     Utils::raiseWindow(topLevelWidget());
330 }
331 
get(const QString & name)332 QListWidgetItem *  StreamsSettings::get(const QString &name)
333 {
334     for (int i=0; i<categories->count(); ++i) {
335         QListWidgetItem *item=categories->item(i);
336         if (!item->data(BuiltInRole).toBool() && item->text()==name) {
337             return item;
338         }
339     }
340     return nullptr;
341 }
342 
343 #include "moc_streamssettings.cpp"
344