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 "painterutils.h"
48 const QString recentKeywordsKey = "recentKeywords";
49 const QString recentChannelsKey = "recentChannels";
52 SearchView::SearchView(QWidget *parent) : View(parent) {
53 setBackgroundRole(QPalette::Base);
54 setForegroundRole(QPalette::Text);
55 setAutoFillBackground(true);
57 const int padding = 30;
59 QBoxLayout *vLayout = new QVBoxLayout(this);
60 vLayout->setMargin(padding);
61 vLayout->setSpacing(0);
63 // hidden message widget
64 message = new QLabel(this);
66 vLayout->addWidget(message);
68 vLayout->addStretch();
70 QBoxLayout *hLayout = new QHBoxLayout();
71 hLayout->setAlignment(Qt::AlignCenter);
73 vLayout->addLayout(hLayout);
75 hLayout->addStretch();
77 logo = new ClickableLabel();
78 auto setLogoPixmap = [this] {
79 logo->setPixmap(IconUtils::pixmap(":/images/app.png", logo->devicePixelRatioF()));
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);
87 QVBoxLayout *layout = new QVBoxLayout();
88 layout->setAlignment(Qt::AlignCenter);
89 hLayout->addLayout(layout);
91 QLabel *welcomeLabel = new QLabel();
92 auto setupWelcomeLabel = [this, welcomeLabel] {
93 QColor titleColor = palette().color(QPalette::WindowText);
94 titleColor.setAlphaF(.75);
96 titleColor.getRgb(&r, &g, &b, &a);
97 QString cssColor = QString::asprintf("rgba(%d,%d,%d,%d)", r, g, b, a);
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) +
104 welcomeLabel->setText(text);
107 connect(qApp, &QGuiApplication::paletteChanged, this, setupWelcomeLabel);
108 welcomeLabel->setOpenExternalLinks(true);
109 welcomeLabel->setFont(FontUtils::light(welcomeLabel->font().pointSize()));
110 layout->addWidget(welcomeLabel);
112 layout->addSpacing(padding / 2);
114 //: "Enter", as in "type". The whole phrase says: "Enter a keyword to start watching videos"
115 // QLabel *tipLabel = new QLabel(tr("Enter"), this);
117 if (qApp->layoutDirection() == Qt::RightToLeft) {
118 tip = tr("to start watching videos.") + " " + tr("a keyword") + " " + tr("Enter");
120 tip = tr("Enter") + " " + tr("a keyword") + " " + tr("to start watching videos.");
123 layout->addSpacing(padding / 2);
125 QBoxLayout *searchLayout = new QHBoxLayout();
126 searchLayout->setAlignment(Qt::AlignVCenter);
128 #ifdef APP_MAC_SEARCHFIELD
129 SearchLineEditMac *slem = new SearchLineEditMac(this);
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);
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);
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);
156 searchLayout->addWidget(queryEdit->toWidget(), 0, Qt::AlignBaseline);
158 layout->addLayout(searchLayout);
160 layout->addSpacing(padding);
162 QHBoxLayout *recentLayout = new QHBoxLayout();
163 recentLayout->setMargin(0);
164 recentLayout->setSpacing(10);
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);
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);
188 layout->addLayout(recentLayout);
190 hLayout->addStretch();
192 vLayout->addStretch();
194 #ifdef APP_ACTIVATION
195 if (!Activation::instance().isActivated())
196 vLayout->addWidget(ActivationView::buyButton(tr("Get the full version")), 0,
201 void SearchView::appear() {
202 MainWindow *w = MainWindow::instance();
203 w->showActionsInStatusBar(
204 {w->getAction("manualplay"), w->getAction("safeSearch"), w->getAction("definition")},
207 updateRecentKeywords();
208 updateRecentChannels();
210 queryEdit->selectAll();
211 queryEdit->enableSuggest();
212 QTimer::singleShot(0, queryEdit->toWidget(), SLOT(setFocus()));
215 void SearchView::disappear() {
216 MainWindow *w = MainWindow::instance();
217 w->showActionsInStatusBar(
218 {w->getAction("manualplay"), w->getAction("safeSearch"), w->getAction("definition")},
222 void SearchView::updateRecentKeywords() {
225 const QStringList keywords = settings.value(recentKeywordsKey).toStringList();
226 if (keywords == recentKeywords) return;
227 recentKeywords = keywords;
231 while (recentKeywordsLayout->count() - 1 > recentKeywords.size() &&
232 (item = recentKeywordsLayout->takeAt(1)) != nullptr) {
233 item->widget()->close();
237 recentKeywordsLabel->setVisible(!keywords.isEmpty());
238 MainWindow::instance()->getAction("clearRecentKeywords")->setEnabled(!keywords.isEmpty());
240 const int maxDisplayLength = 25;
243 QPalette p = palette();
244 p.setColor(QPalette::Highlight, mac::accentColor());
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);
259 bool needStatusTip = display.length() > maxDisplayLength;
261 display.truncate(maxDisplayLength);
262 display.append(QStringLiteral("\u2026"));
265 ClickableLabel *item;
266 if (recentKeywordsLayout->count() - 1 >= counter) {
267 item = qobject_cast<ClickableLabel *>(recentKeywordsLayout->itemAt(counter)->widget());
270 item = new ClickableLabel();
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);
280 for (int i = 1; i < recentKeywordsLayout->count(); ++i) {
281 QWidget *w = recentKeywordsLayout->itemAt(i)->widget();
283 w->setForegroundRole(QPalette::WindowText);
288 recentKeywordsLayout->addWidget(item);
291 item->setText(display);
293 item->setStatusTip(link);
295 item->setStatusTip(QString());
297 disconnect(item, &ClickableLabel::clicked, nullptr, nullptr);
298 connect(item, &ClickableLabel::clicked, this, [this, link]() { watchKeywords(link); });
304 void SearchView::updateRecentChannels() {
307 const QStringList keywords = settings.value(recentChannelsKey).toStringList();
308 if (keywords == recentChannels) return;
309 recentChannels = keywords;
313 while ((item = recentChannelsLayout->takeAt(1)) != nullptr) {
314 item->widget()->close();
318 recentChannelsLabel->setVisible(!keywords.isEmpty());
321 QPalette p = palette();
322 p.setColor(QPalette::Highlight, mac::accentColor());
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);
334 ClickableLabel *item = new ClickableLabel(display);
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);
345 recentChannelsLayout->addWidget(item);
349 void SearchView::watch() {
350 QString query = queryEdit->text();
354 void SearchView::textChanged(const QString &text) {
355 lastChannelSuggestions.clear();
358 void SearchView::watch(const QString &query) {
359 QString q = query.simplified();
361 // check for empty query
363 queryEdit->toWidget()->setFocus(Qt::OtherFocusReason);
367 SearchParams *searchParams = new SearchParams();
368 searchParams->setKeywords(q);
371 emit search(searchParams);
374 void SearchView::watchChannel(const QString &channelId) {
375 if (channelId.isEmpty()) {
376 queryEdit->toWidget()->setFocus(Qt::OtherFocusReason);
380 QString id = channelId;
383 const QLatin1String uc("UC");
384 if (!id.startsWith(uc)) id = uc + id;
386 SearchParams *searchParams = new SearchParams();
387 searchParams->setChannelId(id);
388 searchParams->setSortBy(SearchParams::SortByNewest);
391 emit search(searchParams);
394 void SearchView::watchKeywords(const QString &query) {
395 QString q = query.simplified();
397 // check for empty query
399 queryEdit->toWidget()->setFocus(Qt::OtherFocusReason);
403 queryEdit->setText(q);
405 SearchParams *searchParams = new SearchParams();
406 searchParams->setKeywords(q);
409 emit search(searchParams);
412 void SearchView::searchTypeChanged(int index) {
414 queryEdit->setSuggester(youtubeSuggest);
416 queryEdit->setSuggester(channelSuggest);
418 queryEdit->selectAll();
419 queryEdit->toWidget()->setFocus();
422 void SearchView::suggestionAccepted(Suggestion *suggestion) {
423 if (suggestion->type == QLatin1String("channel")) {
424 watchChannel(suggestion->userData);
426 watch(suggestion->value);
429 void SearchView::onChannelSuggestions(const QVector<Suggestion *> &suggestions) {
430 lastChannelSuggestions = suggestions;