]> git.sur5r.net Git - minitube/blob - src/searchview.cpp
Upload 3.9.3-2 to unstable
[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 namespace {
49 const QString recentKeywordsKey = "recentKeywords";
50 const QString recentChannelsKey = "recentChannels";
51 } // namespace
52
53 SearchView::SearchView(QWidget *parent) : View(parent) {
54     setBackgroundRole(QPalette::Base);
55     setForegroundRole(QPalette::Text);
56     setAutoFillBackground(true);
57
58     const int padding = 30;
59
60     QBoxLayout *vLayout = new QVBoxLayout(this);
61     vLayout->setMargin(padding);
62     vLayout->setSpacing(0);
63
64     messageBar = new MessageBar();
65     messageBar->hide();
66     vLayout->addWidget(messageBar, 0, Qt::AlignCenter);
67     vLayout->addSpacing(padding);
68     maybeShowMessage();
69
70     vLayout->addStretch();
71
72     QBoxLayout *hLayout = new QHBoxLayout();
73     hLayout->setAlignment(Qt::AlignCenter);
74
75     vLayout->addLayout(hLayout);
76
77     hLayout->addStretch();
78
79     logo = new ClickableLabel();
80     auto setLogoPixmap = [this] {
81         logo->setPixmap(IconUtils::pixmap(":/images/app.png", logo->devicePixelRatioF()));
82     };
83     setLogoPixmap();
84     connect(window()->windowHandle(), &QWindow::screenChanged, this, setLogoPixmap);
85     connect(logo, &ClickableLabel::clicked, MainWindow::instance(), &MainWindow::visitSite);
86     hLayout->addWidget(logo, 0, Qt::AlignTop);
87     hLayout->addSpacing(padding);
88
89     QVBoxLayout *layout = new QVBoxLayout();
90     layout->setAlignment(Qt::AlignCenter);
91     hLayout->addLayout(layout);
92
93     QLabel *welcomeLabel = new QLabel();
94     auto setupWelcomeLabel = [this, welcomeLabel] {
95         QColor titleColor = palette().color(QPalette::WindowText);
96         titleColor.setAlphaF(.75);
97         int r, g, b, a;
98         titleColor.getRgb(&r, &g, &b, &a);
99         QString cssColor = QString::asprintf("rgba(%d,%d,%d,%d)", r, g, b, a);
100         QString text =
101                 QString("<h1 style='font-weight:300;color:%1'>").arg(cssColor) +
102                 tr("Welcome to <a href='%1'>%2</a>,")
103                         .replace("<a ", "<a style='text-decoration:none; color:palette(text)' ")
104                         .arg(Constants::WEBSITE, Constants::NAME) +
105                 "</h1>";
106         welcomeLabel->setText(text);
107     };
108     setupWelcomeLabel();
109     connect(qApp, &QGuiApplication::paletteChanged, this, setupWelcomeLabel);
110     welcomeLabel->setOpenExternalLinks(true);
111     welcomeLabel->setFont(FontUtils::light(welcomeLabel->font().pointSize()));
112     layout->addWidget(welcomeLabel);
113
114     layout->addSpacing(padding / 2);
115
116     //: "Enter", as in "type". The whole phrase says: "Enter a keyword to start watching videos"
117     // QLabel *tipLabel = new QLabel(tr("Enter"), this);
118     QString tip;
119     if (qApp->layoutDirection() == Qt::RightToLeft) {
120         tip = tr("to start watching videos.") + " " + tr("a keyword") + " " + tr("Enter");
121     } else {
122         tip = tr("Enter") + " " + tr("a keyword") + " " + tr("to start watching videos.");
123     }
124
125     layout->addSpacing(padding / 2);
126
127     QBoxLayout *searchLayout = new QHBoxLayout();
128     searchLayout->setAlignment(Qt::AlignVCenter);
129
130 #ifdef APP_MAC_SEARCHFIELD
131     SearchLineEditMac *slem = new SearchLineEditMac(this);
132     queryEdit = slem;
133     setFocusProxy(slem);
134 #else
135     SearchLineEdit *sle = new SearchLineEdit(this);
136     sle->setFont(FontUtils::medium());
137     int tipWidth = sle->fontMetrics().size(Qt::TextSingleLine, tip).width();
138     sle->setMinimumWidth(tipWidth + sle->fontMetrics().width('m') * 6);
139     sle->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
140     queryEdit = sle;
141 #endif
142
143     connect(queryEdit->toWidget(), SIGNAL(search(const QString &)), SLOT(watch(const QString &)));
144     connect(queryEdit->toWidget(), SIGNAL(textChanged(const QString &)),
145             SLOT(textChanged(const QString &)));
146     connect(queryEdit->toWidget(), SIGNAL(textEdited(const QString &)),
147             SLOT(textChanged(const QString &)));
148     connect(queryEdit->toWidget(), SIGNAL(suggestionAccepted(Suggestion *)),
149             SLOT(suggestionAccepted(Suggestion *)));
150     queryEdit->setPlaceholderText(tip);
151
152     youtubeSuggest = new YTSuggester(this);
153     channelSuggest = new ChannelSuggest(this);
154     connect(channelSuggest, SIGNAL(ready(QVector<Suggestion *>)),
155             SLOT(onChannelSuggestions(QVector<Suggestion *>)));
156     searchTypeChanged(0);
157
158     searchLayout->addWidget(queryEdit->toWidget(), 0, Qt::AlignBaseline);
159
160     layout->addLayout(searchLayout);
161
162     layout->addSpacing(padding);
163
164     QHBoxLayout *recentLayout = new QHBoxLayout();
165     recentLayout->setMargin(0);
166     recentLayout->setSpacing(10);
167
168     recentKeywordsLayout = new QVBoxLayout();
169     recentKeywordsLayout->setMargin(0);
170     recentKeywordsLayout->setSpacing(0);
171     recentKeywordsLayout->setAlignment(Qt::AlignTop | Qt::AlignLeft);
172     recentKeywordsLabel = new QLabel(tr("Recent keywords"));
173     recentKeywordsLabel->setProperty("recentHeader", true);
174     recentKeywordsLabel->hide();
175     recentKeywordsLabel->setEnabled(false);
176     recentKeywordsLayout->addWidget(recentKeywordsLabel);
177     recentLayout->addLayout(recentKeywordsLayout);
178
179     recentChannelsLayout = new QVBoxLayout();
180     recentChannelsLayout->setMargin(0);
181     recentChannelsLayout->setSpacing(0);
182     recentChannelsLayout->setAlignment(Qt::AlignTop | Qt::AlignLeft);
183     recentChannelsLabel = new QLabel(tr("Recent channels"));
184     recentChannelsLabel->setProperty("recentHeader", true);
185     recentChannelsLabel->hide();
186     recentChannelsLabel->setEnabled(false);
187     recentChannelsLayout->addWidget(recentChannelsLabel);
188     recentLayout->addLayout(recentChannelsLayout);
189
190     layout->addLayout(recentLayout);
191
192     hLayout->addStretch();
193
194     vLayout->addStretch();
195
196 #ifdef APP_ACTIVATION
197     if (!Activation::instance().isActivated())
198         vLayout->addWidget(ActivationView::buyButton(tr("Get the full version")), 0,
199                            Qt::AlignRight);
200 #endif
201 }
202
203 void SearchView::appear() {
204     MainWindow *w = MainWindow::instance();
205     w->showActionsInStatusBar(
206             {w->getAction("manualplay"), w->getAction("safeSearch"), w->getAction("definition")},
207             true);
208
209     updateRecentKeywords();
210     updateRecentChannels();
211
212     queryEdit->selectAll();
213     queryEdit->enableSuggest();
214     QTimer::singleShot(0, queryEdit->toWidget(), SLOT(setFocus()));
215 }
216
217 void SearchView::disappear() {
218     MainWindow *w = MainWindow::instance();
219     w->showActionsInStatusBar(
220             {w->getAction("manualplay"), w->getAction("safeSearch"), w->getAction("definition")},
221             false);
222 }
223
224 void SearchView::updateRecentKeywords() {
225     // load
226     QSettings settings;
227     const QStringList keywords = settings.value(recentKeywordsKey).toStringList();
228     if (keywords == recentKeywords) return;
229     recentKeywords = keywords;
230
231     // cleanup
232     QLayoutItem *item;
233     while (recentKeywordsLayout->count() - 1 > recentKeywords.size() &&
234            (item = recentKeywordsLayout->takeAt(1)) != nullptr) {
235         item->widget()->close();
236         delete item;
237     }
238
239     recentKeywordsLabel->setVisible(!keywords.isEmpty());
240     MainWindow::instance()->getAction("clearRecentKeywords")->setEnabled(!keywords.isEmpty());
241
242     const int maxDisplayLength = 25;
243
244 #ifdef APP_MAC
245     QPalette p = palette();
246     p.setColor(QPalette::Highlight, mac::accentColor());
247 #endif
248
249     int counter = 1;
250     for (const QString &keyword : keywords) {
251         QString link = keyword;
252         QString display = keyword;
253         if (keyword.startsWith(QLatin1String("http://")) ||
254             keyword.startsWith(QLatin1String("https://"))) {
255             int separator = keyword.indexOf('|');
256             if (separator > 0 && separator + 1 < keyword.length()) {
257                 link = keyword.left(separator);
258                 display = keyword.mid(separator + 1);
259             }
260         }
261         bool needStatusTip = display.length() > maxDisplayLength;
262         if (needStatusTip) {
263             display.truncate(maxDisplayLength);
264             display.append(QStringLiteral("\u2026"));
265         }
266
267         ClickableLabel *item;
268         if (recentKeywordsLayout->count() - 1 >= counter) {
269             item = qobject_cast<ClickableLabel *>(recentKeywordsLayout->itemAt(counter)->widget());
270         } else {
271             item = new ClickableLabel();
272 #ifdef APP_MAC
273             item->setPalette(p);
274 #endif
275             item->setAttribute(Qt::WA_DeleteOnClose);
276             item->setProperty("recentItem", true);
277             item->setFocusPolicy(Qt::TabFocus);
278             connect(item, &ClickableLabel::hovered, this, [item, this](bool value) {
279                 item->setForegroundRole(value ? QPalette::Highlight : QPalette::WindowText);
280                 if (value) {
281                     for (int i = 1; i < recentKeywordsLayout->count(); ++i) {
282                         QWidget *w = recentKeywordsLayout->itemAt(i)->widget();
283                         if (w != item) {
284                             w->setForegroundRole(QPalette::WindowText);
285                         }
286                     }
287                 }
288             });
289             item->setContextMenuPolicy(Qt::ActionsContextMenu);
290             auto removeAction = new QAction(tr("Remove"));
291             item->addAction(removeAction);
292             connect(removeAction, &QAction::triggered, item, [item] {
293                 QSettings settings;
294                 QStringList keywords = settings.value(recentKeywordsKey).toStringList();
295                 QString keyword = item->property("keyword").toString();
296                 keywords.removeOne(keyword);
297                 settings.setValue(recentKeywordsKey, keywords);
298                 item->deleteLater();
299             });
300             recentKeywordsLayout->addWidget(item);
301         }
302
303         item->setText(display);
304         if (needStatusTip)
305             item->setStatusTip(link);
306         else
307             item->setStatusTip(QString());
308         item->setProperty("keyword", keyword);
309
310         disconnect(item, &ClickableLabel::clicked, nullptr, nullptr);
311         connect(item, &ClickableLabel::clicked, this, [this, link]() { watchKeywords(link); });
312
313         counter++;
314     }
315 }
316
317 void SearchView::updateRecentChannels() {
318     // load
319     QSettings settings;
320     const QStringList keywords = settings.value(recentChannelsKey).toStringList();
321     if (keywords == recentChannels) return;
322     recentChannels = keywords;
323
324     // cleanup
325     QLayoutItem *item;
326     while ((item = recentChannelsLayout->takeAt(1)) != nullptr) {
327         item->widget()->close();
328         delete item;
329     }
330
331     recentChannelsLabel->setVisible(!keywords.isEmpty());
332
333 #ifdef APP_MAC
334     QPalette p = palette();
335     p.setColor(QPalette::Highlight, mac::accentColor());
336 #endif
337
338     for (const QString &keyword : keywords) {
339         QString link = keyword;
340         QString display = keyword;
341         int separator = keyword.indexOf('|');
342         if (separator > 0 && separator + 1 < keyword.length()) {
343             link = keyword.left(separator);
344             display = keyword.mid(separator + 1);
345         }
346
347         ClickableLabel *item = new ClickableLabel(display);
348         item->setProperty("keyword", keyword);
349 #ifdef APP_MAC
350         item->setPalette(p);
351 #endif
352         item->setAttribute(Qt::WA_DeleteOnClose);
353         item->setProperty("recentItem", true);
354         item->setFocusPolicy(Qt::TabFocus);
355         connect(item, &ClickableLabel::clicked, [this, link]() { watchChannel(link); });
356         connect(item, &ClickableLabel::hovered, item, [item](bool value) {
357             item->setForegroundRole(value ? QPalette::Highlight : QPalette::WindowText);
358         });
359         item->setContextMenuPolicy(Qt::ActionsContextMenu);
360         auto removeAction = new QAction(tr("Remove"));
361         item->addAction(removeAction);
362         connect(removeAction, &QAction::triggered, item, [item] {
363             QSettings settings;
364             QStringList keywords = settings.value(recentChannelsKey).toStringList();
365             QString keyword = item->property("keyword").toString();
366             keywords.removeOne(keyword);
367             settings.setValue(recentChannelsKey, keywords);
368             item->deleteLater();
369         });
370         recentChannelsLayout->addWidget(item);
371     }
372 }
373
374 void SearchView::watch() {
375     QString query = queryEdit->text();
376     watch(query);
377 }
378
379 void SearchView::textChanged(const QString &text) {
380     lastChannelSuggestions.clear();
381 }
382
383 void SearchView::watch(const QString &query) {
384     QString q = query.simplified();
385
386     // check for empty query
387     if (q.isEmpty()) {
388         queryEdit->toWidget()->setFocus(Qt::OtherFocusReason);
389         return;
390     }
391
392     SearchParams *searchParams = new SearchParams();
393     searchParams->setKeywords(q);
394
395     // go!
396     emit search(searchParams);
397 }
398
399 void SearchView::watchChannel(const QString &channelId) {
400     if (channelId.isEmpty()) {
401         queryEdit->toWidget()->setFocus(Qt::OtherFocusReason);
402         return;
403     }
404
405     QString id = channelId;
406
407     // Fix old settings
408     const QLatin1String uc("UC");
409     if (!id.startsWith(uc)) id = uc + id;
410
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         connect(messageBar, &MessageBar::closed, this, [key] {
474             QSettings settings;
475             settings.setValue(key, true);
476         });
477         messageBar->show();
478         showMessages = false;
479     }
480 #endif
481
482     if (showMessages) {
483         key = "donate" + QLatin1String(Constants::VERSION);
484         if (!settings.contains(key)) {
485             bool oneYearUsage = true;
486 #ifdef APP_ACTIVATION
487             oneYearUsage = (QDateTime::currentSecsSinceEpoch() -
488                             Activation::instance().getLicenseTimestamp()) > 86400 * 365;
489 #endif
490             if (oneYearUsage) {
491                 QString msg =
492                         tr("I keep improving %1 to make it the best I can. Support this work!")
493                                 .arg(Constants::NAME);
494                 msg = "<a href='https://" + QLatin1String(Constants::ORG_DOMAIN) + "/donate" +
495                       "' style = 'text-decoration:none;color:palette(windowText)' > " + msg +
496                       "</a>";
497                 messageBar->setMessage(msg);
498                 connect(messageBar, &MessageBar::closed, this, [key] {
499                     QSettings settings;
500                     settings.setValue(key, true);
501                 });
502                 messageBar->show();
503             }
504         }
505     }
506 }