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