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 "devicepropertieswidget.h"
25 #include "filenameschemedialog.h"
26 #include "gui/covers.h"
27 #include "widgets/icons.h"
28 #include "support/utils.h"
29 #include <QValidator>
30 #include <QTabWidget>
31 #include <QTimer>
32 
33 class CoverNameValidator : public QValidator
34 {
35     public:
36 
CoverNameValidator(QObject * parent)37     CoverNameValidator(QObject *parent) : QValidator(parent) { }
38 
validate(QString & input,int &) const39     State validate(QString &input, int &) const override
40     {
41         int dotCount(0);
42 
43         for (int i=0; i<input.length(); ++i) {
44             if (QChar('.')==input[i]) {
45                 if (++dotCount>1) {
46                     return Invalid;
47                 }
48             }
49             else if (!input[i].isLetterOrNumber() || input[i].isSpace()) {
50                 return Invalid;
51             }
52         }
53         if (input.endsWith('.')) {
54             return Intermediate;
55         }
56 
57         return Acceptable;
58     }
59 
60 //     void fixup(QString &input) const
61 //     {
62 //         QString out;
63 //         int dotCount(0);
64 //         for (int i=0; i<input.length(); ++i) {
65 //             if (input[i].isLetterOrNumber() && !input[i].isSpace()) {
66 //                 out+=input[i];
67 //             } else if (QChar('.')==input[i] && ++dotCount<1) {
68 //                 out+=input[i];
69 //             }
70 //         }
71 //
72 //         if (!out.endsWith(".jpg") && !out.endsWith(".png")) {
73 //             out.replace(".", "");
74 //             out+=".jpg";
75 //         }
76 //         input=out;
77 //     }
78 };
79 
DevicePropertiesWidget(QWidget * parent)80 DevicePropertiesWidget::DevicePropertiesWidget(QWidget *parent)
81     : QWidget(parent)
82     , schemeDlg(nullptr)
83     , noCoverText(tr("Don't copy covers"))
84     , embedCoverText(tr("Embed cover within each file"))
85     , modified(false)
86     , saveable(false)
87 {
88     setupUi(this);
89     configFilename->setIcon(Icons::self()->configureIcon);
90     coverMaxSize->insertItems(0, QStringList() << tr("No maximum size") << tr("400 pixels") << tr("300 pixels") << tr("200 pixels") << tr("100 pixels"));
91     fixVariousArtists->setToolTip(tr("<p>When copying tracks to a device, and the 'Album Artist' is set to 'Various Artists', "
92                                        "then Cantata will set the 'Artist' tag of all tracks to 'Various Artists' and the "
93                                        "track 'Title' tag to 'TrackArtist - TrackTitle'.<hr/> When copying from a device, Cantata "
94                                        "will check if 'Album Artist' and 'Artist' are both set to 'Various Artists'. If so, it "
95                                        "will attempt to extract the real artist from the 'Title' tag, and remove the artist name "
96                                        "from the 'Title' tag.</p>"));
97 
98     useCache->setToolTip(tr("<p>If you enable this, then Cantata will create a cache of the device's music library. "
99                               "This will help to speed up subsequent library scans (as the cache file will be used instead of "
100                               "having to read the tags of each file.)<hr/><b>NOTE:</b> If you use another application to update "
101                               "the device's library, then this cache will become out-of-date. To rectify this, simply "
102                               "click on the 'refresh' icon in the device list. This will cause the cache file to be removed, and "
103                               "the contents of the device re-scanned.</p>"));
104 
105     if (qobject_cast<QTabWidget *>(parent)) {
106         verticalLayout->setMargin(4);
107     }
108 }
109 
110 #define REMOVE(w) \
111     w->setVisible(false); \
112     w->deleteLater(); \
113     w=0;
114 
update(const QString & path,const DeviceOptions & opts,const QList<DeviceStorage> & storage,int props,int disabledProps)115 void DevicePropertiesWidget::update(const QString &path, const DeviceOptions &opts, const QList<DeviceStorage> &storage, int props, int disabledProps)
116 {
117     bool allowCovers=(props&Prop_CoversAll)||(props&Prop_CoversBasic);
118     albumCovers->clear();
119     if (allowCovers) {
120         if (props&Prop_CoversAll) {
121             albumCovers->insertItems(0, QStringList() << noCoverText << embedCoverText << Covers::standardNames());
122         } else {
123             albumCovers->insertItems(0, QStringList() << noCoverText << embedCoverText);
124         }
125     }
126     if (props&Prop_Name) {
127         name->setText(opts.name);
128         connect(name, SIGNAL(textChanged(const QString &)), SLOT(checkSaveable()));
129     } else {
130         REMOVE(name)
131         REMOVE(nameLabel)
132     }
133     if (props&Prop_FileName) {
134         filenameScheme->setText(opts.scheme);
135         vfatSafe->setChecked(opts.vfatSafe);
136         asciiOnly->setChecked(opts.asciiOnly);
137         ignoreThe->setChecked(opts.ignoreThe);
138         replaceSpaces->setChecked(opts.replaceSpaces);
139     } else {
140         REMOVE(filenamesGroupBox)
141         filenameScheme=nullptr;
142         vfatSafe=nullptr;
143         asciiOnly=nullptr;
144         ignoreThe=nullptr;
145         replaceSpaces=nullptr;
146         configFilename=nullptr;
147     }
148     origOpts=opts;
149 
150     if (props&Prop_Folder) {
151         musicFolder->setText(Utils::convertPathForDisplay(path));
152         connect(musicFolder, SIGNAL(textChanged(const QString &)), this, SLOT(checkSaveable()));
153         if (disabledProps&Prop_Folder) {
154             musicFolder->setDisabled(true);
155         }
156     } else {
157         REMOVE(musicFolder);
158         REMOVE(musicFolderLabel);
159     }
160     if (allowCovers) {
161         albumCovers->setEditable(props&Prop_CoversAll);
162         if (origOpts.coverName==Device::constNoCover) {
163             origOpts.coverName=noCoverText;
164             albumCovers->setCurrentIndex(0);
165         }
166         if (origOpts.coverName==Device::constEmbedCover) {
167             origOpts.coverName=embedCoverText;
168             albumCovers->setCurrentIndex(1);
169         } else {
170             albumCovers->setCurrentIndex(0);
171             for (int i=1; i<albumCovers->count(); ++i) {
172                 if (albumCovers->itemText(i)==origOpts.coverName) {
173                     albumCovers->setCurrentIndex(i);
174                     break;
175                 }
176             }
177         }
178         if (0!=origOpts.coverMaxSize) {
179             int coverMax=origOpts.coverMaxSize/100;
180             if (coverMax<0 || coverMax>=coverMaxSize->count()) {
181                 coverMax=0;
182             }
183             coverMaxSize->setCurrentIndex(0==coverMax ? 0 : (coverMaxSize->count()-coverMax));
184         } else {
185             coverMaxSize->setCurrentIndex(0);
186         }
187         albumCovers->setValidator(new CoverNameValidator(this));
188         connect(albumCovers, SIGNAL(editTextChanged(const QString &)), this, SLOT(albumCoversChanged()));
189         connect(coverMaxSize, SIGNAL(currentIndexChanged(int)), this, SLOT(checkSaveable()));
190     } else {
191         REMOVE(albumCovers);
192         REMOVE(albumCoversLabel);
193         REMOVE(coverMaxSize);
194         REMOVE(coverMaxSizeLabel);
195     }
196     if (props&Prop_Va) {
197         fixVariousArtists->setChecked(opts.fixVariousArtists);
198         connect(fixVariousArtists, SIGNAL(stateChanged(int)), this, SLOT(checkSaveable()));
199     } else {
200         REMOVE(fixVariousArtists);
201     }
202     if (props&Prop_Cache) {
203         useCache->setChecked(opts.useCache);
204         connect(useCache, SIGNAL(stateChanged(int)), this, SLOT(checkSaveable()));
205     } else {
206         REMOVE(useCache);
207     }
208     if (props&Prop_AutoScan) {
209         autoScan->setChecked(opts.autoScan);
210         connect(autoScan, SIGNAL(stateChanged(int)), this, SLOT(checkSaveable()));
211     } else {
212         REMOVE(autoScan);
213     }
214 
215     if (props&Prop_Transcoder || props&Prop_Encoder) {
216         bool transcode=props&Prop_Transcoder;
217         transcoderName->clear();
218         if (transcode) {
219             transcoderName->addItem(tr("Do not transcode"), QString());
220             transcoderName->setCurrentIndex(0);
221             transcoderValue->setVisible(false);
222             transcoderWhen->addItem(tr("Always transcode"), DeviceOptions::TW_Always);
223             transcoderWhen->addItem(tr("Only transcode if source file is of a different format"), DeviceOptions::TW_IfDifferent);
224             transcoderWhen->addItem(tr("Only transcode if source is FLAC/WAV"), DeviceOptions::TW_IfLossess);
225             transcoderWhen->setVisible(false);
226             transcoderWhen->setCurrentIndex(0);
227             for (int i=0; i<transcoderWhen->count(); ++i) {
228                 if (transcoderWhen->itemData(i).toInt()==opts.transcoderWhen) {
229                     transcoderWhen->setCurrentIndex(i);
230                     break;
231                 }
232             }
233             connect(transcoderWhen, SIGNAL(currentIndexChanged(int)), this, SLOT(checkSaveable()));
234         } else {
235             transcoderFrame->setTitle(tr("Encoder"));
236             REMOVE(transcoderWhen);
237         }
238 
239         QList<Encoders::Encoder> encs=Encoders::getAvailable();
240 
241         if (encs.isEmpty()) {
242             transcoderFrame->setVisible(false);
243         } else {
244             for (const Encoders::Encoder &e: encs) {
245                 if (!transcode || e.transcoder) {
246                     QString name=e.name;
247                     if (transcode && name.endsWith(QLatin1String(" (ffmpeg)"))) {
248                         name=name.left(name.length()-9);
249                     }
250                     transcoderName->addItem(transcode ? tr("Transcode to %1").arg(name) : name, e.codec);
251                 }
252             }
253 
254             if (opts.transcoderCodec.isEmpty()) {
255                 transcoderChanged();
256             } else {
257                 Encoders::Encoder enc=Encoders::getEncoder(opts.transcoderCodec);
258                 if (!enc.isNull()) {
259                     for (int i=1; i<transcoderName->count(); ++i) {
260                         if (transcoderName->itemData(i).toString()==opts.transcoderCodec) {
261                             transcoderName->setCurrentIndex(i);
262                             transcoderChanged();
263                             transcoderValue->setValue(opts.transcoderValue);
264                             break;
265                         }
266                     }
267                 }
268             }
269         }
270         connect(transcoderName, SIGNAL(currentIndexChanged(int)), this, SLOT(transcoderChanged()));
271         connect(transcoderValue, SIGNAL(valueChanged(int)), this, SLOT(checkSaveable()));
272     } else {
273         REMOVE(transcoderFrame);
274     }
275 
276     if (storage.count()<2) {
277         REMOVE(defaultVolume);
278         REMOVE(defaultVolumeLabel);
279     } else {
280         for (const DeviceStorage &ds: storage) {
281             defaultVolume->addItem(tr("%1 (%2 free)", "name (size free)")
282                                    .arg(ds.description).arg(Utils::formatByteSize(ds.size-ds.used)), ds.volumeIdentifier);
283         }
284 
285         for (int i=0; i<defaultVolume->count(); ++i) {
286             if (defaultVolume->itemData(i).toString()==opts.volumeId) {
287                 defaultVolume->setCurrentIndex(i);
288                 break;
289             }
290         }
291         connect(defaultVolume,SIGNAL(currentIndexChanged(int)), this, SLOT(checkSaveable()));
292     }
293 
294     origMusicFolder=Utils::fixPath(path);
295     if (props&Prop_FileName) {
296         connect(configFilename, SIGNAL(clicked()), SLOT(configureFilenameScheme()));
297         connect(filenameScheme, SIGNAL(textChanged(const QString &)), this, SLOT(checkSaveable()));
298         connect(vfatSafe, SIGNAL(stateChanged(int)), this, SLOT(checkSaveable()));
299         connect(asciiOnly, SIGNAL(stateChanged(int)), this, SLOT(checkSaveable()));
300         connect(ignoreThe, SIGNAL(stateChanged(int)), this, SLOT(checkSaveable()));
301         connect(replaceSpaces, SIGNAL(stateChanged(int)), this, SLOT(checkSaveable()));
302     }
303 
304     if (albumCovers) {
305         albumCoversChanged();
306     }
307     QTimer::singleShot(0, this, SLOT(setSize()));
308 }
309 
transcoderChanged()310 void DevicePropertiesWidget::transcoderChanged()
311 {
312     QString codec=transcoderName->itemData(transcoderName->currentIndex()).toString();
313     if (codec.isEmpty()) {
314         transcoderName->setToolTip(QString());
315         transcoderValue->setVisible(false);
316         if (transcoderWhen) {
317             transcoderWhen->setVisible(false);
318         }
319     } else {
320         Encoders::Encoder enc=Encoders::getEncoder(codec);
321         transcoderName->setToolTip(enc.description);
322         if (transcoderWhen) {
323             transcoderWhen->setVisible(true);
324         }
325         if (enc.values.count()) {
326             transcoderValue->setValues(enc);
327             transcoderValue->setVisible(true);
328         } else {
329             transcoderValue->setVisible(false);
330         }
331     }
332 
333     Utils::resizeWindow(this, true, false);
334     checkSaveable();
335 }
336 
albumCoversChanged()337 void DevicePropertiesWidget::albumCoversChanged()
338 {
339     if (coverMaxSize) {
340         bool enableSize=albumCovers->currentText()!=noCoverText;
341         coverMaxSize->setEnabled(enableSize);
342         coverMaxSizeLabel->setEnabled(enableSize);
343     }
344     checkSaveable();
345 }
346 
checkSaveable()347 void DevicePropertiesWidget::checkSaveable()
348 {
349     DeviceOptions opts=settings();
350     bool checkFolder=musicFolder ? musicFolder->isEnabled() : false;
351 
352     modified=opts!=origOpts;
353     if (!modified && checkFolder) {
354         modified=music()!=origMusicFolder;
355     }
356     saveable=!opts.scheme.isEmpty() && (!checkFolder || !music().isEmpty()) && !opts.coverName.isEmpty();
357     if (saveable &&
358         ( (-1!=opts.coverName.indexOf(noCoverText) && opts.coverName!=noCoverText) ||
359           (-1!=opts.coverName.indexOf(embedCoverText) && opts.coverName!=embedCoverText) ) ) {
360         saveable=false;
361     }
362     emit updated();
363 }
364 
configureFilenameScheme()365 void DevicePropertiesWidget::configureFilenameScheme()
366 {
367     if (!schemeDlg) {
368         schemeDlg=new FilenameSchemeDialog(this);
369         connect(schemeDlg, SIGNAL(scheme(const QString &)), filenameScheme, SLOT(setText(const QString &)));
370     }
371     schemeDlg->show(settings());
372 }
373 
settings()374 DeviceOptions DevicePropertiesWidget::settings()
375 {
376     DeviceOptions opts;
377     if (name && name->isEnabled()) {
378         opts.name=name->text().trimmed();
379     }
380     if (filenameScheme) {
381         opts.scheme=filenameScheme->text().trimmed();
382     }
383     if (vfatSafe) {
384         opts.vfatSafe=vfatSafe->isChecked();
385     }
386     if (asciiOnly) {
387         opts.asciiOnly=asciiOnly->isChecked();
388     }
389     if (ignoreThe) {
390         opts.ignoreThe=ignoreThe->isChecked();
391     }
392     if (replaceSpaces) {
393         opts.replaceSpaces=replaceSpaces->isChecked();
394     }
395     opts.fixVariousArtists=fixVariousArtists ? fixVariousArtists->isChecked() : false;
396     opts.useCache=useCache ? useCache->isChecked() : false;
397     opts.autoScan=autoScan ? autoScan->isChecked() : false;
398     opts.transcoderCodec=QString();
399     opts.transcoderValue=0;
400     opts.transcoderWhen=DeviceOptions::TW_Always;
401     opts.coverName=cover();
402     opts.coverMaxSize=(!coverMaxSize || 0==coverMaxSize->currentIndex()) ? 0 : ((coverMaxSize->count()-coverMaxSize->currentIndex())*100);
403     opts.volumeId=defaultVolume && defaultVolume->isVisible() ? defaultVolume->itemData(defaultVolume->currentIndex()).toString() : QString();
404     if (transcoderFrame && transcoderFrame->isVisible()) {
405         opts.transcoderCodec=transcoderName->itemData(transcoderName->currentIndex()).toString();
406 
407         if (!opts.transcoderCodec.isEmpty()) {
408             Encoders::Encoder enc=Encoders::getEncoder(opts.transcoderCodec);
409 
410             if (transcoderWhen) {
411                 opts.transcoderWhen=(DeviceOptions::TranscodeWhen)transcoderWhen->itemData(transcoderWhen->currentIndex()).toUInt();
412             }
413             if (!enc.isNull() && transcoderValue->value()<enc.values.count()) {
414                 opts.transcoderValue=enc.values.at(transcoderValue->value()).value;
415             }
416         }
417     }
418     return opts;
419 }
420 
cover() const421 QString DevicePropertiesWidget::cover() const
422 {
423     QString coverName=albumCovers ? albumCovers->currentText().trimmed() : noCoverText;
424     return coverName==noCoverText
425             ? Device::constNoCover
426             : coverName==embedCoverText
427                 ? Device::constEmbedCover
428                 : coverName;
429 }
430 
setSize()431 void DevicePropertiesWidget::setSize()
432 {
433     Utils::resizeWindow(this, true, false);
434 }
435 
436 #include "moc_devicepropertieswidget.cpp"
437