]> git.sur5r.net Git - minitube/blob - src/searchview.cpp
New upstream version 3.9.1
[minitube] / src / searchview.cpp
1 /* $BEGIN_LICENSE
2
3 This file is part of Minitube.
4 Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
5
6 Minitube is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10
11 Minitube is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with Minitube.  If not, see <http://www.gnu.org/licenses/>.
18
19 $END_LICENSE */
20
21 #include "searchview.h"
22 #include "channelsuggest.h"
23 #include "constants.h"
24 #include "fontutils.h"
25 #include "searchparams.h"
26 #include "ytsuggester.h"
27 #ifdef APP_MAC_SEARCHFIELD
28 #include "searchlineedit_mac.h"
29 #else
30 #include "searchlineedit.h"
31 #endif
32 #ifdef APP_MAC
33 #include "macutils.h"
34 #endif
35 #ifdef APP_EXTRA
36 #include "extra.h"
37 #endif
38 #ifdef APP_ACTIVATION
39 #include "activation.h"
40 #include "activationview.h"
41 #endif
42 #include "clickablelabel.h"
43 #include "iconutils.h"
44 #include "mainwindow.h"
45 #include "messagebar.h"
46 #include "painterutils.h"
47
48 #ifdef UPDATER
49 #include "updater.h"
50 #endif
51
52 namespace {
53 const QString recentKeywordsKey = "recentKeywords";
54 const QString recentChannelsKey = "recentChannels";
55 } // namespace
56
57 SearchView::SearchView(QWidget *parent) : View(parent) {
58     setBackgroundRole(QPalette::Base);
59     setForegroundRole(QPalette::Text);
60     setAutoFillBackground(true);
61
62     const int padding = 30;
63
64     QBoxLayout *vLayout = new QVBoxLayout(this);
65     vLayout->setMargin(padding);
66     vLayout->setSpacing(0);
67
68     messageBar = new MessageBar();
69     messageBar->hide();
70     vLayout->addWidget(messageBar, 0, Qt::AlignCenter);
71     vLayout->addSpacing(padding);
72     maybeShowMessage();
73
74     vLayout->addStretch();
75
76     QBoxLayout *hLayout = new QHBoxLayout();
77     hLayout->setAlignment(Qt::AlignCenter);
78
79     vLayout->addLayout(hLayout);
80
81     hLayout->addStretch();
82
83     logo = new ClickableLabel();
84     auto setLogoPixmap = [this] {
85         logo->setPixmap(IconUtils::pixmap(":/images/app.png", logo->devicePixelRatioF()));
86     };
87     setLogoPixmap();
88     connect(window()->windowHandle(), &QWindow::screenChanged, this, setLogoPixmap);
89     connect(logo, &ClickableLabel::clicked, MainWindow::instance(), &MainWindow::visitSite);
90     hLayout->addWidget(logo, 0, Qt::AlignTop);
91     hLayout->addSpacing(padding);
92
93     QVBoxLayout *layout = new QVBoxLayout();
94     layout->setAlignment(Qt::AlignCenter);
95     hLayout->addLayout(layout);
96
97     QLabel *welcomeLabel = new QLabel();
98     auto setupWelcomeLabel = [this, welcomeLabel] {
99         QColor titleColor = palette().color(QPalette::WindowText);
100         titleColor.setAlphaF(.75);
101         int r, g, b, a;
102         titleColor.getRgb(&r, &g, &b, &a);
103         QString cssColor = QString::asprintf("rgba(%d,%d,%d,%d)", r, g, b, a);
104         QString text =
105                 QString("<h1 style='font-weight:300;color:%1'>").arg(cssColor) +
106                 tr("Welcome to <a href='%1'>%2</a>,")
107                         .replace("<a ", "<a style='text-decoration:none; color:palette(text)' ")
108                         .arg(Constants::WEBSITE, Constants::NAME) +
109                 "</h1>";
110         welcomeLabel->setText(text);
111     };
112     setupWelcomeLabel();
113     connect(qApp, &QGuiApplication::paletteChanged, this, setupWelcomeLabel);
114     welcomeLabel->setOpenExternalLinks(true);
115     welcomeLabel->setFont(FontUtils::light(welcomeLabel->font().pointSize()));
116     layout->addWidget(welcomeLabel);
117
118     layout->addSpacing(padding / 2);
119
120     //: "Enter", as in "type". The whole phrase says: "Enter a keyword to start watching videos"
121     // QLabel *tipLabel = new QLabel(tr("Enter"), this);
122     QString tip;
123     if (qApp->layoutDirection() == Qt::RightToLeft) {
124         tip = tr("to start watching videos.") + " " + tr("a keyword") + " " + tr("Enter");
125     } else {
126         tip = tr("Enter") + " " + tr("a keyword") + " " + tr("to start watching videos.");
127     }
128
129     layout->addSpacing(padding / 2);
130
131     QBoxLayout *searchLayout = new QHBoxLayout();
132     searchLayout->setAlignment(Qt::AlignVCenter);
133
134 #ifdef APP_MAC_SEARCHFIELD
135     SearchLineEditMac *slem = new SearchLineEditMac(this);
136     queryEdit = slem;
137     setFocusProxy(slem);
138 #else
139     SearchLineEdit *sle = new SearchLineEdit(this);
140     sle->setFont(FontUtils::medium());
141     int tipWidth = sle->fontMetrics().size(Qt::TextSingleLine, tip).width();
142     sle->setMinimumWidth(tipWidth + sle->fontMetrics().width('m') * 6);
143     sle->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
144     queryEdit = sle;
145 #endif
146
147     connect(queryEdit->toWidget(), SIGNAL(search(const QString &)), SLOT(watch(const QString &)));
148     connect(queryEdit->toWidget(), SIGNAL(textChanged(const QString &)),
149             SLOT(textChanged(const QString &)));
150     connect(queryEdit->toWidget(), SIGNAL(textEdited(const QString &)),
151             SLOT(textChanged(const QString &)));
152     connect(queryEdit->toWidget(), SIGNAL(suggestionAccepted(Suggestion *)),
153             SLOT(suggestionAccepted(Suggestion *)));
154     queryEdit->setPlaceholderText(tip);
155
156     youtubeSuggest = new YTSuggester(this);
157     channelSuggest = new ChannelSuggest(this);
158     connect(channelSuggest, SIGNAL(ready(QVector<Suggestion *>)),
159             SLOT(onChannelSuggestions(QVector<Suggestion *>)));
160     searchTypeChanged(0);
161
162     searchLayout->addWidget(queryEdit->toWidget(), 0, Qt::AlignBaseline);
163
164     layout->addLayout(searchLayout);
165
166     layout->addSpacing(padding);
167
168     QHBoxLayout *recentLayout = new QHBoxLayout();
169     recentLayout->setMargin(0);
170     recentLayout->setSpacing(10);
171
172     recentKeywordsLayout = new QVBoxLayout();
173     recentKeywordsLayout->setMargin(0);
174     recentKeywordsLayout->setSpacing(0);
175     recentKeywordsLayout->setAlignment(Qt::AlignTop | Qt::AlignLeft);
176     recentKeywordsLabel = new QLabel(tr("Recent keywords"));
177     recentKeywordsLabel->setProperty("recentHeader", true);
178     recentKeywordsLabel->hide();
179     recentKeywordsLabel->setEnabled(false);
180     recentKeywordsLayout->addWidget(recentKeywordsLabel);
181     recentLayout->addLayout(recentKeywordsLayout);
182
183     recentChannelsLayout = new QVBoxLayout();
184     recentChannelsLayout->setMargin(0);
185     recentChannelsLayout->setSpacing(0);
186     recentChannelsLayout->setAlignment(Qt::AlignTop | Qt::AlignLeft);
187     recentChannelsLabel = new QLabel(tr("Recent channels"));
188     recentChannelsLabel->setProperty("recentHeader", true);
189     recentChannelsLabel->hide();
190     recentChannelsLabel->setEnabled(false);
191     recentChannelsLayout->addWidget(recentChannelsLabel);
192     recentLayout->addLayout(recentChannelsLayout);
193
194     layout->addLayout(recentLayout);
195
196     hLayout->addStretch();
197
198     vLayout->addStretch();
199
200 #ifdef APP_ACTIVATION
201     if (!Activation::instance().isActivated())
202         vLayout->addWidget(ActivationView::buyButton(tr("Get the full version")), 0,
203                            Qt::AlignRight);
204 #endif
205 }
206
207 void SearchView::appear() {
208     MainWindow *w = MainWindow::instance();
209     w->showActionsInStatusBar(
210             {w->getAction("manualplay"), w->getAction("safeSearch"), w->getAction("definition")},
211             true);
212
213     updateRecentKeywords();
214     updateRecentChannels();
215
216     queryEdit->selectAll();
217     queryEdit->enableSuggest();
218     QTimer::singleShot(0, queryEdit->toWidget(), SLOT(setFocus()));
219 }
220
221 void SearchView::disappear() {
222     MainWindow *w = MainWindow::instance();
223     w->showActionsInStatusBar(
224             {w->getAction("manualplay"), w->getAction("safeSearch"), w->getAction("definition")},
225             false);
226 }
227
228 void SearchView::updateRecentKeywords() {
229     // load
230     QSettings settings;
231     const QStringList keywords = settings.value(recentKeywordsKey).toStringList();
232     if (keywords == recentKeywords) return;
233     recentKeywords = keywords;
234
235     // cleanup
236     QLayoutItem *item;
237     while (recentKeywordsLayout->count() - 1 > recentKeywords.size() &&
238            (item = recentKeywordsLayout->takeAt(1)) != nullptr) {
239         item->widget()->close();
240         delete item;
241     }
242
243     recentKeywordsLabel->setVisible(!keywords.isEmpty());
244     MainWindow::instance()->getAction("clearRecentKeywords")->setEnabled(!keywords.isEmpty());
245
246     const int maxDisplayLength = 25;
247
248 #ifdef APP_MAC
249     QPalette p = palette();
250     p.setColor(QPalette::Highlight, mac::accentColor());
251 #endif
252
253     int counter = 1;
254     for (const QString &keyword : keywords) {
255         QString link = keyword;
256         QString display = keyword;
257         if (keyword.startsWith(QLatin1String("http://")) ||
258             keyword.startsWith(QLatin1String("https://"))) {
259             int separator = keyword.indexOf('|');
260             if (separator > 0 && separator + 1 < keyword.length()) {
261                 link = keyword.left(separator);
262                 display = keyword.mid(separator + 1);
263             }
264         }
265         bool needStatusTip = display.length() > maxDisplayLength;
266         if (needStatusTip) {
267             display.truncate(maxDisplayLength);
268             display.append(QStringLiteral("\u2026"));
269         }
270
271         ClickableLabel *item;
272         if (recentKeywordsLayout->count() - 1 >= counter) {
273             item = qobject_cast<ClickableLabel *>(recentKeywordsLayout->itemAt(counter)->widget());
274         } else {
275             item = new ClickableLabel();
276 #ifdef APP_MAC
277             item->setPalette(p);
278 #endif
279             item->setAttribute(Qt::WA_DeleteOnClose);
280             item->setProperty("recentItem", true);
281             item->setFocusPolicy(Qt::TabFocus);
282             connect(item, &ClickableLabel::hovered, this, [item, this](bool value) {
283                 item->setForegroundRole(value ? QPalette::Highlight : QPalette::WindowText);
284                 if (value) {
285                     for (int i = 1; i < recentKeywordsLayout->count(); ++i) {
286                         QWidget *w = recentKeywordsLayout->itemAt(i)->widget();
287                         if (w != item) {
288                             w->setForegroundRole(QPalette::WindowText);
289                         }
290                     }
291                 }
292             });
293             item->setContextMenuPolicy(Qt::ActionsContextMenu);
294             auto removeAction = new QAction(tr("Remove"));
295             item->addAction(removeAction);
296             connect(removeAction, &QAction::triggered, item, [item] {
297                 QSettings settings;
298                 QStringList keywords = settings.value(recentKeywordsKey).toStringList();
299                 QString keyword = item->property("keyword").toString();
300                 keywords.removeOne(keyword);
301                 settings.setValue(recentKeywordsKey, keywords);
302                 item->deleteLater();
303             });
304             recentKeywordsLayout->addWidget(item);
305         }
306
307         item->setText(display);
308         if (needStatusTip)
309             item->setStatusTip(link);
310         else
311             item->setStatusTip(QString());
312         item->setProperty("keyword", keyword);
313
314         disconnect(item, &ClickableLabel::clicked, nullptr, nullptr);
315         connect(item, &ClickableLabel::clicked, this, [this, link]() { watchKeywords(link); });
316
317         counter++;
318     }
319 }
320
321 void SearchView::updateRecentChannels() {
322     // load
323     QSettings settings;
324     const QStringList keywords = settings.value(recentChannelsKey).toStringList();
325     if (keywords == recentChannels) return;
326     recentChannels = keywords;
327
328     // cleanup
329     QLayoutItem *item;
330     while ((item = recentChannelsLayout->takeAt(1)) != nullptr) {
331         item->widget()->close();
332         delete item;
333     }
334
335     recentChannelsLabel->setVisible(!keywords.isEmpty());
336
337 #ifdef APP_MAC
338     QPalette p = palette();
339     p.setColor(QPalette::Highlight, mac::accentColor());
340 #endif
341
342     for (const QString &keyword : keywords) {
343         QString link = keyword;
344         QString display = keyword;
345         int separator = keyword.indexOf('|');
346         if (separator > 0 && separator + 1 < keyword.length()) {
347             link = keyword.left(separator);
348             display = keyword.mid(separator + 1);
349         }
350
351         ClickableLabel *item = new ClickableLabel(display);
352         item->setProperty("keyword", keyword);
353 #ifdef APP_MAC
354         item->setPalette(p);
355 #endif
356         item->setAttribute(Qt::WA_DeleteOnClose);
357         item->setProperty("recentItem", true);
358         item->setFocusPolicy(Qt::TabFocus);
359         connect(item, &ClickableLabel::clicked, [this, link]() { watchChannel(link); });
360         connect(item, &ClickableLabel::hovered, item, [item](bool value) {
361             item->setForegroundRole(value ? QPalette::Highlight : QPalette::WindowText);
362         });
363         item->setContextMenuPolicy(Qt::ActionsContextMenu);
364         auto removeAction = new QAction(tr("Remove"));
365         item->addAction(removeAction);
366         connect(removeAction, &QAction::triggered, item, [item] {
367             QSettings settings;
368             QStringList keywords = settings.value(recentChannelsKey).toStringList();
369             QString keyword = item->property("keyword").toString();
370             keywords.removeOne(keyword);
371             settings.setValue(recentChannelsKey, keywords);
372             item->deleteLater();
373         });
374         recentChannelsLayout->addWidget(item);
375     }
376 }
377
378 void SearchView::watch() {
379     QString query = queryEdit->text();
380     watch(query);
381 }
382
383 void SearchView::textChanged(const QString &text) {
384     lastChannelSuggestions.clear();
385 }
386
387 void SearchView::watch(const QString &query) {
388     QString q = query.simplified();
389
390     // check for empty query
391     if (q.isEmpty()) {
392         queryEdit->toWidget()->setFocus(Qt::OtherFocusReason);
393         return;
394     }
395
396     SearchParams *searchParams = new SearchParams();
397     searchParams->setKeywords(q);
398
399     // go!
400     emit search(searchParams);
401 }
402
403 void SearchView::watchChannel(const QString &channelId) {
404     if (channelId.isEmpty()) {
405         queryEdit->toWidget()->setFocus(Qt::OtherFocusReason);
406         return;
407     }
408
409     QString id = channelId;
410     qDebug() << "Channel" << id;
411     SearchParams *searchParams = new SearchParams();
412     searchParams->setChannelId(id);
413     searchParams->setSortBy(SearchParams::SortByNewest);
414
415     // go!
416     emit search(searchParams);
417 }
418
419 void SearchView::watchKeywords(const QString &query) {
420     QString q = query.simplified();
421
422     // check for empty query
423     if (q.isEmpty()) {
424         queryEdit->toWidget()->setFocus(Qt::OtherFocusReason);
425         return;
426     }
427
428     queryEdit->setText(q);
429
430     SearchParams *searchParams = new SearchParams();
431     searchParams->setKeywords(q);
432
433     // go!
434     emit search(searchParams);
435 }
436
437 void SearchView::searchTypeChanged(int index) {
438     if (index == 0) {
439         queryEdit->setSuggester(youtubeSuggest);
440     } else {
441         queryEdit->setSuggester(channelSuggest);
442     }
443     queryEdit->selectAll();
444     queryEdit->toWidget()->setFocus();
445 }
446
447 void SearchView::suggestionAccepted(Suggestion *suggestion) {
448     if (suggestion->type == QLatin1String("channel")) {
449         watchChannel(suggestion->userData);
450     } else
451         watch(suggestion->value);
452 }
453
454 void SearchView::onChannelSuggestions(const QVector<Suggestion *> &suggestions) {
455     lastChannelSuggestions = suggestions;
456 }
457
458 void SearchView::maybeShowMessage() {
459     QSettings settings;
460     QString key;
461
462     bool showMessages = true;
463 #ifdef APP_ACTIVATION
464     showMessages = Activation::instance().isActivated();
465 #endif
466
467 #if defined APP_MAC && !defined APP_MAC_STORE
468     if (showMessages && !settings.contains(key = "sofa")) {
469         QString msg = tr("Need a remote control for %1? Try %2!").arg(Constants::NAME).arg("Sofa");
470         msg = "<a href='https://" + QLatin1String(Constants::ORG_DOMAIN) + '/' + key +
471               "' style='text-decoration:none;color:palette(windowText)'>" + msg + "</a>";
472         messageBar->setMessage(msg);
473         messageBar->setOpenExternalLinks(true);
474         disconnect(messageBar);
475         connect(messageBar, &MessageBar::closed, this, [key] {
476             QSettings settings;
477             settings.setValue(key, true);
478         });
479         messageBar->show();
480         showMessages = false;
481     }
482 #endif
483
484     if (showMessages) {
485         key = "donate" + QLatin1String(Constants::VERSION);
486         if (!settings.contains(key)) {
487             bool oneYearUsage = true;
488 #ifdef APP_ACTIVATION
489             oneYearUsage = (QDateTime::currentSecsSinceEpoch() -
490                             Activation::instance().getLicenseTimestamp()) > 86400 * 365;
491 #elif defined APP_MAC_STORE
492             oneYearUsage = false;
493 #endif
494             if (oneYearUsage) {
495                 QString msg =
496                         tr("I keep improving %1 to make it the best I can. Support this work!")
497                                 .arg(Constants::NAME);
498                 msg = "<a href='https://" + QLatin1String(Constants::ORG_DOMAIN) + "/donate" +
499                       "' style='text-decoration:none;color:palette(windowText)'>" + msg + "</a>";
500                 messageBar->setMessage(msg);
501                 messageBar->setOpenExternalLinks(true);
502                 disconnect(messageBar);
503                 connect(messageBar, &MessageBar::closed, this, [key] {
504                     QSettings settings;
505                     settings.setValue(key, true);
506                 });
507                 messageBar->show();
508             }
509         }
510     }
511
512 #ifdef UPDATER
513     connect(&Updater::instance(), &Updater::statusChanged, this, [this](auto status) {
514         if (status == Updater::Status::UpdateDownloaded) {
515             QString msg = tr("An update is ready to be installed. Quit and install update.");
516             msg = "<a href='http://quit' style='text-decoration:none;color:palette(windowText)'>" +
517                   msg + "</a>";
518             messageBar->setMessage(msg);
519             messageBar->setOpenExternalLinks(false);
520             disconnect(messageBar);
521             connect(messageBar, &MessageBar::linkActivated, this, [] {
522                 Updater::instance().setRelaunchAfterInstall(true);
523                 qApp->quit();
524             });
525             messageBar->show();
526         }
527     });
528 #endif
529 }