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"
53 const QString recentKeywordsKey = "recentKeywords";
54 const QString recentChannelsKey = "recentChannels";
57 SearchView::SearchView(QWidget *parent) : View(parent) {
58 setBackgroundRole(QPalette::Base);
59 setForegroundRole(QPalette::Text);
60 setAutoFillBackground(true);
62 const int padding = 30;
64 QBoxLayout *vLayout = new QVBoxLayout(this);
65 vLayout->setMargin(padding);
66 vLayout->setSpacing(0);
68 messageBar = new MessageBar();
70 vLayout->addWidget(messageBar, 0, Qt::AlignCenter);
71 vLayout->addSpacing(padding);
74 vLayout->addStretch();
76 QBoxLayout *hLayout = new QHBoxLayout();
77 hLayout->setAlignment(Qt::AlignCenter);
79 vLayout->addLayout(hLayout);
81 hLayout->addStretch();
83 logo = new ClickableLabel();
84 auto setLogoPixmap = [this] {
85 logo->setPixmap(IconUtils::pixmap(":/images/app.png", logo->devicePixelRatioF()));
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);
93 QVBoxLayout *layout = new QVBoxLayout();
94 layout->setAlignment(Qt::AlignCenter);
95 hLayout->addLayout(layout);
97 QLabel *welcomeLabel = new QLabel();
98 auto setupWelcomeLabel = [this, welcomeLabel] {
99 QColor titleColor = palette().color(QPalette::WindowText);
100 titleColor.setAlphaF(.75);
102 titleColor.getRgb(&r, &g, &b, &a);
103 QString cssColor = QString::asprintf("rgba(%d,%d,%d,%d)", r, g, b, a);
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) +
110 welcomeLabel->setText(text);
113 connect(qApp, &QGuiApplication::paletteChanged, this, setupWelcomeLabel);
114 welcomeLabel->setOpenExternalLinks(true);
115 welcomeLabel->setFont(FontUtils::light(welcomeLabel->font().pointSize()));
116 layout->addWidget(welcomeLabel);
118 layout->addSpacing(padding / 2);
120 //: "Enter", as in "type". The whole phrase says: "Enter a keyword to start watching videos"
121 // QLabel *tipLabel = new QLabel(tr("Enter"), this);
123 if (qApp->layoutDirection() == Qt::RightToLeft) {
124 tip = tr("to start watching videos.") + " " + tr("a keyword") + " " + tr("Enter");
126 tip = tr("Enter") + " " + tr("a keyword") + " " + tr("to start watching videos.");
129 layout->addSpacing(padding / 2);
131 QBoxLayout *searchLayout = new QHBoxLayout();
132 searchLayout->setAlignment(Qt::AlignVCenter);
134 #ifdef APP_MAC_SEARCHFIELD
135 SearchLineEditMac *slem = new SearchLineEditMac(this);
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);
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);
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);
162 searchLayout->addWidget(queryEdit->toWidget(), 0, Qt::AlignBaseline);
164 layout->addLayout(searchLayout);
166 layout->addSpacing(padding);
168 QHBoxLayout *recentLayout = new QHBoxLayout();
169 recentLayout->setMargin(0);
170 recentLayout->setSpacing(10);
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);
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);
194 layout->addLayout(recentLayout);
196 hLayout->addStretch();
198 vLayout->addStretch();
200 #ifdef APP_ACTIVATION
201 if (!Activation::instance().isActivated())
202 vLayout->addWidget(ActivationView::buyButton(tr("Get the full version")), 0,
207 void SearchView::appear() {
208 MainWindow *w = MainWindow::instance();
209 w->showActionsInStatusBar(
210 {w->getAction("manualplay"), w->getAction("safeSearch"), w->getAction("definition")},
213 updateRecentKeywords();
214 updateRecentChannels();
216 queryEdit->selectAll();
217 queryEdit->enableSuggest();
218 QTimer::singleShot(0, queryEdit->toWidget(), SLOT(setFocus()));
221 void SearchView::disappear() {
222 MainWindow *w = MainWindow::instance();
223 w->showActionsInStatusBar(
224 {w->getAction("manualplay"), w->getAction("safeSearch"), w->getAction("definition")},
228 void SearchView::updateRecentKeywords() {
231 const QStringList keywords = settings.value(recentKeywordsKey).toStringList();
232 if (keywords == recentKeywords) return;
233 recentKeywords = keywords;
237 while (recentKeywordsLayout->count() - 1 > recentKeywords.size() &&
238 (item = recentKeywordsLayout->takeAt(1)) != nullptr) {
239 item->widget()->close();
243 recentKeywordsLabel->setVisible(!keywords.isEmpty());
244 MainWindow::instance()->getAction("clearRecentKeywords")->setEnabled(!keywords.isEmpty());
246 const int maxDisplayLength = 25;
249 QPalette p = palette();
250 p.setColor(QPalette::Highlight, mac::accentColor());
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);
265 bool needStatusTip = display.length() > maxDisplayLength;
267 display.truncate(maxDisplayLength);
268 display.append(QStringLiteral("\u2026"));
271 ClickableLabel *item;
272 if (recentKeywordsLayout->count() - 1 >= counter) {
273 item = qobject_cast<ClickableLabel *>(recentKeywordsLayout->itemAt(counter)->widget());
275 item = new ClickableLabel();
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);
285 for (int i = 1; i < recentKeywordsLayout->count(); ++i) {
286 QWidget *w = recentKeywordsLayout->itemAt(i)->widget();
288 w->setForegroundRole(QPalette::WindowText);
293 item->setContextMenuPolicy(Qt::ActionsContextMenu);
294 auto removeAction = new QAction(tr("Remove"));
295 item->addAction(removeAction);
296 connect(removeAction, &QAction::triggered, item, [item] {
298 QStringList keywords = settings.value(recentKeywordsKey).toStringList();
299 QString keyword = item->property("keyword").toString();
300 keywords.removeOne(keyword);
301 settings.setValue(recentKeywordsKey, keywords);
304 recentKeywordsLayout->addWidget(item);
307 item->setText(display);
309 item->setStatusTip(link);
311 item->setStatusTip(QString());
312 item->setProperty("keyword", keyword);
314 disconnect(item, &ClickableLabel::clicked, nullptr, nullptr);
315 connect(item, &ClickableLabel::clicked, this, [this, link]() { watchKeywords(link); });
321 void SearchView::updateRecentChannels() {
324 const QStringList keywords = settings.value(recentChannelsKey).toStringList();
325 if (keywords == recentChannels) return;
326 recentChannels = keywords;
330 while ((item = recentChannelsLayout->takeAt(1)) != nullptr) {
331 item->widget()->close();
335 recentChannelsLabel->setVisible(!keywords.isEmpty());
338 QPalette p = palette();
339 p.setColor(QPalette::Highlight, mac::accentColor());
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);
351 ClickableLabel *item = new ClickableLabel(display);
352 item->setProperty("keyword", keyword);
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);
363 item->setContextMenuPolicy(Qt::ActionsContextMenu);
364 auto removeAction = new QAction(tr("Remove"));
365 item->addAction(removeAction);
366 connect(removeAction, &QAction::triggered, item, [item] {
368 QStringList keywords = settings.value(recentChannelsKey).toStringList();
369 QString keyword = item->property("keyword").toString();
370 keywords.removeOne(keyword);
371 settings.setValue(recentChannelsKey, keywords);
374 recentChannelsLayout->addWidget(item);
378 void SearchView::watch() {
379 QString query = queryEdit->text();
383 void SearchView::textChanged(const QString &text) {
384 lastChannelSuggestions.clear();
387 void SearchView::watch(const QString &query) {
388 QString q = query.simplified();
390 // check for empty query
392 queryEdit->toWidget()->setFocus(Qt::OtherFocusReason);
396 SearchParams *searchParams = new SearchParams();
397 searchParams->setKeywords(q);
400 emit search(searchParams);
403 void SearchView::watchChannel(const QString &channelId) {
404 if (channelId.isEmpty()) {
405 queryEdit->toWidget()->setFocus(Qt::OtherFocusReason);
409 QString id = channelId;
410 qDebug() << "Channel" << 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 messageBar->setOpenExternalLinks(true);
474 disconnect(messageBar);
475 connect(messageBar, &MessageBar::closed, this, [key] {
477 settings.setValue(key, true);
480 showMessages = false;
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;
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] {
505 settings.setValue(key, true);
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)'>" +
518 messageBar->setMessage(msg);
519 messageBar->setOpenExternalLinks(false);
520 disconnect(messageBar);
521 connect(messageBar, &MessageBar::linkActivated, this, [] {
522 Updater::instance().setRelaunchAfterInstall(true);