3 This file is part of Minitube.
4 Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
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.
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.
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/>.
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"
30 #include "searchlineedit.h"
39 #include "activation.h"
40 #include "activationview.h"
42 #include "clickablelabel.h"
43 #include "iconutils.h"
44 #include "mainwindow.h"
45 #include "messagebar.h"
46 #include "painterutils.h"
49 const QString recentKeywordsKey = "recentKeywords";
50 const QString recentChannelsKey = "recentChannels";
53 SearchView::SearchView(QWidget *parent) : View(parent) {
54 setBackgroundRole(QPalette::Base);
55 setForegroundRole(QPalette::Text);
56 setAutoFillBackground(true);
58 const int padding = 30;
60 QBoxLayout *vLayout = new QVBoxLayout(this);
61 vLayout->setMargin(padding);
62 vLayout->setSpacing(0);
64 messageBar = new MessageBar();
66 vLayout->addWidget(messageBar, 0, Qt::AlignCenter);
67 vLayout->addSpacing(padding);
70 vLayout->addStretch();
72 QBoxLayout *hLayout = new QHBoxLayout();
73 hLayout->setAlignment(Qt::AlignCenter);
75 vLayout->addLayout(hLayout);
77 hLayout->addStretch();
79 logo = new ClickableLabel();
80 auto setLogoPixmap = [this] {
81 logo->setPixmap(IconUtils::pixmap(":/images/app.png", logo->devicePixelRatioF()));
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);
89 QVBoxLayout *layout = new QVBoxLayout();
90 layout->setAlignment(Qt::AlignCenter);
91 hLayout->addLayout(layout);
93 QLabel *welcomeLabel = new QLabel();
94 auto setupWelcomeLabel = [this, welcomeLabel] {
95 QColor titleColor = palette().color(QPalette::WindowText);
96 titleColor.setAlphaF(.75);
98 titleColor.getRgb(&r, &g, &b, &a);
99 QString cssColor = QString::asprintf("rgba(%d,%d,%d,%d)", r, g, b, a);
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) +
106 welcomeLabel->setText(text);
109 connect(qApp, &QGuiApplication::paletteChanged, this, setupWelcomeLabel);
110 welcomeLabel->setOpenExternalLinks(true);
111 welcomeLabel->setFont(FontUtils::light(welcomeLabel->font().pointSize()));
112 layout->addWidget(welcomeLabel);
114 layout->addSpacing(padding / 2);
116 //: "Enter", as in "type". The whole phrase says: "Enter a keyword to start watching videos"
117 // QLabel *tipLabel = new QLabel(tr("Enter"), this);
119 if (qApp->layoutDirection() == Qt::RightToLeft) {
120 tip = tr("to start watching videos.") + " " + tr("a keyword") + " " + tr("Enter");
122 tip = tr("Enter") + " " + tr("a keyword") + " " + tr("to start watching videos.");
125 layout->addSpacing(padding / 2);
127 QBoxLayout *searchLayout = new QHBoxLayout();
128 searchLayout->setAlignment(Qt::AlignVCenter);
130 #ifdef APP_MAC_SEARCHFIELD
131 SearchLineEditMac *slem = new SearchLineEditMac(this);
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);
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);
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);
158 searchLayout->addWidget(queryEdit->toWidget(), 0, Qt::AlignBaseline);
160 layout->addLayout(searchLayout);
162 layout->addSpacing(padding);
164 QHBoxLayout *recentLayout = new QHBoxLayout();
165 recentLayout->setMargin(0);
166 recentLayout->setSpacing(10);
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);
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);
190 layout->addLayout(recentLayout);
192 hLayout->addStretch();
194 vLayout->addStretch();
196 #ifdef APP_ACTIVATION
197 if (!Activation::instance().isActivated())
198 vLayout->addWidget(ActivationView::buyButton(tr("Get the full version")), 0,
203 void SearchView::appear() {
204 MainWindow *w = MainWindow::instance();
205 w->showActionsInStatusBar(
206 {w->getAction("manualplay"), w->getAction("safeSearch"), w->getAction("definition")},
209 updateRecentKeywords();
210 updateRecentChannels();
212 queryEdit->selectAll();
213 queryEdit->enableSuggest();
214 QTimer::singleShot(0, queryEdit->toWidget(), SLOT(setFocus()));
217 void SearchView::disappear() {
218 MainWindow *w = MainWindow::instance();
219 w->showActionsInStatusBar(
220 {w->getAction("manualplay"), w->getAction("safeSearch"), w->getAction("definition")},
224 void SearchView::updateRecentKeywords() {
227 const QStringList keywords = settings.value(recentKeywordsKey).toStringList();
228 if (keywords == recentKeywords) return;
229 recentKeywords = keywords;
233 while (recentKeywordsLayout->count() - 1 > recentKeywords.size() &&
234 (item = recentKeywordsLayout->takeAt(1)) != nullptr) {
235 item->widget()->close();
239 recentKeywordsLabel->setVisible(!keywords.isEmpty());
240 MainWindow::instance()->getAction("clearRecentKeywords")->setEnabled(!keywords.isEmpty());
242 const int maxDisplayLength = 25;
245 QPalette p = palette();
246 p.setColor(QPalette::Highlight, mac::accentColor());
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);
261 bool needStatusTip = display.length() > maxDisplayLength;
263 display.truncate(maxDisplayLength);
264 display.append(QStringLiteral("\u2026"));
267 ClickableLabel *item;
268 if (recentKeywordsLayout->count() - 1 >= counter) {
269 item = qobject_cast<ClickableLabel *>(recentKeywordsLayout->itemAt(counter)->widget());
271 item = new ClickableLabel();
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);
281 for (int i = 1; i < recentKeywordsLayout->count(); ++i) {
282 QWidget *w = recentKeywordsLayout->itemAt(i)->widget();
284 w->setForegroundRole(QPalette::WindowText);
289 item->setContextMenuPolicy(Qt::ActionsContextMenu);
290 auto removeAction = new QAction(tr("Remove"));
291 item->addAction(removeAction);
292 connect(removeAction, &QAction::triggered, item, [item] {
294 QStringList keywords = settings.value(recentKeywordsKey).toStringList();
295 QString keyword = item->property("keyword").toString();
296 keywords.removeOne(keyword);
297 settings.setValue(recentKeywordsKey, keywords);
300 recentKeywordsLayout->addWidget(item);
303 item->setText(display);
305 item->setStatusTip(link);
307 item->setStatusTip(QString());
308 item->setProperty("keyword", keyword);
310 disconnect(item, &ClickableLabel::clicked, nullptr, nullptr);
311 connect(item, &ClickableLabel::clicked, this, [this, link]() { watchKeywords(link); });
317 void SearchView::updateRecentChannels() {
320 const QStringList keywords = settings.value(recentChannelsKey).toStringList();
321 if (keywords == recentChannels) return;
322 recentChannels = keywords;
326 while ((item = recentChannelsLayout->takeAt(1)) != nullptr) {
327 item->widget()->close();
331 recentChannelsLabel->setVisible(!keywords.isEmpty());
334 QPalette p = palette();
335 p.setColor(QPalette::Highlight, mac::accentColor());
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);
347 ClickableLabel *item = new ClickableLabel(display);
348 item->setProperty("keyword", keyword);
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);
359 item->setContextMenuPolicy(Qt::ActionsContextMenu);
360 auto removeAction = new QAction(tr("Remove"));
361 item->addAction(removeAction);
362 connect(removeAction, &QAction::triggered, item, [item] {
364 QStringList keywords = settings.value(recentChannelsKey).toStringList();
365 QString keyword = item->property("keyword").toString();
366 keywords.removeOne(keyword);
367 settings.setValue(recentChannelsKey, keywords);
370 recentChannelsLayout->addWidget(item);
374 void SearchView::watch() {
375 QString query = queryEdit->text();
379 void SearchView::textChanged(const QString &text) {
380 lastChannelSuggestions.clear();
383 void SearchView::watch(const QString &query) {
384 QString q = query.simplified();
386 // check for empty query
388 queryEdit->toWidget()->setFocus(Qt::OtherFocusReason);
392 SearchParams *searchParams = new SearchParams();
393 searchParams->setKeywords(q);
396 emit search(searchParams);
399 void SearchView::watchChannel(const QString &channelId) {
400 if (channelId.isEmpty()) {
401 queryEdit->toWidget()->setFocus(Qt::OtherFocusReason);
405 QString id = channelId;
408 const QLatin1String uc("UC");
409 if (!id.startsWith(uc)) id = uc + id;
411 SearchParams *searchParams = new SearchParams();
412 searchParams->setChannelId(id);
413 searchParams->setSortBy(SearchParams::SortByNewest);
416 emit search(searchParams);
419 void SearchView::watchKeywords(const QString &query) {
420 QString q = query.simplified();
422 // check for empty query
424 queryEdit->toWidget()->setFocus(Qt::OtherFocusReason);
428 queryEdit->setText(q);
430 SearchParams *searchParams = new SearchParams();
431 searchParams->setKeywords(q);
434 emit search(searchParams);
437 void SearchView::searchTypeChanged(int index) {
439 queryEdit->setSuggester(youtubeSuggest);
441 queryEdit->setSuggester(channelSuggest);
443 queryEdit->selectAll();
444 queryEdit->toWidget()->setFocus();
447 void SearchView::suggestionAccepted(Suggestion *suggestion) {
448 if (suggestion->type == QLatin1String("channel")) {
449 watchChannel(suggestion->userData);
451 watch(suggestion->value);
454 void SearchView::onChannelSuggestions(const QVector<Suggestion *> &suggestions) {
455 lastChannelSuggestions = suggestions;
458 void SearchView::maybeShowMessage() {
462 bool showMessages = true;
463 #ifdef APP_ACTIVATION
464 showMessages = Activation::instance().isActivated();
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] {
475 settings.setValue(key, true);
478 showMessages = false;
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;
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 +
497 messageBar->setMessage(msg);
498 connect(messageBar, &MessageBar::closed, this, [key] {
500 settings.setValue(key, true);