init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
This commit is contained in:
267
Telegram/SourceFiles/ui/boxes/auto_delete_settings.cpp
Normal file
267
Telegram/SourceFiles/ui/boxes/auto_delete_settings.cpp
Normal file
@@ -0,0 +1,267 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/boxes/auto_delete_settings.h"
|
||||
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/painter.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
object_ptr<Ui::RpWidget> CreateSliderForTTL(
|
||||
not_null<QWidget*> parent,
|
||||
std::vector<QString> labels,
|
||||
int dashedAfterIndex,
|
||||
int selected,
|
||||
Fn<void(int)> callback) {
|
||||
Expects(labels.size() > 1);
|
||||
Expects(selected >= 0 && selected < labels.size());
|
||||
Expects(dashedAfterIndex >= 0 && dashedAfterIndex < labels.size());
|
||||
|
||||
struct State {
|
||||
std::vector<int> points;
|
||||
std::vector<QString> labels;
|
||||
int selected = 0;
|
||||
};
|
||||
static const auto st = &st::defaultSliderForTTL;
|
||||
const auto height = st->font->height + st->skip + st->chosenSize;
|
||||
const auto count = int(labels.size());
|
||||
|
||||
auto result = object_ptr<Ui::FixedHeightWidget>(parent.get(), height);
|
||||
const auto raw = result.data();
|
||||
const auto slider = Ui::CreateChild<Ui::FixedHeightWidget>(
|
||||
raw,
|
||||
st->chosenSize);
|
||||
slider->setCursor(style::cur_pointer);
|
||||
slider->move(0, height - slider->height());
|
||||
|
||||
auto &lifetime = raw->lifetime();
|
||||
const auto state = lifetime.make_state<State>(State{
|
||||
.labels = std::move(labels),
|
||||
.selected = selected
|
||||
});
|
||||
state->points.resize(count, 0);
|
||||
|
||||
raw->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
for (auto i = 0; i != count; ++i) {
|
||||
state->points[i] = (width * i) / (count - 1);
|
||||
}
|
||||
slider->resize(width, slider->height());
|
||||
}, lifetime);
|
||||
|
||||
raw->paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
auto p = QPainter(raw);
|
||||
|
||||
p.setFont(st->font);
|
||||
for (auto i = 0; i != count; ++i) {
|
||||
// Label
|
||||
p.setPen(st->textFg);
|
||||
const auto &text = state->labels[i];
|
||||
const auto textWidth = st->font->width(text);
|
||||
const auto shift = (i == count - 1)
|
||||
? textWidth
|
||||
: (i > 0)
|
||||
? (textWidth / 2)
|
||||
: 0;
|
||||
const auto x = state->points[i] - shift;
|
||||
const auto y = st->font->ascent;
|
||||
p.drawText(x, y, text);
|
||||
}
|
||||
}, lifetime);
|
||||
|
||||
slider->paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
auto p = QPainter(slider);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
|
||||
p.setFont(st->font);
|
||||
for (auto i = 0; i != count; ++i) {
|
||||
const auto middle = (st->chosenSize / 2.);
|
||||
|
||||
// Point
|
||||
const auto size = (i == state->selected)
|
||||
? st->chosenSize
|
||||
: st->pointSize;
|
||||
const auto pointfg = (i <= state->selected)
|
||||
? st->activeFg
|
||||
: st->inactiveFg;
|
||||
const auto shift = (i == count - 1)
|
||||
? float64(size)
|
||||
: (i > 0)
|
||||
? (size / 2.)
|
||||
: 0.;
|
||||
const auto pointx = state->points[i] - shift;
|
||||
const auto pointy = middle - (size / 2.);
|
||||
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(pointfg);
|
||||
p.drawEllipse(QRectF{ pointx, pointy, size * 1., size * 1. });
|
||||
|
||||
// Line
|
||||
if (i + 1 == count) {
|
||||
break;
|
||||
}
|
||||
const auto nextSize = (i + 1 == state->selected)
|
||||
? st->chosenSize
|
||||
: st->pointSize;
|
||||
const auto nextShift = (i + 1 == count - 1)
|
||||
? float64(nextSize)
|
||||
: (nextSize / 2.);
|
||||
const auto &linefg = (i + 1 <= state->selected)
|
||||
? st->activeFg
|
||||
: st->inactiveFg;
|
||||
const auto from = pointx + size + st->stroke * 1.5;
|
||||
const auto till = state->points[i + 1] - nextShift - st->stroke * 1.5;
|
||||
|
||||
auto pen = linefg->p;
|
||||
pen.setWidthF(st->stroke);
|
||||
if (i >= dashedAfterIndex) {
|
||||
// Try to fill the line with exact number of dash segments.
|
||||
// UPD Doesn't work so well because it changes when clicking.
|
||||
//const auto length = till - from;
|
||||
//const auto offSegmentsCount = int(base::SafeRound(
|
||||
// (length - st->dashOn) / (st->dashOn + st->dashOff)));
|
||||
//const auto onSegmentsCount = offSegmentsCount + 1;
|
||||
//const auto idealLength = offSegmentsCount * st->dashOff
|
||||
// + onSegmentsCount * st->dashOn;
|
||||
//const auto multiplier = length / float64(idealLength);
|
||||
|
||||
const auto multiplier = 1.;
|
||||
auto dashPattern = QVector<qreal>{
|
||||
st->dashOn * multiplier / st->stroke,
|
||||
st->dashOff * multiplier / st->stroke
|
||||
};
|
||||
pen.setDashPattern(dashPattern);
|
||||
}
|
||||
pen.setCapStyle(Qt::RoundCap);
|
||||
p.setPen(pen);
|
||||
|
||||
p.setBrush(Qt::NoBrush);
|
||||
p.drawLine(QPointF(from, middle), QPointF(till, middle));
|
||||
}
|
||||
}, lifetime);
|
||||
|
||||
slider->events(
|
||||
) | rpl::filter([=](not_null<QEvent*> e) {
|
||||
return (e->type() == QEvent::MouseButtonPress)
|
||||
&& (static_cast<QMouseEvent*>(e.get())->button()
|
||||
== Qt::LeftButton)
|
||||
&& (state->points[1] > 0);
|
||||
}) | rpl::map([=](not_null<QEvent*> e) {
|
||||
return rpl::single(
|
||||
static_cast<QMouseEvent*>(e.get())->pos()
|
||||
) | rpl::then(slider->events(
|
||||
) | rpl::take_while([=](not_null<QEvent*> e) {
|
||||
return (e->type() != QEvent::MouseButtonRelease)
|
||||
|| (static_cast<QMouseEvent*>(e.get())->button()
|
||||
!= Qt::LeftButton);
|
||||
}) | rpl::filter([=](not_null<QEvent*> e) {
|
||||
return (e->type() == QEvent::MouseMove);
|
||||
}) | rpl::map([=](not_null<QEvent*> e) {
|
||||
return static_cast<QMouseEvent*>(e.get())->pos();
|
||||
}));
|
||||
}) | rpl::flatten_latest(
|
||||
) | rpl::on_next([=](QPoint position) {
|
||||
state->selected = std::clamp(
|
||||
(position.x() + (state->points[1] / 2)) / state->points[1],
|
||||
0,
|
||||
count - 1);
|
||||
slider->update();
|
||||
callback(state->selected);
|
||||
}, lifetime);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void AutoDeleteSettingsBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
TimeId ttlPeriod,
|
||||
rpl::producer<QString> about,
|
||||
Fn<void(TimeId)> callback) {
|
||||
box->setTitle(tr::lng_manage_messages_ttl_title());
|
||||
|
||||
struct State {
|
||||
TimeId period = 0;
|
||||
};
|
||||
|
||||
const auto state = box->lifetime().make_state<State>(State{
|
||||
.period = ttlPeriod,
|
||||
});
|
||||
|
||||
const auto options = std::vector<QString>{
|
||||
tr::lng_manage_messages_ttl_disable(tr::now),
|
||||
//u"5 seconds"_q, AssertIsDebug()
|
||||
tr::lng_manage_messages_ttl_after1(tr::now),
|
||||
tr::lng_manage_messages_ttl_after2(tr::now),
|
||||
tr::lng_manage_messages_ttl_after3(tr::now),
|
||||
};
|
||||
const auto periodToIndex = [&](TimeId period) {
|
||||
return !period
|
||||
? 0
|
||||
//: (period == 5) AssertIsDebug()
|
||||
//? 1 AssertIsDebug()
|
||||
: (period < 2 * 86400)
|
||||
? 1
|
||||
: (period < 8 * 86400)
|
||||
? 2
|
||||
: 3;
|
||||
};
|
||||
const auto indexToPeriod = [&](int index) {
|
||||
return !index
|
||||
? 0
|
||||
//: (index == 1) AssertIsDebug()
|
||||
//? 5 AssertIsDebug()
|
||||
: (index == 1)
|
||||
? 86400
|
||||
: (index == 2)
|
||||
? 7 * 86400
|
||||
: 31 * 86400;
|
||||
};
|
||||
const auto sliderCallback = [=](int index) {
|
||||
state->period = indexToPeriod(index);
|
||||
};
|
||||
box->addRow(
|
||||
CreateSliderForTTL(
|
||||
box,
|
||||
options | ranges::to_vector,
|
||||
options.size() - 1,
|
||||
periodToIndex(ttlPeriod),
|
||||
sliderCallback),
|
||||
{
|
||||
st::boxRowPadding.left(),
|
||||
0,
|
||||
st::boxRowPadding.right(),
|
||||
st::boxMediumSkip });
|
||||
|
||||
box->addRow(
|
||||
object_ptr<Ui::DividerLabel>(
|
||||
box,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
std::move(about),
|
||||
st::boxDividerLabel),
|
||||
st::ttlDividerLabelPadding),
|
||||
style::margins());
|
||||
|
||||
box->addButton(tr::lng_settings_save(), [=] {
|
||||
const auto period = state->period;
|
||||
box->closeBox();
|
||||
|
||||
callback(period);
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
20
Telegram/SourceFiles/ui/boxes/auto_delete_settings.h
Normal file
20
Telegram/SourceFiles/ui/boxes/auto_delete_settings.h
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/layers/generic_box.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
void AutoDeleteSettingsBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
TimeId ttlPeriod,
|
||||
rpl::producer<QString> about,
|
||||
Fn<void(TimeId)> callback);
|
||||
|
||||
} // namespace Ui
|
||||
986
Telegram/SourceFiles/ui/boxes/boost_box.cpp
Normal file
986
Telegram/SourceFiles/ui/boxes/boost_box.cpp
Normal file
@@ -0,0 +1,986 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/boxes/boost_box.h"
|
||||
|
||||
#include "info/profile/info_profile_icon.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/effects/fireworks_animation.h"
|
||||
#include "ui/effects/premium_bubble.h"
|
||||
#include "ui/effects/premium_graphics.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/wrap/fade_wrap.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rect.h"
|
||||
#include "styles/style_giveaway.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_premium.h"
|
||||
|
||||
#include <QtGui/QGuiApplication>
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
[[nodiscard]] BoostCounters AdjustByReached(BoostCounters data) {
|
||||
const auto exact = (data.boosts == data.thisLevelBoosts);
|
||||
const auto reached = !data.nextLevelBoosts || (exact && data.mine > 0);
|
||||
if (reached) {
|
||||
--data.level;
|
||||
data.boosts = data.nextLevelBoosts = std::max({
|
||||
data.boosts,
|
||||
data.thisLevelBoosts,
|
||||
1
|
||||
});
|
||||
data.thisLevelBoosts = 0;
|
||||
} else {
|
||||
data.boosts = std::max(data.thisLevelBoosts, data.boosts);
|
||||
data.nextLevelBoosts = std::max(
|
||||
data.nextLevelBoosts,
|
||||
data.boosts + 1);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> MakeTitle(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
rpl::producer<QString> title,
|
||||
rpl::producer<QString> repeated,
|
||||
bool centered = true) {
|
||||
auto result = object_ptr<Ui::RpWidget>(parent);
|
||||
|
||||
struct State {
|
||||
not_null<Ui::FlatLabel*> title;
|
||||
not_null<Ui::FlatLabel*> repeated;
|
||||
};
|
||||
const auto notEmpty = [](const QString &text) {
|
||||
return !text.isEmpty();
|
||||
};
|
||||
const auto state = parent->lifetime().make_state<State>(State{
|
||||
.title = Ui::CreateChild<Ui::FlatLabel>(
|
||||
result.data(),
|
||||
rpl::duplicate(title),
|
||||
st::boostTitle),
|
||||
.repeated = Ui::CreateChild<Ui::FlatLabel>(
|
||||
result.data(),
|
||||
rpl::duplicate(repeated) | rpl::filter(notEmpty),
|
||||
st::boostTitleBadge),
|
||||
});
|
||||
state->title->show();
|
||||
state->repeated->showOn(std::move(repeated) | rpl::map(notEmpty));
|
||||
|
||||
result->resize(result->width(), st::boostTitle.style.font->height);
|
||||
|
||||
rpl::combine(
|
||||
result->widthValue(),
|
||||
rpl::duplicate(title),
|
||||
state->repeated->shownValue(),
|
||||
state->repeated->widthValue()
|
||||
) | rpl::on_next([=](int outer, auto&&, bool shown, int badge) {
|
||||
const auto repeated = shown ? badge : 0;
|
||||
const auto skip = st::boostTitleBadgeSkip;
|
||||
const auto available = outer - repeated - skip;
|
||||
const auto use = std::min(state->title->textMaxWidth(), available);
|
||||
state->title->resizeToWidth(use);
|
||||
const auto left = centered
|
||||
? (outer - use - skip - repeated) / 2
|
||||
: 0;
|
||||
state->title->moveToLeft(left, 0);
|
||||
const auto mleft = st::boostTitleBadge.margin.left();
|
||||
const auto mtop = st::boostTitleBadge.margin.top();
|
||||
state->repeated->moveToLeft(left + use + skip + mleft, mtop);
|
||||
}, result->lifetime());
|
||||
|
||||
const auto badge = state->repeated;
|
||||
badge->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(badge);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
const auto radius = std::min(badge->width(), badge->height()) / 2;
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st::premiumButtonBg2);
|
||||
p.drawRoundedRect(badge->rect(), radius, radius);
|
||||
}, badge->lifetime());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::FlatLabel> MakeFeaturesBadge(
|
||||
not_null<QWidget*> parent,
|
||||
rpl::producer<QString> text) {
|
||||
return MakeBoostFeaturesBadge(parent, std::move(text), [](QRect rect) {
|
||||
auto gradient = QLinearGradient(
|
||||
rect.topLeft(),
|
||||
rect.topRight());
|
||||
gradient.setStops(Ui::Premium::GiftGradientStops());
|
||||
return QBrush(gradient);
|
||||
});
|
||||
}
|
||||
|
||||
void AddFeaturesList(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
const Ui::BoostFeatures &features,
|
||||
int startFromLevel,
|
||||
bool group) {
|
||||
const auto add = [&](
|
||||
rpl::producer<TextWithEntities> text,
|
||||
const style::icon &st) {
|
||||
const auto label = container->add(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
container,
|
||||
std::move(text),
|
||||
st::boostFeatureLabel),
|
||||
st::boostFeaturePadding);
|
||||
object_ptr<Info::Profile::FloatingIcon>(
|
||||
label,
|
||||
st,
|
||||
st::boostFeatureIconPosition);
|
||||
};
|
||||
const auto lowMax = std::max({
|
||||
features.linkLogoLevel,
|
||||
features.profileIconLevel,
|
||||
features.autotranslateLevel,
|
||||
features.transcribeLevel,
|
||||
features.emojiPackLevel,
|
||||
features.emojiStatusLevel,
|
||||
features.wallpaperLevel,
|
||||
features.customWallpaperLevel,
|
||||
(features.nameColorsByLevel.empty()
|
||||
? 0
|
||||
: features.nameColorsByLevel.back().first),
|
||||
(features.linkStylesByLevel.empty()
|
||||
? 0
|
||||
: features.linkStylesByLevel.back().first),
|
||||
(features.profileColorsByLevel.empty()
|
||||
? 0
|
||||
: features.profileColorsByLevel.back().first),
|
||||
});
|
||||
const auto highMax = std::max(lowMax, features.sponsoredLevel);
|
||||
auto nameColors = 0;
|
||||
auto linkStyles = 0;
|
||||
auto profileColors = 0;
|
||||
for (auto i = std::max(startFromLevel, 1); i <= highMax; ++i) {
|
||||
if ((i > lowMax) && (i < highMax)) {
|
||||
continue;
|
||||
}
|
||||
const auto unlocks = (i == startFromLevel);
|
||||
{
|
||||
const auto badge = container->add(
|
||||
MakeFeaturesBadge(
|
||||
container,
|
||||
(unlocks
|
||||
? tr::lng_boost_level_unlocks
|
||||
: tr::lng_boost_level)(
|
||||
lt_count,
|
||||
rpl::single(float64(i)))),
|
||||
st::boostLevelBadgePadding,
|
||||
style::al_top);
|
||||
const auto padding = st::boxRowPadding;
|
||||
const auto line = Ui::CreateChild<Ui::RpWidget>(container);
|
||||
badge->geometryValue() | rpl::on_next([=](const QRect &r) {
|
||||
line->setGeometry(
|
||||
padding.left(),
|
||||
r.y(),
|
||||
container->width() - rect::m::sum::h(padding),
|
||||
r.height());
|
||||
}, line->lifetime());
|
||||
const auto shift = st::lineWidth * 10;
|
||||
line->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(line);
|
||||
p.setPen(st::windowSubTextFg);
|
||||
const auto y = line->height() / 2;
|
||||
const auto left = badge->x() - shift - padding.left();
|
||||
const auto right = left + badge->width() + shift * 2;
|
||||
if (left > 0) {
|
||||
p.drawLine(0, y, left, y);
|
||||
}
|
||||
if (right < line->width()) {
|
||||
p.drawLine(right, y, line->width(), y);
|
||||
}
|
||||
}, line->lifetime());
|
||||
}
|
||||
if (i >= features.sponsoredLevel) {
|
||||
add(
|
||||
tr::lng_channel_earn_off(tr::rich),
|
||||
st::boostFeatureOffSponsored);
|
||||
}
|
||||
if (i >= features.customWallpaperLevel) {
|
||||
add(
|
||||
(group
|
||||
? tr::lng_feature_custom_background_group
|
||||
: tr::lng_feature_custom_background_channel)(tr::rich),
|
||||
st::boostFeatureCustomBackground);
|
||||
}
|
||||
if (i >= features.wallpaperLevel) {
|
||||
add(
|
||||
(group
|
||||
? tr::lng_feature_backgrounds_group
|
||||
: tr::lng_feature_backgrounds_channel)(
|
||||
lt_count,
|
||||
rpl::single(float64(features.wallpapersCount)),
|
||||
tr::rich),
|
||||
st::boostFeatureBackground);
|
||||
}
|
||||
if (i >= features.emojiStatusLevel) {
|
||||
add(
|
||||
tr::lng_feature_emoji_status(tr::rich),
|
||||
st::boostFeatureEmojiStatus);
|
||||
}
|
||||
if (const auto j = features.profileColorsByLevel.find(i)
|
||||
; j != end(features.profileColorsByLevel)) {
|
||||
profileColors += j->second;
|
||||
}
|
||||
if (i >= features.profileIconLevel) {
|
||||
add(
|
||||
(group
|
||||
? tr::lng_feature_profile_icon_group
|
||||
: tr::lng_feature_profile_icon_channel)(tr::rich),
|
||||
st::boostFeatureProfileIcon);
|
||||
}
|
||||
if (profileColors > 0) {
|
||||
add((group
|
||||
? tr::lng_feature_profile_color_group
|
||||
: tr::lng_feature_profile_color_channel)(
|
||||
lt_count,
|
||||
rpl::single(float64(profileColors)),
|
||||
tr::rich
|
||||
), st::boostFeatureProfileColor);
|
||||
}
|
||||
if (!group) {
|
||||
if (const auto j = features.linkStylesByLevel.find(i)
|
||||
; j != end(features.linkStylesByLevel)) {
|
||||
linkStyles += j->second;
|
||||
}
|
||||
if (i >= features.linkLogoLevel) {
|
||||
add(
|
||||
tr::lng_feature_link_emoji(tr::rich),
|
||||
st::boostFeatureCustomLink);
|
||||
}
|
||||
if (linkStyles > 0) {
|
||||
add(tr::lng_feature_link_style_channel(
|
||||
lt_count,
|
||||
rpl::single(float64(linkStyles)),
|
||||
tr::rich
|
||||
), st::boostFeatureLink);
|
||||
}
|
||||
if (const auto j = features.nameColorsByLevel.find(i)
|
||||
; j != end(features.nameColorsByLevel)) {
|
||||
nameColors += j->second;
|
||||
}
|
||||
if (nameColors > 0) {
|
||||
add(tr::lng_feature_name_color_channel(
|
||||
lt_count,
|
||||
rpl::single(float64(nameColors)),
|
||||
tr::rich
|
||||
), st::boostFeatureName);
|
||||
}
|
||||
add(tr::lng_feature_reactions(
|
||||
lt_count,
|
||||
rpl::single(float64(i)),
|
||||
tr::rich
|
||||
), st::boostFeatureCustomReactions);
|
||||
}
|
||||
add(
|
||||
tr::lng_feature_stories(lt_count, rpl::single(1. * i), tr::rich),
|
||||
st::boostFeatureStories);
|
||||
if (!group && i >= features.autotranslateLevel) {
|
||||
add(
|
||||
tr::lng_feature_autotranslate(tr::rich),
|
||||
st::boostFeatureAutoTranslate);
|
||||
}
|
||||
if (group && i >= features.transcribeLevel) {
|
||||
add(
|
||||
tr::lng_feature_transcribe(tr::rich),
|
||||
st::boostFeatureTranscribe);
|
||||
}
|
||||
if (group && i >= features.emojiPackLevel) {
|
||||
add(
|
||||
tr::lng_feature_custom_emoji_pack(tr::rich),
|
||||
st::boostFeatureCustomEmoji);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void StartFireworks(not_null<QWidget*> parent) {
|
||||
const auto result = Ui::CreateChild<RpWidget>(parent.get());
|
||||
result->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
result->setGeometry(parent->rect());
|
||||
result->show();
|
||||
|
||||
auto &lifetime = result->lifetime();
|
||||
const auto animation = lifetime.make_state<FireworksAnimation>([=] {
|
||||
result->update();
|
||||
});
|
||||
result->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(result);
|
||||
if (!animation->paint(p, result->rect())) {
|
||||
crl::on_main(result, [=] { delete result; });
|
||||
}
|
||||
}, lifetime);
|
||||
}
|
||||
|
||||
void BoostBox(
|
||||
not_null<GenericBox*> box,
|
||||
BoostBoxData data,
|
||||
Fn<void(Fn<void(BoostCounters)>)> boost) {
|
||||
box->setWidth(st::boxWideWidth);
|
||||
box->setStyle(st::boostBox);
|
||||
|
||||
//AssertIsDebug();
|
||||
//data.boost = {
|
||||
// .level = 2,
|
||||
// .boosts = 3,
|
||||
// .thisLevelBoosts = 2,
|
||||
// .nextLevelBoosts = 5,
|
||||
// .mine = 2,
|
||||
//};
|
||||
|
||||
struct State {
|
||||
rpl::variable<BoostCounters> data;
|
||||
rpl::variable<bool> full;
|
||||
bool submitted = false;
|
||||
};
|
||||
const auto state = box->lifetime().make_state<State>();
|
||||
state->data = std::move(data.boost);
|
||||
|
||||
FillBoostLimit(
|
||||
BoxShowFinishes(box),
|
||||
box->verticalLayout(),
|
||||
state->data.value(),
|
||||
st::boxRowPadding);
|
||||
|
||||
box->setMaxHeight(st::boostBoxMaxHeight);
|
||||
const auto close = box->addTopButton(
|
||||
st::boxTitleClose,
|
||||
[=] { box->closeBox(); });
|
||||
|
||||
const auto name = data.name;
|
||||
|
||||
auto title = state->data.value(
|
||||
) | rpl::map([=](BoostCounters counters) {
|
||||
return (counters.mine > 0)
|
||||
? tr::lng_boost_channel_you_title(
|
||||
lt_channel,
|
||||
rpl::single(name))
|
||||
: !counters.nextLevelBoosts
|
||||
? tr::lng_boost_channel_title_max()
|
||||
: counters.level
|
||||
? (data.group
|
||||
? tr::lng_boost_channel_title_more_group()
|
||||
: tr::lng_boost_channel_title_more())
|
||||
: (data.group
|
||||
? tr::lng_boost_channel_title_first_group()
|
||||
: tr::lng_boost_channel_title_first());
|
||||
}) | rpl::flatten_latest();
|
||||
auto repeated = state->data.value(
|
||||
) | rpl::map([=](BoostCounters counters) {
|
||||
return (counters.mine > 1) ? u"x%1"_q.arg(counters.mine) : u""_q;
|
||||
});
|
||||
|
||||
const auto wasMine = state->data.current().mine;
|
||||
const auto wasLifting = data.lifting;
|
||||
auto text = state->data.value(
|
||||
) | rpl::map([=](BoostCounters counters) {
|
||||
const auto lifting = wasLifting
|
||||
? (wasLifting
|
||||
- std::clamp(counters.mine - wasMine, 0, wasLifting - 1))
|
||||
: 0;
|
||||
const auto bold = tr::bold(name);
|
||||
const auto now = counters.boosts;
|
||||
const auto full = !counters.nextLevelBoosts;
|
||||
const auto left = (counters.nextLevelBoosts > now)
|
||||
? (counters.nextLevelBoosts - now)
|
||||
: 0;
|
||||
auto post = tr::lng_boost_channel_post_stories(
|
||||
lt_count,
|
||||
rpl::single(float64(counters.level + (left ? 1 : 0))),
|
||||
tr::rich);
|
||||
return (lifting > 1)
|
||||
? tr::lng_boost_group_lift_restrictions_many(
|
||||
lt_count,
|
||||
rpl::single(float64(lifting)),
|
||||
tr::rich)
|
||||
: lifting
|
||||
? tr::lng_boost_group_lift_restrictions(tr::rich)
|
||||
: (counters.mine || full)
|
||||
? (left
|
||||
? tr::lng_boost_channel_needs_unlock(
|
||||
lt_count,
|
||||
rpl::single(float64(left)),
|
||||
lt_channel,
|
||||
rpl::single(bold),
|
||||
tr::rich)
|
||||
: (!counters.level
|
||||
? (data.group
|
||||
? tr::lng_boost_channel_reached_first_group
|
||||
: tr::lng_boost_channel_reached_first)(
|
||||
tr::rich)
|
||||
: (data.group
|
||||
? tr::lng_boost_channel_reached_more_group
|
||||
: tr::lng_boost_channel_reached_more)(
|
||||
lt_count,
|
||||
rpl::single(float64(counters.level)),
|
||||
lt_post,
|
||||
std::move(post),
|
||||
tr::rich)))
|
||||
: tr::lng_boost_channel_needs_unlock(
|
||||
lt_count,
|
||||
rpl::single(float64(left)),
|
||||
lt_channel,
|
||||
rpl::single(bold),
|
||||
tr::rich);
|
||||
}) | rpl::flatten_latest();
|
||||
if (wasLifting) {
|
||||
state->data.value(
|
||||
) | rpl::on_next([=](BoostCounters counters) {
|
||||
if (counters.mine - wasMine >= wasLifting) {
|
||||
box->closeBox();
|
||||
}
|
||||
}, box->lifetime());
|
||||
}
|
||||
|
||||
auto faded = object_ptr<Ui::FadeWrap<>>(
|
||||
close->parentWidget(),
|
||||
MakeTitle(
|
||||
box,
|
||||
(data.group
|
||||
? tr::lng_boost_group_button
|
||||
: tr::lng_boost_channel_button)(),
|
||||
rpl::duplicate(repeated),
|
||||
false));
|
||||
const auto titleInner = faded.data();
|
||||
titleInner->move(st::boxTitlePosition);
|
||||
titleInner->resizeToWidth(st::boxWideWidth
|
||||
- st::boxTitleClose.width
|
||||
- st::boxTitlePosition.x());
|
||||
titleInner->hide(anim::type::instant);
|
||||
crl::on_main(titleInner, [=] {
|
||||
titleInner->raise();
|
||||
titleInner->toggleOn(rpl::single(
|
||||
rpl::empty
|
||||
) | rpl::then(
|
||||
box->scrolls()
|
||||
) | rpl::map([=] {
|
||||
return box->scrollTop() > 0;
|
||||
}));
|
||||
});
|
||||
|
||||
box->addRow(
|
||||
MakeTitle(box, std::move(title), std::move(repeated)),
|
||||
st::boxRowPadding + QMargins(0, st::boostTitleSkip, 0, 0));
|
||||
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
std::move(text),
|
||||
st::boostText),
|
||||
(st::boxRowPadding
|
||||
+ QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip)));
|
||||
|
||||
const auto current = state->data.current();
|
||||
box->setTitle(rpl::single(QString()));
|
||||
AddFeaturesList(
|
||||
box->verticalLayout(),
|
||||
data.features,
|
||||
current.level + (current.nextLevelBoosts ? 1 : 0),
|
||||
data.group);
|
||||
|
||||
const auto allowMulti = data.allowMulti;
|
||||
auto submit = state->data.value(
|
||||
) | rpl::map([=](BoostCounters counters) {
|
||||
return (!counters.nextLevelBoosts || (counters.mine && !allowMulti))
|
||||
? tr::lng_box_ok()
|
||||
: (counters.mine > 0)
|
||||
? tr::lng_boost_again_button()
|
||||
: data.group
|
||||
? tr::lng_boost_group_button()
|
||||
: tr::lng_boost_channel_button();
|
||||
}) | rpl::flatten_latest();
|
||||
|
||||
box->addButton(rpl::duplicate(submit), [=] {
|
||||
if (state->submitted) {
|
||||
return;
|
||||
} else if (state->data.current().nextLevelBoosts > 0
|
||||
&& (allowMulti || !state->data.current().mine)) {
|
||||
state->submitted = true;
|
||||
const auto was = state->data.current().mine;
|
||||
|
||||
//AssertIsDebug();
|
||||
//state->submitted = false;
|
||||
//if (state->data.current().level == 5
|
||||
// && state->data.current().boosts == 11) {
|
||||
// state->data = BoostCounters{
|
||||
// .level = 5,
|
||||
// .boosts = 14,
|
||||
// .thisLevelBoosts = 9,
|
||||
// .nextLevelBoosts = 15,
|
||||
// .mine = 14,
|
||||
// };
|
||||
//} else if (state->data.current().level == 5) {
|
||||
// state->data = BoostCounters{
|
||||
// .level = 7,
|
||||
// .boosts = 16,
|
||||
// .thisLevelBoosts = 15,
|
||||
// .nextLevelBoosts = 19,
|
||||
// .mine = 16,
|
||||
// };
|
||||
//} else if (state->data.current().level == 4) {
|
||||
// state->data = BoostCounters{
|
||||
// .level = 5,
|
||||
// .boosts = 11,
|
||||
// .thisLevelBoosts = 9,
|
||||
// .nextLevelBoosts = 15,
|
||||
// .mine = 9,
|
||||
// };
|
||||
//} else if (state->data.current().level == 3) {
|
||||
// state->data = BoostCounters{
|
||||
// .level = 4,
|
||||
// .boosts = 7,
|
||||
// .thisLevelBoosts = 7,
|
||||
// .nextLevelBoosts = 9,
|
||||
// .mine = 5,
|
||||
// };
|
||||
//} else {
|
||||
// state->data = BoostCounters{
|
||||
// .level = 3,
|
||||
// .boosts = 5,
|
||||
// .thisLevelBoosts = 5,
|
||||
// .nextLevelBoosts = 7,
|
||||
// .mine = 3,
|
||||
// };
|
||||
//}
|
||||
//return;
|
||||
|
||||
boost(crl::guard(box, [=](BoostCounters result) {
|
||||
state->submitted = false;
|
||||
|
||||
if (result.thisLevelBoosts || result.nextLevelBoosts) {
|
||||
if (result.mine > was) {
|
||||
StartFireworks(box->parentWidget());
|
||||
}
|
||||
state->data = result;
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
box->closeBox();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
object_ptr<Ui::RpWidget> MakeLinkLabel(
|
||||
not_null<QWidget*> parent,
|
||||
rpl::producer<QString> text,
|
||||
rpl::producer<QString> link,
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
object_ptr<Ui::RpWidget> right) {
|
||||
auto result = object_ptr<Ui::AbstractButton>(parent);
|
||||
const auto raw = result.data();
|
||||
|
||||
const auto rawRight = right.release();
|
||||
if (rawRight) {
|
||||
rawRight->setParent(raw);
|
||||
rawRight->show();
|
||||
}
|
||||
|
||||
struct State {
|
||||
State(
|
||||
not_null<QWidget*> parent,
|
||||
rpl::producer<QString> value,
|
||||
rpl::producer<QString> link)
|
||||
: text(std::move(value))
|
||||
, link(std::move(link))
|
||||
, label(parent, text.value(), st::giveawayGiftCodeLink)
|
||||
, bg(st::roundRadiusLarge, st::windowBgOver) {
|
||||
}
|
||||
|
||||
rpl::variable<QString> text;
|
||||
rpl::variable<QString> link;
|
||||
Ui::FlatLabel label;
|
||||
Ui::RoundRect bg;
|
||||
};
|
||||
|
||||
const auto state = raw->lifetime().make_state<State>(
|
||||
raw,
|
||||
rpl::duplicate(text),
|
||||
std::move(link));
|
||||
state->label.setSelectable(true);
|
||||
|
||||
rpl::combine(
|
||||
raw->widthValue(),
|
||||
std::move(text)
|
||||
) | rpl::on_next([=](int outer, const auto&) {
|
||||
const auto textWidth = state->label.textMaxWidth();
|
||||
const auto skipLeft = st::giveawayGiftCodeLink.margin.left();
|
||||
const auto skipRight = rawRight
|
||||
? rawRight->width()
|
||||
: st::giveawayGiftCodeLink.margin.right();
|
||||
const auto available = outer - skipRight - skipLeft;
|
||||
const auto use = std::min(textWidth, available);
|
||||
state->label.resizeToWidth(use);
|
||||
const auto forCenter = (outer - use) / 2;
|
||||
const auto x = (forCenter < skipLeft)
|
||||
? skipLeft
|
||||
: (forCenter > outer - skipRight - use)
|
||||
? (outer - skipRight - use)
|
||||
: forCenter;
|
||||
state->label.moveToLeft(x, st::giveawayGiftCodeLink.margin.top());
|
||||
}, raw->lifetime());
|
||||
|
||||
raw->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(raw);
|
||||
state->bg.paint(p, raw->rect());
|
||||
}, raw->lifetime());
|
||||
|
||||
state->label.setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
|
||||
raw->resize(raw->width(), st::giveawayGiftCodeLinkHeight);
|
||||
if (rawRight) {
|
||||
raw->widthValue() | rpl::on_next([=](int width) {
|
||||
rawRight->move(width - rawRight->width(), 0);
|
||||
}, raw->lifetime());
|
||||
}
|
||||
raw->setClickedCallback([=] {
|
||||
QGuiApplication::clipboard()->setText(state->link.current());
|
||||
show->showToast(tr::lng_username_copied(tr::now));
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void BoostBoxAlready(not_null<GenericBox*> box, bool group) {
|
||||
ConfirmBox(box, {
|
||||
.text = (group
|
||||
? tr::lng_boost_error_already_text_group
|
||||
: tr::lng_boost_error_already_text)(tr::rich),
|
||||
.title = tr::lng_boost_error_already_title(),
|
||||
.inform = true,
|
||||
});
|
||||
}
|
||||
|
||||
void GiftForBoostsBox(
|
||||
not_null<GenericBox*> box,
|
||||
QString channel,
|
||||
int receive,
|
||||
bool again) {
|
||||
ConfirmBox(box, {
|
||||
.text = (again
|
||||
? tr::lng_boost_need_more_again
|
||||
: tr::lng_boost_need_more_text)(
|
||||
lt_count,
|
||||
rpl::single(receive) | tr::to_count(),
|
||||
lt_channel,
|
||||
rpl::single(TextWithEntities{ channel }),
|
||||
tr::rich),
|
||||
.title = tr::lng_boost_need_more(),
|
||||
.inform = true,
|
||||
});
|
||||
}
|
||||
|
||||
void GiftedNoBoostsBox(not_null<GenericBox*> box, bool group) {
|
||||
InformBox(box, {
|
||||
.text = (group
|
||||
? tr::lng_boost_error_gifted_text_group
|
||||
: tr::lng_boost_error_gifted_text)(tr::rich),
|
||||
.title = tr::lng_boost_error_gifted_title(),
|
||||
});
|
||||
}
|
||||
|
||||
void PremiumForBoostsBox(
|
||||
not_null<GenericBox*> box,
|
||||
bool group,
|
||||
Fn<void()> buyPremium) {
|
||||
ConfirmBox(box, {
|
||||
.text = (group
|
||||
? tr::lng_boost_error_premium_text_group
|
||||
: tr::lng_boost_error_premium_text)(tr::rich),
|
||||
.confirmed = buyPremium,
|
||||
.confirmText = tr::lng_boost_error_premium_yes(),
|
||||
.title = tr::lng_boost_error_premium_title(),
|
||||
});
|
||||
}
|
||||
|
||||
void AskBoostBox(
|
||||
not_null<GenericBox*> box,
|
||||
AskBoostBoxData data,
|
||||
Fn<void()> openStatistics,
|
||||
Fn<void()> startGiveaway) {
|
||||
box->setWidth(st::boxWideWidth);
|
||||
box->setStyle(st::boostBox);
|
||||
box->setNoContentMargin(true);
|
||||
box->addSkip(st::boxRowPadding.left());
|
||||
|
||||
FillBoostLimit(
|
||||
BoxShowFinishes(box),
|
||||
box->verticalLayout(),
|
||||
rpl::single(data.boost),
|
||||
st::boxRowPadding);
|
||||
|
||||
box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); });
|
||||
|
||||
auto title = v::match(data.reason.data, [](AskBoostChannelColor) {
|
||||
return tr::lng_boost_channel_title_color();
|
||||
}, [](AskBoostAutotranslate) {
|
||||
return tr::lng_boost_channel_title_autotranslate();
|
||||
}, [](AskBoostWallpaper) {
|
||||
return tr::lng_boost_channel_title_wallpaper();
|
||||
}, [](AskBoostEmojiStatus) {
|
||||
return tr::lng_boost_channel_title_status();
|
||||
}, [](AskBoostEmojiPack) {
|
||||
return tr::lng_boost_group_title_emoji();
|
||||
}, [](AskBoostCustomReactions) {
|
||||
return tr::lng_boost_channel_title_reactions();
|
||||
}, [](AskBoostCpm) {
|
||||
return tr::lng_boost_channel_title_cpm();
|
||||
}, [](AskBoostWearCollectible) {
|
||||
return tr::lng_boost_channel_title_wear();
|
||||
});
|
||||
auto isGroup = false;
|
||||
auto reasonText = v::match(data.reason.data, [&](
|
||||
AskBoostChannelColor data) {
|
||||
return tr::lng_boost_channel_needs_level_color(
|
||||
lt_count,
|
||||
rpl::single(float64(data.requiredLevel)),
|
||||
tr::rich);
|
||||
}, [&](AskBoostAutotranslate data) {
|
||||
return tr::lng_boost_channel_needs_level_autotranslate(
|
||||
lt_count,
|
||||
rpl::single(float64(data.requiredLevel)),
|
||||
tr::rich);
|
||||
}, [&](AskBoostWallpaper data) {
|
||||
isGroup = data.group;
|
||||
return (data.group
|
||||
? tr::lng_boost_group_needs_level_wallpaper
|
||||
: tr::lng_boost_channel_needs_level_wallpaper)(
|
||||
lt_count,
|
||||
rpl::single(float64(data.requiredLevel)),
|
||||
tr::rich);
|
||||
}, [&](AskBoostEmojiStatus data) {
|
||||
isGroup = data.group;
|
||||
return (data.group
|
||||
? tr::lng_boost_group_needs_level_status
|
||||
: tr::lng_boost_channel_needs_level_status)(
|
||||
lt_count,
|
||||
rpl::single(float64(data.requiredLevel)),
|
||||
tr::rich);
|
||||
}, [&](AskBoostEmojiPack data) {
|
||||
isGroup = true;
|
||||
return tr::lng_boost_group_needs_level_emoji(
|
||||
lt_count,
|
||||
rpl::single(float64(data.requiredLevel)),
|
||||
tr::rich);
|
||||
}, [&](AskBoostCustomReactions data) {
|
||||
return tr::lng_boost_channel_needs_level_reactions(
|
||||
lt_count,
|
||||
rpl::single(float64(data.count)),
|
||||
lt_same_count,
|
||||
rpl::single(TextWithEntities{ QString::number(data.count) }),
|
||||
tr::rich);
|
||||
}, [&](AskBoostCpm data) {
|
||||
return tr::lng_boost_channel_needs_level_cpm(
|
||||
lt_count,
|
||||
rpl::single(float64(data.requiredLevel)),
|
||||
tr::rich);
|
||||
}, [&](AskBoostWearCollectible data) {
|
||||
return tr::lng_boost_channel_needs_level_wear(
|
||||
lt_count,
|
||||
rpl::single(float64(data.requiredLevel)),
|
||||
tr::rich);
|
||||
});
|
||||
auto text = rpl::combine(
|
||||
std::move(reasonText),
|
||||
(isGroup ? tr::lng_boost_group_ask : tr::lng_boost_channel_ask)(
|
||||
tr::rich)
|
||||
) | rpl::map([](TextWithEntities &&text, TextWithEntities &&ask) {
|
||||
return text.append(u"\n\n"_q).append(std::move(ask));
|
||||
});
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
std::move(title),
|
||||
st::boostCenteredTitle),
|
||||
st::boxRowPadding + QMargins(0, st::boostTitleSkip, 0, 0),
|
||||
style::al_top);
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
std::move(text),
|
||||
st::boostText),
|
||||
(st::boxRowPadding
|
||||
+ QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip)),
|
||||
style::al_top);
|
||||
|
||||
auto stats = object_ptr<Ui::IconButton>(box, st::boostLinkStatsButton);
|
||||
stats->setClickedCallback(openStatistics);
|
||||
box->addRow(MakeLinkLabel(
|
||||
box,
|
||||
rpl::single(data.link),
|
||||
rpl::single(data.link),
|
||||
box->uiShow(),
|
||||
std::move(stats)));
|
||||
|
||||
AddFeaturesList(
|
||||
box->verticalLayout(),
|
||||
data.features,
|
||||
data.boost.level + (data.boost.nextLevelBoosts ? 1 : 0),
|
||||
data.group);
|
||||
|
||||
auto submit = tr::lng_boost_channel_ask_button();
|
||||
box->addButton(rpl::duplicate(submit), [=] {
|
||||
QGuiApplication::clipboard()->setText(data.link);
|
||||
box->uiShow()->showToast(tr::lng_username_copied(tr::now));
|
||||
});
|
||||
}
|
||||
|
||||
void FillBoostLimit(
|
||||
rpl::producer<> showFinished,
|
||||
not_null<VerticalLayout*> container,
|
||||
rpl::producer<BoostCounters> data,
|
||||
style::margins limitLinePadding) {
|
||||
const auto addSkip = [&](int skip) {
|
||||
container->add(object_ptr<Ui::FixedHeightWidget>(container, skip));
|
||||
};
|
||||
|
||||
const auto ratio = [=](BoostCounters counters) {
|
||||
const auto min = counters.thisLevelBoosts;
|
||||
const auto max = counters.nextLevelBoosts;
|
||||
|
||||
Assert(counters.boosts >= min && counters.boosts <= max);
|
||||
const auto count = (max - min);
|
||||
const auto index = (counters.boosts - min);
|
||||
if (!index) {
|
||||
return 0.;
|
||||
} else if (index == count) {
|
||||
return 1.;
|
||||
} else if (count == 2) {
|
||||
return 0.5;
|
||||
}
|
||||
const auto available = st::boxWideWidth
|
||||
- st::boxPadding.left()
|
||||
- st::boxPadding.right();
|
||||
const auto average = available / float64(count);
|
||||
const auto levelWidth = [&](int add) {
|
||||
return st::normalFont->width(
|
||||
tr::lng_boost_level(
|
||||
tr::now,
|
||||
lt_count,
|
||||
counters.level + add));
|
||||
};
|
||||
const auto paddings = 2 * st::premiumLineTextSkip;
|
||||
const auto labelLeftWidth = paddings + levelWidth(0);
|
||||
const auto labelRightWidth = paddings + levelWidth(1);
|
||||
const auto first = std::max(average, labelLeftWidth * 1.);
|
||||
const auto last = std::max(average, labelRightWidth * 1.);
|
||||
const auto other = (available - first - last) / (count - 2);
|
||||
return (first + (index - 1) * other) / available;
|
||||
};
|
||||
|
||||
auto adjustedData = rpl::duplicate(data) | rpl::map(AdjustByReached);
|
||||
|
||||
auto bubbleRowState = rpl::duplicate(
|
||||
adjustedData
|
||||
) | rpl::combine_previous(
|
||||
BoostCounters()
|
||||
) | rpl::map([=](BoostCounters previous, BoostCounters counters) {
|
||||
return Premium::BubbleRowState{
|
||||
.counter = counters.boosts,
|
||||
.ratio = ratio(counters),
|
||||
.animateFromZero = (counters.level != previous.level),
|
||||
.dynamic = true,
|
||||
};
|
||||
});
|
||||
Premium::AddBubbleRow(
|
||||
container,
|
||||
st::boostBubble,
|
||||
std::move(showFinished),
|
||||
rpl::duplicate(bubbleRowState),
|
||||
Premium::BubbleType::Premium,
|
||||
nullptr,
|
||||
&st::premiumIconBoost,
|
||||
limitLinePadding);
|
||||
addSkip(st::premiumLineTextSkip);
|
||||
|
||||
const auto level = [](int level) {
|
||||
return tr::lng_boost_level(tr::now, lt_count, level);
|
||||
};
|
||||
auto limitState = std::move(
|
||||
bubbleRowState
|
||||
) | rpl::map([](const Premium::BubbleRowState &state) {
|
||||
return Premium::LimitRowState{
|
||||
.ratio = state.ratio,
|
||||
.animateFromZero = state.animateFromZero,
|
||||
.dynamic = state.dynamic
|
||||
};
|
||||
});
|
||||
auto left = rpl::duplicate(
|
||||
adjustedData
|
||||
) | rpl::map([=](BoostCounters counters) {
|
||||
return level(counters.level);
|
||||
});
|
||||
auto right = rpl::duplicate(
|
||||
adjustedData
|
||||
) | rpl::map([=](BoostCounters counters) {
|
||||
return level(counters.level + 1);
|
||||
});
|
||||
Premium::AddLimitRow(
|
||||
container,
|
||||
st::boostLimits,
|
||||
Premium::LimitRowLabels{
|
||||
.leftLabel = std::move(left),
|
||||
.rightLabel = std::move(right),
|
||||
},
|
||||
std::move(limitState),
|
||||
limitLinePadding);
|
||||
}
|
||||
|
||||
object_ptr<Ui::FlatLabel> MakeBoostFeaturesBadge(
|
||||
not_null<QWidget*> parent,
|
||||
rpl::producer<QString> text,
|
||||
Fn<QBrush(QRect)> bg) {
|
||||
auto result = object_ptr<Ui::FlatLabel>(
|
||||
parent,
|
||||
std::move(text),
|
||||
st::boostLevelBadge);
|
||||
const auto label = result.data();
|
||||
|
||||
label->show();
|
||||
label->paintRequest() | rpl::on_next([=] {
|
||||
const auto size = label->textMaxWidth();
|
||||
const auto rect = QRect(
|
||||
(label->width() - size) / 2,
|
||||
st::boostLevelBadge.margin.top(),
|
||||
size,
|
||||
st::boostLevelBadge.style.font->height
|
||||
).marginsAdded(st::boostLevelBadge.margin);
|
||||
auto p = QPainter(label);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setBrush(bg(rect));
|
||||
p.setPen(Qt::NoPen);
|
||||
p.drawRoundedRect(rect, rect.height() / 2., rect.height() / 2.);
|
||||
|
||||
const auto &lineFg = st::windowBgRipple;
|
||||
const auto line = st::boostLevelBadgeLine;
|
||||
const auto top = st::boostLevelBadge.margin.top()
|
||||
+ ((st::boostLevelBadge.style.font->height - line) / 2);
|
||||
const auto left = 0;
|
||||
const auto skip = st::boostLevelBadgeSkip;
|
||||
if (const auto right = rect.x() - skip; right > left) {
|
||||
p.fillRect(left, top, right - left, line, lineFg);
|
||||
}
|
||||
const auto right = label->width();
|
||||
if (const auto left = rect.x() + rect.width() + skip
|
||||
; left < right) {
|
||||
p.fillRect(left, top, right - left, line, lineFg);
|
||||
}
|
||||
}, label->lifetime());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
154
Telegram/SourceFiles/ui/boxes/boost_box.h
Normal file
154
Telegram/SourceFiles/ui/boxes/boost_box.h
Normal file
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
void StartFireworks(not_null<QWidget*> parent);
|
||||
|
||||
class Show;
|
||||
class RpWidget;
|
||||
class GenericBox;
|
||||
class VerticalLayout;
|
||||
class FlatLabel;
|
||||
|
||||
struct BoostCounters {
|
||||
int level = 0;
|
||||
int boosts = 0;
|
||||
int thisLevelBoosts = 0;
|
||||
int nextLevelBoosts = 0; // Zero means no next level is available.
|
||||
int mine = 0;
|
||||
|
||||
friend inline constexpr bool operator==(
|
||||
BoostCounters,
|
||||
BoostCounters) = default;
|
||||
};
|
||||
|
||||
struct BoostFeatures {
|
||||
base::flat_map<int, int> nameColorsByLevel;
|
||||
base::flat_map<int, int> linkStylesByLevel;
|
||||
base::flat_map<int, int> profileColorsByLevel;
|
||||
int linkLogoLevel = 0;
|
||||
int profileIconLevel = 0;
|
||||
int autotranslateLevel = 0;
|
||||
int transcribeLevel = 0;
|
||||
int emojiPackLevel = 0;
|
||||
int emojiStatusLevel = 0;
|
||||
int wallpaperLevel = 0;
|
||||
int wallpapersCount = 0;
|
||||
int customWallpaperLevel = 0;
|
||||
int sponsoredLevel = 0;
|
||||
};
|
||||
|
||||
struct BoostBoxData {
|
||||
QString name;
|
||||
BoostCounters boost;
|
||||
BoostFeatures features;
|
||||
int lifting = 0;
|
||||
bool allowMulti = false;
|
||||
bool group = false;
|
||||
};
|
||||
|
||||
void BoostBox(
|
||||
not_null<GenericBox*> box,
|
||||
BoostBoxData data,
|
||||
Fn<void(Fn<void(BoostCounters)>)> boost);
|
||||
|
||||
void BoostBoxAlready(not_null<GenericBox*> box, bool group);
|
||||
void GiftForBoostsBox(
|
||||
not_null<GenericBox*> box,
|
||||
QString channel,
|
||||
int receive,
|
||||
bool again);
|
||||
void GiftedNoBoostsBox(not_null<GenericBox*> box, bool group);
|
||||
void PremiumForBoostsBox(
|
||||
not_null<GenericBox*> box,
|
||||
bool group,
|
||||
Fn<void()> buyPremium);
|
||||
|
||||
struct AskBoostChannelColor {
|
||||
int requiredLevel = 0;
|
||||
};
|
||||
|
||||
struct AskBoostAutotranslate {
|
||||
int requiredLevel = 0;
|
||||
};
|
||||
|
||||
struct AskBoostWallpaper {
|
||||
int requiredLevel = 0;
|
||||
bool group = false;
|
||||
};
|
||||
|
||||
struct AskBoostEmojiStatus {
|
||||
int requiredLevel = 0;
|
||||
bool group = false;
|
||||
};
|
||||
|
||||
struct AskBoostEmojiPack {
|
||||
int requiredLevel = 0;
|
||||
};
|
||||
|
||||
struct AskBoostCustomReactions {
|
||||
int count = 0;
|
||||
};
|
||||
|
||||
struct AskBoostCpm {
|
||||
int requiredLevel = 0;
|
||||
};
|
||||
|
||||
struct AskBoostWearCollectible {
|
||||
int requiredLevel = 0;
|
||||
};
|
||||
|
||||
struct AskBoostReason {
|
||||
std::variant<
|
||||
AskBoostChannelColor,
|
||||
AskBoostAutotranslate,
|
||||
AskBoostWallpaper,
|
||||
AskBoostEmojiStatus,
|
||||
AskBoostEmojiPack,
|
||||
AskBoostCustomReactions,
|
||||
AskBoostCpm,
|
||||
AskBoostWearCollectible> data;
|
||||
};
|
||||
|
||||
struct AskBoostBoxData {
|
||||
QString link;
|
||||
BoostCounters boost;
|
||||
BoostFeatures features;
|
||||
AskBoostReason reason;
|
||||
bool group = false;
|
||||
};
|
||||
|
||||
void AskBoostBox(
|
||||
not_null<GenericBox*> box,
|
||||
AskBoostBoxData data,
|
||||
Fn<void()> openStatistics,
|
||||
Fn<void()> startGiveaway);
|
||||
|
||||
[[nodiscard]] object_ptr<RpWidget> MakeLinkLabel(
|
||||
not_null<QWidget*> parent,
|
||||
rpl::producer<QString> text,
|
||||
rpl::producer<QString> link,
|
||||
std::shared_ptr<Show> show,
|
||||
object_ptr<RpWidget> right);
|
||||
|
||||
void FillBoostLimit(
|
||||
rpl::producer<> showFinished,
|
||||
not_null<VerticalLayout*> container,
|
||||
rpl::producer<BoostCounters> data,
|
||||
style::margins limitLinePadding);
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::FlatLabel> MakeBoostFeaturesBadge(
|
||||
not_null<QWidget*> parent,
|
||||
rpl::producer<QString> text,
|
||||
Fn<QBrush(QRect)> bg);
|
||||
|
||||
} // namespace Ui
|
||||
1349
Telegram/SourceFiles/ui/boxes/calendar_box.cpp
Normal file
1349
Telegram/SourceFiles/ui/boxes/calendar_box.cpp
Normal file
File diff suppressed because it is too large
Load Diff
123
Telegram/SourceFiles/ui/boxes/calendar_box.h
Normal file
123
Telegram/SourceFiles/ui/boxes/calendar_box.h
Normal file
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/layers/box_content.h"
|
||||
#include "ui/widgets/tooltip.h"
|
||||
#include "base/required.h"
|
||||
#include "base/timer.h"
|
||||
|
||||
#include <QtCore/QDate>
|
||||
|
||||
namespace style {
|
||||
struct CalendarSizes;
|
||||
struct CalendarColors;
|
||||
} // namespace style
|
||||
|
||||
namespace st {
|
||||
extern const style::CalendarSizes &defaultCalendarSizes;
|
||||
extern const style::CalendarColors &defaultCalendarColors;
|
||||
} // namespace st
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class IconButton;
|
||||
class ScrollArea;
|
||||
class CalendarBox;
|
||||
|
||||
struct CalendarBoxArgs {
|
||||
template <typename T>
|
||||
using required = base::required<T>;
|
||||
|
||||
required<QDate> month;
|
||||
required<QDate> highlighted;
|
||||
required<Fn<void(QDate date)>> callback;
|
||||
FnMut<void(not_null<CalendarBox*>)> finalize;
|
||||
const style::CalendarSizes &st = st::defaultCalendarSizes;
|
||||
QDate minDate;
|
||||
QDate maxDate;
|
||||
bool allowsSelection = false;
|
||||
Fn<void(
|
||||
not_null<Ui::CalendarBox*>,
|
||||
std::optional<int>)> selectionChanged;
|
||||
const style::CalendarColors &stColors = st::defaultCalendarColors;
|
||||
};
|
||||
|
||||
class CalendarBox final : public BoxContent, private AbstractTooltipShower {
|
||||
public:
|
||||
CalendarBox(QWidget*, CalendarBoxArgs &&args);
|
||||
~CalendarBox();
|
||||
|
||||
void toggleSelectionMode(bool enabled);
|
||||
|
||||
[[nodiscard]] QDate selectedFirstDate() const;
|
||||
[[nodiscard]] QDate selectedLastDate() const;
|
||||
|
||||
protected:
|
||||
void prepare() override;
|
||||
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
private:
|
||||
void monthChanged(QDate month);
|
||||
|
||||
bool isPreviousEnabled() const;
|
||||
bool isNextEnabled() const;
|
||||
|
||||
void goPreviousMonth();
|
||||
void goNextMonth();
|
||||
void setExactScroll();
|
||||
void processScroll();
|
||||
void createButtons();
|
||||
|
||||
void showJumpTooltip(not_null<IconButton*> button);
|
||||
void jumpAfterDelay(not_null<IconButton*> button);
|
||||
void jump(QPointer<IconButton> button);
|
||||
|
||||
QString tooltipText() const override;
|
||||
QPoint tooltipPos() const override;
|
||||
bool tooltipWindowActive() const override;
|
||||
|
||||
const style::CalendarSizes &_st;
|
||||
const style::CalendarColors &_styleColors;
|
||||
|
||||
class Context;
|
||||
std::unique_ptr<Context> _context;
|
||||
|
||||
std::unique_ptr<ScrollArea> _scroll;
|
||||
|
||||
class Inner;
|
||||
not_null<Inner*> _inner;
|
||||
|
||||
class FloatingDate;
|
||||
std::unique_ptr<FloatingDate> _floatingDate;
|
||||
|
||||
class Title;
|
||||
object_ptr<Title> _title;
|
||||
object_ptr<IconButton> _previous;
|
||||
object_ptr<IconButton> _next;
|
||||
bool _previousEnabled = false;
|
||||
bool _nextEnabled = false;
|
||||
|
||||
Fn<void(QDate date)> _callback;
|
||||
FnMut<void(not_null<CalendarBox*>)> _finalize;
|
||||
bool _watchScroll = false;
|
||||
|
||||
QPointer<IconButton> _tooltipButton;
|
||||
QPointer<IconButton> _jumpButton;
|
||||
base::Timer _jumpTimer;
|
||||
|
||||
bool _selectionMode = false;
|
||||
Fn<void(
|
||||
not_null<Ui::CalendarBox*>,
|
||||
std::optional<int>)> _selectionChanged;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
341
Telegram/SourceFiles/ui/boxes/choose_date_time.cpp
Normal file
341
Telegram/SourceFiles/ui/boxes/choose_date_time.cpp
Normal file
@@ -0,0 +1,341 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/boxes/choose_date_time.h"
|
||||
|
||||
#include "base/unixtime.h"
|
||||
#include "base/event_filter.h"
|
||||
#include "ui/boxes/calendar_box.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/widgets/time_input.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_boxes.h"
|
||||
|
||||
#include <QtWidgets/QTextEdit>
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMinimalSchedule = TimeId(10);
|
||||
|
||||
QString DayString(const QDate &date) {
|
||||
return tr::lng_month_day(
|
||||
tr::now,
|
||||
lt_month,
|
||||
Lang::MonthDay(date.month())(tr::now),
|
||||
lt_day,
|
||||
QString::number(date.day()));
|
||||
}
|
||||
|
||||
QString TimeString(QTime time) {
|
||||
return QString("%1:%2"
|
||||
).arg(time.hour()
|
||||
).arg(time.minute(), 2, 10, QLatin1Char('0'));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ChooseDateTimeStyleArgs::ChooseDateTimeStyleArgs()
|
||||
: labelStyle(&st::boxLabel)
|
||||
, dateFieldStyle(&st::scheduleDateField)
|
||||
, timeFieldStyle(&st::scheduleTimeField)
|
||||
, separatorStyle(&st::scheduleTimeSeparator)
|
||||
, atStyle(&st::scheduleAtLabel)
|
||||
, calendarStyle(&st::defaultCalendarColors) {
|
||||
}
|
||||
|
||||
ChooseDateTimeBoxDescriptor ChooseDateTimeBox(
|
||||
not_null<GenericBox*> box,
|
||||
ChooseDateTimeBoxArgs &&args) {
|
||||
struct State {
|
||||
rpl::variable<QDate> date;
|
||||
rpl::variable<int> width;
|
||||
not_null<InputField*> day;
|
||||
not_null<TimeInput*> time;
|
||||
not_null<FlatLabel*> at;
|
||||
};
|
||||
box->setTitle(std::move(args.title));
|
||||
box->setWidth(st::boxWideWidth);
|
||||
|
||||
const auto content = box->addRow(
|
||||
object_ptr<FixedHeightWidget>(box, st::scheduleHeight),
|
||||
style::al_top);
|
||||
if (args.description) {
|
||||
box->addRow(object_ptr<FlatLabel>(
|
||||
box,
|
||||
std::move(args.description),
|
||||
*args.style.labelStyle));
|
||||
}
|
||||
const auto parsed = base::unixtime::parse(args.time);
|
||||
const auto state = box->lifetime().make_state<State>(State{
|
||||
.date = parsed.date(),
|
||||
.day = CreateChild<InputField>(
|
||||
content,
|
||||
*args.style.dateFieldStyle),
|
||||
.time = CreateChild<TimeInput>(
|
||||
content,
|
||||
TimeString(parsed.time()),
|
||||
*args.style.timeFieldStyle,
|
||||
*args.style.dateFieldStyle,
|
||||
*args.style.separatorStyle,
|
||||
st::scheduleTimeSeparatorPadding),
|
||||
.at = CreateChild<FlatLabel>(
|
||||
content,
|
||||
tr::lng_schedule_at(),
|
||||
*args.style.atStyle),
|
||||
});
|
||||
|
||||
state->date.value(
|
||||
) | rpl::on_next([=](QDate date) {
|
||||
state->day->setText(DayString(date));
|
||||
state->time->setFocusFast();
|
||||
}, state->day->lifetime());
|
||||
|
||||
const auto min = args.min ? args.min : [] {
|
||||
return base::unixtime::now() + kMinimalSchedule;
|
||||
};
|
||||
const auto max = args.max ? args.max : [] {
|
||||
return base::unixtime::serialize(
|
||||
QDateTime::currentDateTime().addYears(1)) - 1;
|
||||
};
|
||||
const auto minDate = [=] {
|
||||
return base::unixtime::parse(min()).date();
|
||||
};
|
||||
const auto maxDate = [=] {
|
||||
return base::unixtime::parse(max()).date();
|
||||
};
|
||||
|
||||
const auto &dayViewport = state->day->rawTextEdit()->viewport();
|
||||
base::install_event_filter(dayViewport, [=](not_null<QEvent*> event) {
|
||||
if (event->type() == QEvent::Wheel) {
|
||||
const auto e = static_cast<QWheelEvent*>(event.get());
|
||||
const auto direction = Ui::WheelDirection(e);
|
||||
if (!direction) {
|
||||
return base::EventFilterResult::Continue;
|
||||
}
|
||||
const auto d = state->date.current().addDays(direction);
|
||||
state->date = std::clamp(d, minDate(), maxDate());
|
||||
return base::EventFilterResult::Cancel;
|
||||
}
|
||||
return base::EventFilterResult::Continue;
|
||||
});
|
||||
|
||||
state->at->widthValue() | rpl::on_next([=](int width) {
|
||||
const auto full = st::scheduleDateWidth
|
||||
+ st::scheduleAtSkip
|
||||
+ width
|
||||
+ st::scheduleAtSkip
|
||||
+ st::scheduleTimeWidth;
|
||||
content->setNaturalWidth(full);
|
||||
state->width = full;
|
||||
}, state->at->lifetime());
|
||||
|
||||
content->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
const auto paddings = width
|
||||
- state->at->width()
|
||||
- 2 * st::scheduleAtSkip
|
||||
- st::scheduleDateWidth
|
||||
- st::scheduleTimeWidth;
|
||||
const auto left = paddings / 2;
|
||||
state->day->resizeToWidth(st::scheduleDateWidth);
|
||||
state->day->moveToLeft(left, st::scheduleDateTop, width);
|
||||
state->at->moveToLeft(
|
||||
left + st::scheduleDateWidth + st::scheduleAtSkip,
|
||||
st::scheduleAtTop,
|
||||
width);
|
||||
state->time->resizeToWidth(st::scheduleTimeWidth);
|
||||
state->time->moveToLeft(
|
||||
width - left - st::scheduleTimeWidth,
|
||||
st::scheduleDateTop,
|
||||
width);
|
||||
}, content->lifetime());
|
||||
|
||||
const auto calendar
|
||||
= content->lifetime().make_state<base::weak_qptr<CalendarBox>>();
|
||||
const auto calendarStyle = args.style.calendarStyle;
|
||||
state->day->focusedChanges(
|
||||
) | rpl::on_next([=](bool focused) {
|
||||
if (*calendar || !focused) {
|
||||
return;
|
||||
}
|
||||
*calendar = box->getDelegate()->show(
|
||||
Box<CalendarBox>(Ui::CalendarBoxArgs{
|
||||
.month = state->date.current(),
|
||||
.highlighted = state->date.current(),
|
||||
.callback = crl::guard(box, [=](QDate chosen) {
|
||||
state->date = chosen;
|
||||
(*calendar)->closeBox();
|
||||
}),
|
||||
.minDate = minDate(),
|
||||
.maxDate = maxDate(),
|
||||
.stColors = *calendarStyle,
|
||||
}));
|
||||
(*calendar)->boxClosing(
|
||||
) | rpl::on_next(crl::guard(state->time, [=] {
|
||||
state->time->setFocusFast();
|
||||
}), (*calendar)->lifetime());
|
||||
}, state->day->lifetime());
|
||||
|
||||
const auto collect = [=] {
|
||||
const auto timeValue = state->time->valueCurrent().split(':');
|
||||
if (timeValue.size() != 2) {
|
||||
return 0;
|
||||
}
|
||||
const auto time = QTime(timeValue[0].toInt(), timeValue[1].toInt());
|
||||
if (!time.isValid()) {
|
||||
return 0;
|
||||
}
|
||||
const auto result = base::unixtime::serialize(
|
||||
QDateTime(state->date.current(), time));
|
||||
if (result < min() || result > max()) {
|
||||
return 0;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const auto save = [=, done = args.done] {
|
||||
if (const auto result = collect()) {
|
||||
done(result);
|
||||
} else {
|
||||
state->time->showError();
|
||||
}
|
||||
};
|
||||
state->time->submitRequests(
|
||||
) | rpl::on_next(save, state->time->lifetime());
|
||||
|
||||
auto result = ChooseDateTimeBoxDescriptor();
|
||||
box->setFocusCallback([=] { state->time->setFocusFast(); });
|
||||
result.width = state->width.value();
|
||||
result.submit = box->addButton(std::move(args.submit), save);
|
||||
result.collect = [=] {
|
||||
if (const auto result = collect()) {
|
||||
return result;
|
||||
}
|
||||
state->time->showError();
|
||||
return 0;
|
||||
};
|
||||
result.values = rpl::combine(
|
||||
state->date.value(),
|
||||
state->time->value()
|
||||
) | rpl::map(collect);
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
object_ptr<Ui::RpWidget> ChooseRepeatPeriod(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
ChooseRepeatPeriodArgs &&args) {
|
||||
auto result = object_ptr<Ui::RpWidget>(parent.get());
|
||||
const auto raw = result.data();
|
||||
|
||||
struct Entry {
|
||||
TimeId value = 0;
|
||||
QString text;
|
||||
};
|
||||
auto map = std::vector<Entry>{
|
||||
{ 0, tr::lng_schedule_repeat_never(tr::now) },
|
||||
{ 24 * 60 * 60, tr::lng_schedule_repeat_daily(tr::now) },
|
||||
{ 7 * 24 * 60 * 60, tr::lng_schedule_repeat_weekly(tr::now) },
|
||||
{ 14 * 24 * 60 * 60, tr::lng_schedule_repeat_biweekly(tr::now) },
|
||||
{ 30 * 24 * 60 * 60, tr::lng_schedule_repeat_monthly(tr::now) },
|
||||
{
|
||||
91 * 24 * 60 * 60,
|
||||
tr::lng_schedule_repeat_every_month(tr::now, lt_count, 3)
|
||||
},
|
||||
{
|
||||
182 * 24 * 60 * 60,
|
||||
tr::lng_schedule_repeat_every_month(tr::now, lt_count, 6)
|
||||
},
|
||||
{ 365 * 24 * 60 * 60, tr::lng_schedule_repeat_yearly(tr::now) },
|
||||
};
|
||||
if (args.test) {
|
||||
map.insert(begin(map) + 1, Entry{ 300, u"Every 5 minutes"_q });
|
||||
map.insert(begin(map) + 1, Entry{ 60, u"Every minute"_q });
|
||||
}
|
||||
|
||||
const auto label = Ui::CreateChild<Ui::FlatLabel>(raw, QString());
|
||||
rpl::combine(
|
||||
raw->widthValue(),
|
||||
label->naturalWidthValue()
|
||||
) | rpl::on_next([=](int outer, int natural) {
|
||||
label->resizeToWidth(std::min(outer, natural));
|
||||
}, raw->lifetime());
|
||||
label->heightValue() | rpl::on_next([=](int height) {
|
||||
raw->resize(raw->width(), height);
|
||||
}, label->lifetime());
|
||||
|
||||
struct State {
|
||||
rpl::variable<TimeId> value;
|
||||
rpl::variable<bool> locked;
|
||||
std::unique_ptr<Ui::PopupMenu> menu;
|
||||
};
|
||||
const auto state = raw->lifetime().make_state<State>(State{
|
||||
.value = args.value,
|
||||
.locked = std::move(args.locked),
|
||||
});
|
||||
|
||||
rpl::combine(
|
||||
state->value.value(),
|
||||
state->locked.value()
|
||||
) | rpl::on_next([=](TimeId value, bool locked) {
|
||||
auto result = tr::lng_schedule_repeat_label(
|
||||
tr::now,
|
||||
tr::marked);
|
||||
|
||||
const auto text = [&] {
|
||||
const auto i = ranges::lower_bound(
|
||||
map,
|
||||
value,
|
||||
ranges::less{},
|
||||
&Entry::value);
|
||||
return (i != end(map)) ? i->text : map.back().text;
|
||||
}();
|
||||
|
||||
label->setMarkedText(result.append(' ').append(tr::link(
|
||||
tr::bold(text).append(
|
||||
Ui::Text::IconEmoji(locked
|
||||
? &st::scheduleRepeatDropdownLock
|
||||
: &st::scheduleRepeatDropdownArrow))
|
||||
)));
|
||||
return result;
|
||||
}, label->lifetime());
|
||||
|
||||
label->setClickHandlerFilter([=](const auto &...) {
|
||||
if (args.filter && args.filter()) {
|
||||
return false;
|
||||
}
|
||||
const auto changed = args.changed;
|
||||
|
||||
state->menu = std::make_unique<Ui::PopupMenu>(label);
|
||||
const auto menu = state->menu.get();
|
||||
|
||||
menu->setDestroyedCallback(crl::guard(label, [=] {
|
||||
if (state->menu.get() == menu) {
|
||||
state->menu.release();
|
||||
}
|
||||
}));
|
||||
for (const auto &entry : map) {
|
||||
const auto value = entry.value;
|
||||
menu->addAction(entry.text, [=] {
|
||||
state->value = value;
|
||||
changed(value);
|
||||
});
|
||||
}
|
||||
menu->popup(QCursor::pos());
|
||||
return false;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
66
Telegram/SourceFiles/ui/boxes/choose_date_time.h
Normal file
66
Telegram/SourceFiles/ui/boxes/choose_date_time.h
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/layers/generic_box.h"
|
||||
|
||||
namespace style {
|
||||
struct FlatLabel;
|
||||
struct InputField;
|
||||
struct CalendarColors;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class RoundButton;
|
||||
|
||||
struct ChooseDateTimeBoxDescriptor {
|
||||
QPointer<RoundButton> submit;
|
||||
Fn<TimeId()> collect;
|
||||
rpl::producer<TimeId> values;
|
||||
rpl::producer<int> width;
|
||||
};
|
||||
|
||||
struct ChooseDateTimeStyleArgs {
|
||||
ChooseDateTimeStyleArgs();
|
||||
const style::FlatLabel *labelStyle;
|
||||
const style::InputField *dateFieldStyle;
|
||||
const style::InputField *timeFieldStyle;
|
||||
const style::FlatLabel *separatorStyle;
|
||||
const style::FlatLabel *atStyle;
|
||||
const style::CalendarColors *calendarStyle;
|
||||
};
|
||||
|
||||
struct ChooseDateTimeBoxArgs {
|
||||
rpl::producer<QString> title;
|
||||
rpl::producer<QString> submit;
|
||||
Fn<void(TimeId)> done;
|
||||
Fn<TimeId()> min;
|
||||
TimeId time = 0;
|
||||
Fn<TimeId()> max;
|
||||
rpl::producer<QString> description;
|
||||
ChooseDateTimeStyleArgs style;
|
||||
};
|
||||
|
||||
ChooseDateTimeBoxDescriptor ChooseDateTimeBox(
|
||||
not_null<GenericBox*> box,
|
||||
ChooseDateTimeBoxArgs &&args);
|
||||
|
||||
struct ChooseRepeatPeriodArgs {
|
||||
TimeId value = 0;
|
||||
rpl::variable<bool> locked;
|
||||
Fn<bool()> filter;
|
||||
Fn<void(TimeId)> changed;
|
||||
bool test = false;
|
||||
};
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> ChooseRepeatPeriod(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
ChooseRepeatPeriodArgs &&args);
|
||||
|
||||
} // namespace Ui
|
||||
967
Telegram/SourceFiles/ui/boxes/choose_font_box.cpp
Normal file
967
Telegram/SourceFiles/ui/boxes/choose_font_box.cpp
Normal file
@@ -0,0 +1,967 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/boxes/choose_font_box.h"
|
||||
|
||||
#include "base/event_filter.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/chat/chat_style.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/style/style_core_font.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/widgets/multi_select.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/cached_round_corners.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_settings.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_window.h"
|
||||
|
||||
#include <QtGui/QFontDatabase>
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMinTextWidth = 120;
|
||||
constexpr auto kMaxTextWidth = 320;
|
||||
constexpr auto kMaxTextLines = 3;
|
||||
|
||||
struct PreviewRequest {
|
||||
QString family;
|
||||
QColor msgBg;
|
||||
QColor msgShadow;
|
||||
QColor replyBar;
|
||||
QColor replyNameFg;
|
||||
QColor textFg;
|
||||
QImage bubbleTail;
|
||||
};
|
||||
|
||||
class PreviewPainter {
|
||||
public:
|
||||
PreviewPainter(const QImage &bg, PreviewRequest request);
|
||||
|
||||
QImage takeResult();
|
||||
|
||||
private:
|
||||
void layout();
|
||||
|
||||
void paintBubble(Painter &p);
|
||||
void paintContent(Painter &p);
|
||||
void paintReply(Painter &p);
|
||||
void paintMessage(Painter &p);
|
||||
|
||||
void validateBubbleCache();
|
||||
|
||||
const PreviewRequest _request;
|
||||
const style::owned_color _msgBg;
|
||||
const style::owned_color _msgShadow;
|
||||
|
||||
style::owned_font _nameFontOwned;
|
||||
style::font _nameFont;
|
||||
style::TextStyle _nameStyle;
|
||||
style::owned_font _textFontOwned;
|
||||
style::font _textFont;
|
||||
style::TextStyle _textStyle;
|
||||
|
||||
Ui::Text::String _nameText;
|
||||
Ui::Text::String _replyText;
|
||||
Ui::Text::String _messageText;
|
||||
|
||||
QRect _replyRect;
|
||||
QRect _name;
|
||||
QRect _reply;
|
||||
QRect _message;
|
||||
QRect _content;
|
||||
QRect _bubble;
|
||||
QSize _outer;
|
||||
|
||||
Ui::CornersPixmaps _bubbleCorners;
|
||||
QPixmap _bubbleShadowBottomRight;
|
||||
|
||||
QImage _result;
|
||||
|
||||
};
|
||||
|
||||
class Selector final : public Ui::RpWidget {
|
||||
public:
|
||||
Selector(
|
||||
not_null<QWidget*> parent,
|
||||
const QString &now,
|
||||
rpl::producer<QString> filter,
|
||||
rpl::producer<> submits,
|
||||
Fn<void(QString)> chosen,
|
||||
Fn<void(Ui::ScrollToRequest, anim::type)> scrollTo);
|
||||
|
||||
void initScroll(anim::type animated);
|
||||
void setMinHeight(int height);
|
||||
void selectSkip(Qt::Key direction);
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
QString id;
|
||||
QString key;
|
||||
QString text;
|
||||
QStringList keywords;
|
||||
QImage cache;
|
||||
std::unique_ptr<Ui::RadioView> check;
|
||||
std::unique_ptr<Ui::RippleAnimation> ripple;
|
||||
int paletteVersion = 0;
|
||||
};
|
||||
[[nodiscard]] static std::vector<Entry> FullList(const QString &now);
|
||||
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void leaveEventHook(QEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
|
||||
[[nodiscard]] bool searching() const;
|
||||
[[nodiscard]] int shownRowsCount() const;
|
||||
[[nodiscard]] Entry &shownRowAt(int index);
|
||||
|
||||
void applyFilter(const QString &query);
|
||||
void updateSelected(int selected);
|
||||
void updatePressed(int pressed);
|
||||
void updateRow(int index);
|
||||
void updateRow(not_null<Entry*> row, int hint);
|
||||
void addRipple(int index, QPoint position);
|
||||
void validateCache(Entry &row);
|
||||
void choose(Entry &row);
|
||||
|
||||
const style::SettingsButton &_st;
|
||||
std::vector<Entry> _rows;
|
||||
std::vector<not_null<Entry*>> _filtered;
|
||||
QString _chosen;
|
||||
int _selected = -1;
|
||||
int _pressed = -1;
|
||||
|
||||
std::optional<QPoint> _lastGlobalPoint;
|
||||
bool _selectedByKeyboard = false;
|
||||
|
||||
Fn<void(QString)> _callback;
|
||||
Fn<void(Ui::ScrollToRequest, anim::type)> _scrollTo;
|
||||
|
||||
int _rowsSkip = 0;
|
||||
int _rowHeight = 0;
|
||||
int _minHeight = 0;
|
||||
|
||||
QString _query;
|
||||
QStringList _queryWords;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
Selector::Selector(
|
||||
not_null<QWidget*> parent,
|
||||
const QString &now,
|
||||
rpl::producer<QString> filter,
|
||||
rpl::producer<> submits,
|
||||
Fn<void(QString)> chosen,
|
||||
Fn<void(Ui::ScrollToRequest, anim::type)> scrollTo)
|
||||
: RpWidget(parent)
|
||||
, _st(st::settingsButton)
|
||||
, _rows(FullList(now))
|
||||
, _chosen(now)
|
||||
, _callback(std::move(chosen))
|
||||
, _scrollTo(std::move(scrollTo))
|
||||
, _rowsSkip(st::settingsInfoPhotoSkip)
|
||||
, _rowHeight(_st.height + _st.padding.top() + _st.padding.bottom()) {
|
||||
setMouseTracking(true);
|
||||
|
||||
std::move(filter) | rpl::on_next([=](const QString &query) {
|
||||
applyFilter(query);
|
||||
}, _lifetime);
|
||||
|
||||
std::move(
|
||||
submits
|
||||
) | rpl::on_next([=] {
|
||||
if (_selected >= 0) {
|
||||
choose(shownRowAt(_selected));
|
||||
} else if (searching() && !_filtered.empty()) {
|
||||
choose(*_filtered.front());
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
void Selector::applyFilter(const QString &query) {
|
||||
if (_query == query) {
|
||||
return;
|
||||
}
|
||||
_query = query;
|
||||
|
||||
updateSelected(-1);
|
||||
updatePressed(-1);
|
||||
|
||||
_queryWords = TextUtilities::PrepareSearchWords(_query);
|
||||
|
||||
const auto skip = [](
|
||||
const QStringList &haystack,
|
||||
const QStringList &needles) {
|
||||
const auto find = [](
|
||||
const QStringList &haystack,
|
||||
const QString &needle) {
|
||||
for (const auto &item : haystack) {
|
||||
if (item.startsWith(needle)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
for (const auto &needle : needles) {
|
||||
if (!find(haystack, needle)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
_filtered.clear();
|
||||
if (!_queryWords.isEmpty()) {
|
||||
_filtered.reserve(_rows.size());
|
||||
for (auto &row : _rows) {
|
||||
if (!skip(row.keywords, _queryWords)) {
|
||||
_filtered.push_back(&row);
|
||||
} else {
|
||||
row.ripple = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resizeToWidth(width());
|
||||
Ui::SendPendingMoveResizeEvents(this);
|
||||
update();
|
||||
}
|
||||
|
||||
void Selector::updateSelected(int selected) {
|
||||
if (_selected == selected) {
|
||||
return;
|
||||
}
|
||||
const auto was = (_selected >= 0);
|
||||
updateRow(_selected);
|
||||
_selected = selected;
|
||||
updateRow(_selected);
|
||||
const auto now = (_selected >= 0);
|
||||
if (was != now) {
|
||||
setCursor(now ? style::cur_pointer : style::cur_default);
|
||||
}
|
||||
if (_selectedByKeyboard) {
|
||||
const auto top = (_selected > 0)
|
||||
? (_rowsSkip + _selected * _rowHeight)
|
||||
: 0;
|
||||
const auto bottom = (_selected > 0)
|
||||
? (top + _rowHeight)
|
||||
: _selected
|
||||
? 0
|
||||
: _rowHeight;
|
||||
_scrollTo({ top, bottom }, anim::type::instant);
|
||||
}
|
||||
}
|
||||
|
||||
void Selector::updatePressed(int pressed) {
|
||||
if (_pressed == pressed) {
|
||||
return;
|
||||
} else if (_pressed >= 0) {
|
||||
if (auto &ripple = shownRowAt(_pressed).ripple) {
|
||||
ripple->lastStop();
|
||||
}
|
||||
}
|
||||
updateRow(_pressed);
|
||||
_pressed = pressed;
|
||||
updateRow(_pressed);
|
||||
}
|
||||
|
||||
void Selector::updateRow(int index) {
|
||||
if (index >= 0) {
|
||||
update(0, _rowsSkip + index * _rowHeight, width(), _rowHeight);
|
||||
}
|
||||
}
|
||||
|
||||
void Selector::updateRow(not_null<Entry*> row, int hint) {
|
||||
if (hint >= 0 && hint < shownRowsCount() && &shownRowAt(hint) == row) {
|
||||
updateRow(hint);
|
||||
} else if (searching()) {
|
||||
const auto i = ranges::find(_filtered, row);
|
||||
if (i != end(_filtered)) {
|
||||
updateRow(int(i - begin(_filtered)));
|
||||
}
|
||||
} else {
|
||||
const auto index = int(row.get() - &_rows[0]);
|
||||
Assert(index >= 0 && index < _rows.size());
|
||||
updateRow(index);
|
||||
}
|
||||
}
|
||||
|
||||
void Selector::validateCache(Entry &row) {
|
||||
const auto version = style::PaletteVersion();
|
||||
if (row.cache.isNull()) {
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
row.cache = QImage(
|
||||
QSize(width(), _rowHeight) * ratio,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
row.cache.setDevicePixelRatio(ratio);
|
||||
} else if (row.paletteVersion == version) {
|
||||
return;
|
||||
}
|
||||
row.paletteVersion = version;
|
||||
row.cache.fill(Qt::transparent);
|
||||
auto owned = style::owned_font(row.id, 0, st::boxFontSize);
|
||||
const auto font = owned.font();
|
||||
auto p = QPainter(&row.cache);
|
||||
p.setFont(font);
|
||||
p.setPen(st::windowFg);
|
||||
|
||||
const auto textw = width() - _st.padding.left() - _st.padding.right();
|
||||
const auto textt = (_rowHeight - font->height) / 2.;
|
||||
p.drawText(
|
||||
_st.padding.left(),
|
||||
textt + font->ascent,
|
||||
font->elided(row.text, textw));
|
||||
}
|
||||
|
||||
bool Selector::searching() const {
|
||||
return !_queryWords.isEmpty();
|
||||
}
|
||||
|
||||
int Selector::shownRowsCount() const {
|
||||
return searching() ? int(_filtered.size()) : int(_rows.size());
|
||||
}
|
||||
|
||||
Selector::Entry &Selector::shownRowAt(int index) {
|
||||
return searching() ? *_filtered[index] : _rows[index];
|
||||
}
|
||||
|
||||
void Selector::setMinHeight(int height) {
|
||||
_minHeight = height;
|
||||
if (_minHeight > 0) {
|
||||
resizeToWidth(width());
|
||||
}
|
||||
}
|
||||
|
||||
void Selector::selectSkip(Qt::Key key) {
|
||||
const auto count = shownRowsCount();
|
||||
if (key == Qt::Key_Down) {
|
||||
if (_selected + 1 < count) {
|
||||
_selectedByKeyboard = true;
|
||||
updateSelected(_selected + 1);
|
||||
}
|
||||
} else if (key == Qt::Key_Up) {
|
||||
if (_selected >= 0) {
|
||||
_selectedByKeyboard = true;
|
||||
updateSelected(_selected - 1);
|
||||
}
|
||||
} else if (key == Qt::Key_PageDown) {
|
||||
const auto change = _minHeight / _rowHeight;
|
||||
if (_selected + 1 < count) {
|
||||
_selectedByKeyboard = true;
|
||||
updateSelected(std::min(_selected + change, count - 1));
|
||||
}
|
||||
} else if (key == Qt::Key_PageUp) {
|
||||
const auto change = _minHeight / _rowHeight;
|
||||
if (_selected > 0) {
|
||||
_selectedByKeyboard = true;
|
||||
updateSelected(std::max(_selected - change, 0));
|
||||
} else if (!_selected) {
|
||||
_selectedByKeyboard = true;
|
||||
updateSelected(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Selector::initScroll(anim::type animated) {
|
||||
const auto index = [&] {
|
||||
if (searching()) {
|
||||
const auto i = ranges::find(_filtered, _chosen, &Entry::id);
|
||||
if (i != end(_filtered)) {
|
||||
return int(i - begin(_filtered));
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
const auto i = ranges::find(_rows, _chosen, &Entry::id);
|
||||
Assert(i != end(_rows));
|
||||
return int(i - begin(_rows));
|
||||
}();
|
||||
if (index >= 0) {
|
||||
const auto top = _rowsSkip + index * _rowHeight;
|
||||
const auto use = std::max(top - (_minHeight - _rowHeight) / 2, 0);
|
||||
_scrollTo({ use, use + _minHeight }, animated);
|
||||
}
|
||||
}
|
||||
|
||||
int Selector::resizeGetHeight(int newWidth) {
|
||||
const auto added = 2 * _rowsSkip;
|
||||
return std::max(added + shownRowsCount() * _rowHeight, _minHeight);
|
||||
}
|
||||
|
||||
void Selector::paintEvent(QPaintEvent *e) {
|
||||
auto p = QPainter(this);
|
||||
|
||||
const auto rows = shownRowsCount();
|
||||
if (!rows) {
|
||||
p.setFont(st::normalFont);
|
||||
p.setPen(st::windowSubTextFg);
|
||||
p.drawText(
|
||||
QRect(0, 0, width(), height() * 2 / 3),
|
||||
tr::lng_font_not_found(tr::now),
|
||||
style::al_center);
|
||||
return;
|
||||
}
|
||||
const auto clip = e->rect();
|
||||
const auto clipped = std::max(clip.y() - _rowsSkip, 0);
|
||||
const auto from = std::min(clipped / _rowHeight, rows);
|
||||
const auto till = std::min(
|
||||
(clip.y() + clip.height() - _rowsSkip + _rowHeight - 1) / _rowHeight,
|
||||
rows);
|
||||
const auto active = (_pressed >= 0) ? _pressed : _selected;
|
||||
for (auto i = from; i != till; ++i) {
|
||||
auto &row = shownRowAt(i);
|
||||
const auto y = _rowsSkip + i * _rowHeight;
|
||||
const auto bg = (i == active) ? st::windowBgOver : st::windowBg;
|
||||
const auto rect = QRect(0, y, width(), _rowHeight);
|
||||
p.fillRect(rect, bg);
|
||||
|
||||
if (row.ripple) {
|
||||
row.ripple->paint(p, 0, y, width());
|
||||
if (row.ripple->empty()) {
|
||||
row.ripple = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
validateCache(row);
|
||||
p.drawImage(0, y, row.cache);
|
||||
|
||||
if (!row.check) {
|
||||
row.check = std::make_unique<Ui::RadioView>(
|
||||
st::langsRadio,
|
||||
(row.id == _chosen),
|
||||
[=, row = &row] { updateRow(row, i); });
|
||||
}
|
||||
row.check->paint(
|
||||
p,
|
||||
_st.iconLeft,
|
||||
y + (_rowHeight - st::langsRadio.diameter) / 2,
|
||||
width());
|
||||
}
|
||||
}
|
||||
|
||||
void Selector::leaveEventHook(QEvent *e) {
|
||||
_lastGlobalPoint = std::nullopt;
|
||||
if (!_selectedByKeyboard) {
|
||||
updateSelected(-1);
|
||||
}
|
||||
}
|
||||
|
||||
void Selector::mouseMoveEvent(QMouseEvent *e) {
|
||||
if (!_lastGlobalPoint) {
|
||||
_lastGlobalPoint = e->globalPos();
|
||||
if (_selectedByKeyboard) {
|
||||
return;
|
||||
}
|
||||
} else if (*_lastGlobalPoint == e->globalPos() && _selectedByKeyboard) {
|
||||
return;
|
||||
} else {
|
||||
_lastGlobalPoint = e->globalPos();
|
||||
}
|
||||
_selectedByKeyboard = false;
|
||||
const auto y = e->y() - _rowsSkip;
|
||||
const auto index = (y >= 0) ? (y / _rowHeight) : -1;
|
||||
updateSelected((index >= 0 && index < shownRowsCount()) ? index : -1);
|
||||
}
|
||||
|
||||
void Selector::mousePressEvent(QMouseEvent *e) {
|
||||
updatePressed(_selected);
|
||||
if (_pressed >= 0) {
|
||||
addRipple(_pressed, e->pos());
|
||||
}
|
||||
}
|
||||
|
||||
void Selector::mouseReleaseEvent(QMouseEvent *e) {
|
||||
const auto pressed = _pressed;
|
||||
updatePressed(-1);
|
||||
if (pressed >= 0 && pressed == _selected) {
|
||||
choose(shownRowAt(pressed));
|
||||
}
|
||||
}
|
||||
|
||||
void Selector::choose(Entry &row) {
|
||||
const auto id = row.id;
|
||||
if (_chosen != id) {
|
||||
const auto i = ranges::find(_rows, _chosen, &Entry::id);
|
||||
Assert(i != end(_rows));
|
||||
if (i->check) {
|
||||
i->check->setChecked(false, anim::type::normal);
|
||||
}
|
||||
_chosen = id;
|
||||
if (row.check) {
|
||||
row.check->setChecked(true, anim::type::normal);
|
||||
}
|
||||
}
|
||||
const auto animated = searching()
|
||||
? anim::type::instant
|
||||
: anim::type::normal;
|
||||
_callback(id);
|
||||
initScroll(animated);
|
||||
}
|
||||
|
||||
void Selector::addRipple(int index, QPoint position) {
|
||||
Expects(index >= 0 && index < shownRowsCount());
|
||||
|
||||
const auto row = &shownRowAt(index);
|
||||
if (!row->ripple) {
|
||||
row->ripple = std::make_unique<Ui::RippleAnimation>(
|
||||
st::defaultRippleAnimation,
|
||||
Ui::RippleAnimation::RectMask({ width(), _rowHeight }),
|
||||
[=] { updateRow(row, index); });
|
||||
}
|
||||
row->ripple->add(position - QPoint(0, _rowsSkip + index * _rowHeight));
|
||||
}
|
||||
|
||||
std::vector<Selector::Entry> Selector::FullList(const QString &now) {
|
||||
using namespace TextUtilities;
|
||||
|
||||
auto database = QFontDatabase();
|
||||
auto families = database.families();
|
||||
auto result = std::vector<Entry>();
|
||||
result.reserve(families.size() + 3);
|
||||
const auto add = [&](const QString &text, const QString &id = {}) {
|
||||
result.push_back({
|
||||
.id = id,
|
||||
.text = text,
|
||||
.keywords = PrepareSearchWords(text),
|
||||
});
|
||||
};
|
||||
add(tr::lng_font_default(tr::now));
|
||||
add(tr::lng_font_system(tr::now), style::SystemFontTag());
|
||||
for (const auto &family : families) {
|
||||
if (database.isScalable(family)) {
|
||||
result.push_back({ .id = family });
|
||||
}
|
||||
}
|
||||
auto nowIt = ranges::find(result, now, &Entry::id);
|
||||
if (nowIt == end(result)) {
|
||||
result.push_back({ .id = now });
|
||||
nowIt = end(result) - 1;
|
||||
}
|
||||
for (auto i = begin(result) + 2; i != end(result); ++i) {
|
||||
i->key = TextUtilities::RemoveAccents(i->id).toLower();
|
||||
i->text = i->id;
|
||||
i->keywords = TextUtilities::PrepareSearchWords(i->id);
|
||||
}
|
||||
auto skip = 2;
|
||||
if (nowIt - begin(result) >= skip) {
|
||||
std::swap(result[2], *nowIt);
|
||||
++skip;
|
||||
}
|
||||
ranges::sort(
|
||||
begin(result) + skip, end(result),
|
||||
std::less<>(),
|
||||
&Entry::key);
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] PreviewRequest PrepareRequest(const QString &family) {
|
||||
return {
|
||||
.family = family,
|
||||
.msgBg = st::msgInBg->c,
|
||||
.msgShadow = st::msgInShadow->c,
|
||||
.replyBar = st::msgInReplyBarColor->c,
|
||||
.replyNameFg = st::msgInServiceFg->c,
|
||||
.textFg = st::historyTextInFg->c,
|
||||
.bubbleTail = st::historyBubbleTailInLeft.instance(st::msgInBg->c),
|
||||
};
|
||||
}
|
||||
|
||||
PreviewPainter::PreviewPainter(const QImage &bg, PreviewRequest request)
|
||||
: _request(request)
|
||||
, _msgBg(_request.msgBg)
|
||||
, _msgShadow(_request.msgShadow)
|
||||
, _nameFontOwned(_request.family, style::FontFlag::Semibold, st::fsize)
|
||||
, _nameFont(_nameFontOwned.font())
|
||||
, _nameStyle(st::semiboldTextStyle)
|
||||
, _textFontOwned(_request.family, 0, st::fsize)
|
||||
, _textFont(_textFontOwned.font())
|
||||
, _textStyle(st::defaultTextStyle) {
|
||||
_nameStyle.font = _nameFont;
|
||||
_textStyle.font = _textFont;
|
||||
|
||||
layout();
|
||||
|
||||
const auto ratio = style::DevicePixelRatio();
|
||||
_result = QImage(
|
||||
_outer * ratio,
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
_result.setDevicePixelRatio(ratio);
|
||||
|
||||
auto p = Painter(&_result);
|
||||
p.drawImage(0, 0, bg);
|
||||
|
||||
p.translate(_bubble.topLeft());
|
||||
paintBubble(p);
|
||||
}
|
||||
|
||||
void PreviewPainter::paintBubble(Painter &p) {
|
||||
validateBubbleCache();
|
||||
const auto bubble = QRect(QPoint(), _bubble.size());
|
||||
const auto cornerShadow = _bubbleShadowBottomRight.size()
|
||||
/ _bubbleShadowBottomRight.devicePixelRatio();
|
||||
p.drawPixmap(
|
||||
bubble.width() - cornerShadow.width(),
|
||||
bubble.height() + st::msgShadow - cornerShadow.height(),
|
||||
_bubbleShadowBottomRight);
|
||||
Ui::FillRoundRect(p, bubble, _msgBg.color(), _bubbleCorners);
|
||||
const auto &bubbleTail = _request.bubbleTail;
|
||||
const auto tail = bubbleTail.size() / bubbleTail.devicePixelRatio();
|
||||
p.drawImage(-tail.width(), bubble.height() - tail.height(), bubbleTail);
|
||||
p.fillRect(
|
||||
-tail.width(),
|
||||
bubble.height(),
|
||||
tail.width() + bubble.width() - cornerShadow.width(),
|
||||
st::msgShadow,
|
||||
_request.msgShadow);
|
||||
p.translate(_content.topLeft());
|
||||
const auto local = _content.translated(-_content.topLeft());
|
||||
p.setClipRect(local);
|
||||
paintContent(p);
|
||||
}
|
||||
|
||||
void PreviewPainter::validateBubbleCache() {
|
||||
if (!_bubbleCorners.p[0].isNull()) {
|
||||
return;
|
||||
}
|
||||
const auto radius = st::bubbleRadiusLarge;
|
||||
_bubbleCorners = Ui::PrepareCornerPixmaps(radius, _msgBg.color());
|
||||
_bubbleCorners.p[2] = {};
|
||||
_bubbleShadowBottomRight
|
||||
= Ui::PrepareCornerPixmaps(radius, _msgShadow.color()).p[3];
|
||||
}
|
||||
|
||||
void PreviewPainter::paintContent(Painter &p) {
|
||||
paintReply(p);
|
||||
|
||||
p.translate(_message.topLeft());
|
||||
const auto local = _message.translated(-_message.topLeft());
|
||||
p.setClipRect(local);
|
||||
paintMessage(p);
|
||||
}
|
||||
|
||||
void PreviewPainter::paintReply(Painter &p) {
|
||||
{
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(_request.replyBar);
|
||||
|
||||
const auto outline = st::messageTextStyle.blockquote.outline;
|
||||
const auto radius = st::messageTextStyle.blockquote.radius;
|
||||
p.setOpacity(Ui::kDefaultOutline1Opacity);
|
||||
p.setClipRect(
|
||||
_replyRect.x(),
|
||||
_replyRect.y(),
|
||||
outline,
|
||||
_replyRect.height());
|
||||
p.drawRoundedRect(_replyRect, radius, radius);
|
||||
p.setOpacity(Ui::kDefaultBgOpacity);
|
||||
p.setClipRect(
|
||||
_replyRect.x() + outline,
|
||||
_replyRect.y(),
|
||||
_replyRect.width() - outline,
|
||||
_replyRect.height());
|
||||
p.drawRoundedRect(_replyRect, radius, radius);
|
||||
}
|
||||
p.setOpacity(1.);
|
||||
p.setClipping(false);
|
||||
|
||||
p.setPen(_request.replyNameFg);
|
||||
_nameText.drawLeftElided(
|
||||
p,
|
||||
_name.x(),
|
||||
_name.y(),
|
||||
_name.width(),
|
||||
_outer.width());
|
||||
|
||||
p.setPen(_request.textFg);
|
||||
_replyText.drawLeftElided(
|
||||
p,
|
||||
_reply.x(),
|
||||
_reply.y(),
|
||||
_reply.width(),
|
||||
_outer.width());
|
||||
}
|
||||
|
||||
void PreviewPainter::paintMessage(Painter &p) {
|
||||
p.setPen(_request.textFg);
|
||||
_messageText.drawLeft(p, 0, 0, _message.width(), _message.width());
|
||||
}
|
||||
|
||||
QImage PreviewPainter::takeResult() {
|
||||
return std::move(_result);
|
||||
}
|
||||
|
||||
void PreviewPainter::layout() {
|
||||
const auto skip = st::boxRowPadding.left();
|
||||
const auto minTextWidth = style::ConvertScale(kMinTextWidth);
|
||||
const auto maxTextWidth = st::boxWidth
|
||||
- 2 * skip
|
||||
- st::msgPadding.left()
|
||||
- st::msgPadding.right();
|
||||
|
||||
_nameText = Ui::Text::String(
|
||||
_nameStyle,
|
||||
tr::lng_settings_chat_message_reply_from(tr::now));
|
||||
_replyText = Ui::Text::String(
|
||||
_textStyle,
|
||||
tr::lng_background_text2(tr::now));
|
||||
_messageText = Ui::Text::String(
|
||||
_textStyle,
|
||||
tr::lng_background_text1(tr::now),
|
||||
kDefaultTextOptions,
|
||||
st::msgMinWidth / 2);
|
||||
|
||||
const auto namePosition = QPoint(
|
||||
st::historyReplyPadding.left(),
|
||||
st::historyReplyPadding.top());
|
||||
const auto replyPosition = QPoint(
|
||||
st::historyReplyPadding.left(),
|
||||
(st::historyReplyPadding.top() + _nameFont->height));
|
||||
const auto paddingRight = st::historyReplyPadding.right();
|
||||
|
||||
const auto wantedWidth = std::max({
|
||||
namePosition.x() + _nameText.maxWidth() + paddingRight,
|
||||
replyPosition.x() + _replyText.maxWidth() + paddingRight,
|
||||
_messageText.maxWidth()
|
||||
});
|
||||
|
||||
const auto messageWidth = std::clamp(
|
||||
wantedWidth,
|
||||
minTextWidth,
|
||||
maxTextWidth);
|
||||
const auto messageHeight = _messageText.countHeight(messageWidth);
|
||||
|
||||
_replyRect = QRect(
|
||||
st::msgReplyBarPos.x(),
|
||||
st::historyReplyTop,
|
||||
messageWidth,
|
||||
(st::historyReplyPadding.top()
|
||||
+ _nameFont->height
|
||||
+ _textFont->height
|
||||
+ st::historyReplyPadding.bottom()));
|
||||
|
||||
_name = QRect(
|
||||
_replyRect.topLeft() + namePosition,
|
||||
QSize(messageWidth - namePosition.x(), _nameFont->height));
|
||||
_reply = QRect(
|
||||
_replyRect.topLeft() + replyPosition,
|
||||
QSize(messageWidth - replyPosition.x(), _textFont->height));
|
||||
_message = QRect(0, 0, messageWidth, messageHeight);
|
||||
|
||||
const auto replySkip = _replyRect.y()
|
||||
+ _replyRect.height()
|
||||
+ st::historyReplyBottom;
|
||||
_message.moveTop(replySkip);
|
||||
|
||||
_content = QRect(0, 0, messageWidth, replySkip + messageHeight);
|
||||
|
||||
const auto msgPadding = st::msgPadding;
|
||||
_bubble = _content.marginsAdded(msgPadding);
|
||||
_content.moveTopLeft(-_bubble.topLeft());
|
||||
_bubble.moveTopLeft({});
|
||||
|
||||
_outer = QSize(st::boxWidth, st::boxWidth / 2);
|
||||
|
||||
_bubble.moveTopLeft({ skip, std::max(
|
||||
(_outer.height() - _bubble.height()) / 2,
|
||||
st::msgMargin.top()) });
|
||||
}
|
||||
|
||||
[[nodiscard]] QImage GeneratePreview(
|
||||
const QImage &bg,
|
||||
PreviewRequest request) {
|
||||
return PreviewPainter(bg, request).takeResult();
|
||||
}
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> MakePreview(
|
||||
not_null<QWidget*> parent,
|
||||
Fn<QImage()> generatePreviewBg,
|
||||
rpl::producer<QString> family) {
|
||||
auto result = object_ptr<Ui::RpWidget>(parent.get());
|
||||
const auto raw = result.data();
|
||||
|
||||
struct State {
|
||||
QImage preview;
|
||||
QImage bg;
|
||||
QString family;
|
||||
};
|
||||
const auto state = raw->lifetime().make_state<State>();
|
||||
|
||||
state->bg = generatePreviewBg();
|
||||
style::PaletteChanged() | rpl::on_next([=] {
|
||||
state->bg = generatePreviewBg();
|
||||
}, raw->lifetime());
|
||||
|
||||
rpl::combine(
|
||||
rpl::single(rpl::empty) | rpl::then(style::PaletteChanged()),
|
||||
std::move(family)
|
||||
) | rpl::on_next([=](const auto &, QString family) {
|
||||
state->family = family;
|
||||
if (state->preview.isNull()) {
|
||||
state->preview = GeneratePreview(
|
||||
state->bg,
|
||||
PrepareRequest(family));
|
||||
const auto ratio = state->preview.devicePixelRatio();
|
||||
raw->resize(state->preview.size() / int(ratio));
|
||||
} else {
|
||||
const auto weak = base::make_weak(raw);
|
||||
const auto request = PrepareRequest(family);
|
||||
crl::async([=, bg = state->bg] {
|
||||
crl::on_main([
|
||||
weak,
|
||||
state,
|
||||
preview = GeneratePreview(bg, request)
|
||||
]() mutable {
|
||||
if (const auto strong = weak.get()) {
|
||||
state->preview = std::move(preview);
|
||||
const auto ratio = state->preview.devicePixelRatio();
|
||||
strong->resize(
|
||||
strong->width(),
|
||||
(state->preview.height() / int(ratio)));
|
||||
strong->update();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}, raw->lifetime());
|
||||
|
||||
raw->paintRequest() | rpl::on_next([=](QRect clip) {
|
||||
QPainter(raw).drawImage(0, 0, state->preview);
|
||||
}, raw->lifetime());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ChooseFontBox(
|
||||
not_null<GenericBox*> box,
|
||||
Fn<QImage()> generatePreviewBg,
|
||||
const QString &family,
|
||||
Fn<void(QString)> save) {
|
||||
box->setTitle(tr::lng_font_box_title());
|
||||
|
||||
struct State {
|
||||
rpl::variable<QString> family;
|
||||
rpl::variable<QString> query;
|
||||
rpl::event_stream<> submits;
|
||||
};
|
||||
const auto state = box->lifetime().make_state<State>(State{
|
||||
.family = family,
|
||||
});
|
||||
|
||||
const auto top = box->setPinnedToTopContent(
|
||||
object_ptr<Ui::VerticalLayout>(box));
|
||||
top->add(MakePreview(top, generatePreviewBg, state->family.value()));
|
||||
const auto filter = top->add(object_ptr<Ui::MultiSelect>(
|
||||
top,
|
||||
st::defaultMultiSelect,
|
||||
tr::lng_participant_filter()));
|
||||
top->resizeToWidth(st::boxWidth);
|
||||
|
||||
filter->setSubmittedCallback([=](Qt::KeyboardModifiers) {
|
||||
state->submits.fire({});
|
||||
});
|
||||
filter->setQueryChangedCallback([=](const QString &query) {
|
||||
state->query = query;
|
||||
});
|
||||
filter->setCancelledCallback([=] {
|
||||
filter->clearQuery();
|
||||
});
|
||||
|
||||
const auto chosen = [=](const QString &value) {
|
||||
state->family = value;
|
||||
filter->clearQuery();
|
||||
};
|
||||
const auto scrollTo = [=](
|
||||
Ui::ScrollToRequest request,
|
||||
anim::type animated) {
|
||||
box->scrollTo(request, animated);
|
||||
};
|
||||
const auto selector = box->addRow(
|
||||
object_ptr<Selector>(
|
||||
box,
|
||||
state->family.current(),
|
||||
state->query.value(),
|
||||
state->submits.events(),
|
||||
chosen,
|
||||
scrollTo),
|
||||
QMargins());
|
||||
box->setMinHeight(st::boxMaxListHeight);
|
||||
box->setMaxHeight(st::boxMaxListHeight);
|
||||
|
||||
base::install_event_filter(filter, [=](not_null<QEvent*> e) {
|
||||
if (e->type() == QEvent::KeyPress) {
|
||||
const auto key = static_cast<QKeyEvent*>(e.get())->key();
|
||||
if (key == Qt::Key_Up
|
||||
|| key == Qt::Key_Down
|
||||
|| key == Qt::Key_PageUp
|
||||
|| key == Qt::Key_PageDown) {
|
||||
selector->selectSkip(Qt::Key(key));
|
||||
return base::EventFilterResult::Cancel;
|
||||
}
|
||||
}
|
||||
return base::EventFilterResult::Continue;
|
||||
});
|
||||
|
||||
rpl::combine(
|
||||
box->heightValue(),
|
||||
top->heightValue()
|
||||
) | rpl::on_next([=](int box, int top) {
|
||||
selector->setMinHeight(box - top);
|
||||
}, selector->lifetime());
|
||||
|
||||
const auto apply = [=](QString chosen) {
|
||||
if (chosen == family) {
|
||||
box->closeBox();
|
||||
return;
|
||||
}
|
||||
box->getDelegate()->show(Ui::MakeConfirmBox({
|
||||
.text = tr::lng_settings_need_restart(),
|
||||
.confirmed = [=] { save(chosen); },
|
||||
.confirmText = tr::lng_settings_restart_now(),
|
||||
}));
|
||||
};
|
||||
const auto refreshButtons = [=](QString chosen) {
|
||||
box->clearButtons();
|
||||
// Doesn't fit in most languages.
|
||||
//if (!chosen.isEmpty()) {
|
||||
// box->addLeftButton(tr::lng_background_reset_default(), [=] {
|
||||
// apply(QString());
|
||||
// });
|
||||
//}
|
||||
box->addButton(tr::lng_settings_save(), [=] {
|
||||
apply(chosen);
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] {
|
||||
box->closeBox();
|
||||
});
|
||||
};
|
||||
state->family.value(
|
||||
) | rpl::on_next(refreshButtons, box->lifetime());
|
||||
|
||||
box->setFocusCallback([=] {
|
||||
filter->setInnerFocus();
|
||||
});
|
||||
box->setInitScrollCallback([=] {
|
||||
SendPendingMoveResizeEvents(box);
|
||||
selector->initScroll(anim::type::instant);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
20
Telegram/SourceFiles/ui/boxes/choose_font_box.h
Normal file
20
Telegram/SourceFiles/ui/boxes/choose_font_box.h
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class GenericBox;
|
||||
|
||||
void ChooseFontBox(
|
||||
not_null<GenericBox*> box,
|
||||
Fn<QImage()> generatePreviewBg,
|
||||
const QString &family,
|
||||
Fn<void(QString)> save);
|
||||
|
||||
} // namespace Ui
|
||||
382
Telegram/SourceFiles/ui/boxes/choose_language_box.cpp
Normal file
382
Telegram/SourceFiles/ui/boxes/choose_language_box.cpp
Normal file
@@ -0,0 +1,382 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/boxes/choose_language_box.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "spellcheck/spellcheck_types.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/multi_select.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/painter.h"
|
||||
#include "base/debug_log.h"
|
||||
#include "styles/style_info.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
const auto kLanguageNamePrefix = "cloud_lng_language_";
|
||||
const auto kTranslateToPrefix = "cloud_lng_translate_to_";
|
||||
|
||||
[[nodiscard]] std::vector<LanguageId> TranslationLanguagesList() {
|
||||
// If adding some languages here you need to check that it is
|
||||
// supported on the server. Right now server supports those:
|
||||
//
|
||||
// 'af', 'sq', 'am', 'ar', 'hy', 'az', 'eu', 'be', 'bn', 'bs', 'bg',
|
||||
// 'ca', 'ceb', 'zh-CN', 'zh', 'zh-TW', 'co', 'hr', 'cs', 'da', 'nl',
|
||||
// 'en', 'eo', 'et', 'fi', 'fr', 'fy', 'gl', 'ka', 'de', 'el', 'gu',
|
||||
// 'ht', 'ha', 'haw', 'he', 'iw', 'hi', 'hmn', 'hu', 'is', 'ig', 'id',
|
||||
// 'ga', 'it', 'ja', 'jv', 'kn', 'kk', 'km', 'rw', 'ko', 'ku', 'ky',
|
||||
// 'lo', 'la', 'lv', 'lt', 'lb', 'mk', 'mg', 'ms', 'ml', 'mt', 'mi',
|
||||
// 'mr', 'mn', 'my', 'ne', 'no', 'ny', 'or', 'ps', 'fa', 'pl', 'pt',
|
||||
// 'pa', 'ro', 'ru', 'sm', 'gd', 'sr', 'st', 'sn', 'sd', 'si', 'sk',
|
||||
// 'sl', 'so', 'es', 'su', 'sw', 'sv', 'tl', 'tg', 'ta', 'tt', 'te',
|
||||
// 'th', 'tr', 'tk', 'uk', 'ur', 'ug', 'uz', 'vi', 'cy', 'xh', 'yi',
|
||||
// 'yo', 'zu',
|
||||
return {
|
||||
{ QLocale::English },
|
||||
{ QLocale::Arabic },
|
||||
{ QLocale::Belarusian },
|
||||
{ QLocale::Catalan },
|
||||
{ QLocale::Chinese },
|
||||
{ QLocale::Dutch },
|
||||
{ QLocale::French },
|
||||
{ QLocale::German },
|
||||
{ QLocale::Indonesian },
|
||||
{ QLocale::Italian },
|
||||
{ QLocale::Japanese },
|
||||
{ QLocale::Korean },
|
||||
{ QLocale::Polish },
|
||||
{ QLocale::Portuguese },
|
||||
{ QLocale::Russian },
|
||||
{ QLocale::Spanish },
|
||||
{ QLocale::Ukrainian },
|
||||
|
||||
{ QLocale::Afrikaans },
|
||||
{ QLocale::Albanian },
|
||||
{ QLocale::Amharic },
|
||||
{ QLocale::Armenian },
|
||||
{ QLocale::Azerbaijani },
|
||||
{ QLocale::Basque },
|
||||
{ QLocale::Bosnian },
|
||||
{ QLocale::Bulgarian },
|
||||
{ QLocale::Burmese },
|
||||
{ QLocale::Croatian },
|
||||
{ QLocale::Czech },
|
||||
{ QLocale::Danish },
|
||||
{ QLocale::Esperanto },
|
||||
{ QLocale::Estonian },
|
||||
{ QLocale::Finnish },
|
||||
{ QLocale::Gaelic },
|
||||
{ QLocale::Galician },
|
||||
{ QLocale::Georgian },
|
||||
{ QLocale::Greek },
|
||||
{ QLocale::Gusii },
|
||||
{ QLocale::Hausa },
|
||||
{ QLocale::Hebrew },
|
||||
{ QLocale::Hungarian },
|
||||
{ QLocale::Icelandic },
|
||||
{ QLocale::Igbo },
|
||||
{ QLocale::Irish },
|
||||
{ QLocale::Kazakh },
|
||||
{ QLocale::Kinyarwanda },
|
||||
{ QLocale::Kurdish },
|
||||
{ QLocale::Lao },
|
||||
{ QLocale::Latvian },
|
||||
{ QLocale::Lithuanian },
|
||||
{ QLocale::Luxembourgish },
|
||||
{ QLocale::Macedonian },
|
||||
{ QLocale::Malagasy },
|
||||
{ QLocale::Malay },
|
||||
{ QLocale::Maltese },
|
||||
{ QLocale::Maori },
|
||||
{ QLocale::Mongolian },
|
||||
{ QLocale::Nepali },
|
||||
{ QLocale::Pashto },
|
||||
{ QLocale::Persian },
|
||||
{ QLocale::Romanian },
|
||||
{ QLocale::Serbian },
|
||||
{ QLocale::Shona },
|
||||
{ QLocale::Sindhi },
|
||||
{ QLocale::Sinhala },
|
||||
{ QLocale::Slovak },
|
||||
{ QLocale::Slovenian },
|
||||
{ QLocale::Somali },
|
||||
{ QLocale::Sundanese },
|
||||
{ QLocale::Swahili },
|
||||
{ QLocale::Swedish },
|
||||
{ QLocale::Tajik },
|
||||
{ QLocale::Tatar },
|
||||
{ QLocale::Teso },
|
||||
{ QLocale::Thai },
|
||||
{ QLocale::Turkish },
|
||||
{ QLocale::Turkmen },
|
||||
{ QLocale::Urdu },
|
||||
{ QLocale::Uzbek },
|
||||
{ QLocale::Vietnamese },
|
||||
{ QLocale::Welsh },
|
||||
{ QLocale::WesternFrisian },
|
||||
{ QLocale::Xhosa },
|
||||
{ QLocale::Yiddish },
|
||||
};
|
||||
}
|
||||
|
||||
class Row final : public SettingsButton {
|
||||
public:
|
||||
Row(not_null<RpWidget*> parent, LanguageId id);
|
||||
|
||||
[[nodiscard]] bool filtered(const QString &query) const;
|
||||
[[nodiscard]] LanguageId id() const;
|
||||
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
private:
|
||||
const style::PeerListItem &_st;
|
||||
const LanguageId _id;
|
||||
const QString _status;
|
||||
const QString _titleText;
|
||||
Text::String _title;
|
||||
|
||||
};
|
||||
|
||||
Row::Row(not_null<RpWidget*> parent, LanguageId id)
|
||||
: SettingsButton(parent, rpl::never<QString>())
|
||||
, _st(st::inviteLinkListItem)
|
||||
, _id(id)
|
||||
, _status(LanguageName(id))
|
||||
, _titleText(LanguageNameNative(id))
|
||||
, _title(_st.nameStyle, _titleText) {
|
||||
}
|
||||
|
||||
LanguageId Row::id() const {
|
||||
return _id;
|
||||
}
|
||||
|
||||
bool Row::filtered(const QString &query) const {
|
||||
return _status.startsWith(query, Qt::CaseInsensitive)
|
||||
|| _titleText.startsWith(query, Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
int Row::resizeGetHeight(int newWidth) {
|
||||
return _st.height;
|
||||
}
|
||||
|
||||
void Row::paintEvent(QPaintEvent *e) {
|
||||
auto p = Painter(this);
|
||||
|
||||
const auto paintOver = (isOver() || isDown()) && !isDisabled();
|
||||
SettingsButton::paintBg(p, e->rect(), paintOver);
|
||||
SettingsButton::paintRipple(p, 0, 0);
|
||||
SettingsButton::paintToggle(p, width());
|
||||
|
||||
const auto &color = st::windowSubTextFg;
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(color);
|
||||
|
||||
const auto left = st::defaultSubsectionTitlePadding.left();
|
||||
const auto toggleRect = SettingsButton::maybeToggleRect();
|
||||
const auto right = left
|
||||
+ (toggleRect.isEmpty() ? 0 : (width() - toggleRect.x()));
|
||||
|
||||
const auto availableWidth = std::min(
|
||||
_title.maxWidth(),
|
||||
width() - left - right);
|
||||
p.setPen(_st.nameFg);
|
||||
_title.drawLeft(
|
||||
p,
|
||||
left,
|
||||
_st.namePosition.y(),
|
||||
availableWidth,
|
||||
width() - left - right);
|
||||
|
||||
p.setPen(paintOver ? _st.statusFgOver : _st.statusFg);
|
||||
p.setFont(st::contactsStatusFont);
|
||||
p.drawTextLeft(
|
||||
left,
|
||||
_st.statusPosition.y(),
|
||||
width() - left - right,
|
||||
_status);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
QString LanguageNameTranslated(const QString &twoLetterCode) {
|
||||
return Lang::GetNonDefaultValue(
|
||||
kLanguageNamePrefix + twoLetterCode.toUtf8());
|
||||
}
|
||||
|
||||
QString LanguageNameLocal(LanguageId id) {
|
||||
return QLocale::languageToString(id.language());
|
||||
}
|
||||
|
||||
QString LanguageName(LanguageId id) {
|
||||
const auto translated = LanguageNameTranslated(id.twoLetterCode());
|
||||
return translated.isEmpty() ? LanguageNameLocal(id) : translated;
|
||||
}
|
||||
|
||||
QString LanguageNameNative(LanguageId id) {
|
||||
const auto locale = id.locale();
|
||||
if (locale.language() == QLocale::English
|
||||
&& (locale.country() == QLocale::UnitedStates
|
||||
|| locale.country() == QLocale::AnyCountry)) {
|
||||
return u"English"_q;
|
||||
} else if (locale.language() == QLocale::Spanish) {
|
||||
return QString::fromUtf8("\x45\x73\x70\x61\xc3\xb1\x6f\x6c");
|
||||
} else {
|
||||
const auto name = locale.nativeLanguageName();
|
||||
return name.left(1).toUpper() + name.mid(1);
|
||||
}
|
||||
}
|
||||
|
||||
rpl::producer<QString> TranslateBarTo(LanguageId id) {
|
||||
const auto translated = Lang::GetNonDefaultValue(
|
||||
kTranslateToPrefix + id.twoLetterCode().toUtf8());
|
||||
return (translated.isEmpty()
|
||||
? tr::lng_translate_bar_to_other
|
||||
: tr::lng_translate_bar_to)(
|
||||
lt_name,
|
||||
rpl::single(translated.isEmpty()
|
||||
? LanguageNameLocal(id)
|
||||
: translated));
|
||||
}
|
||||
|
||||
QString TranslateMenuDont(tr::now_t, LanguageId id) {
|
||||
const auto translated = Lang::GetNonDefaultValue(
|
||||
kTranslateToPrefix + id.twoLetterCode().toUtf8());
|
||||
return (translated.isEmpty()
|
||||
? tr::lng_translate_menu_dont_other
|
||||
: tr::lng_translate_menu_dont)(
|
||||
tr::now,
|
||||
lt_name,
|
||||
translated.isEmpty() ? LanguageNameLocal(id) : translated);
|
||||
}
|
||||
|
||||
void ChooseLanguageBox(
|
||||
not_null<GenericBox*> box,
|
||||
rpl::producer<QString> title,
|
||||
Fn<void(std::vector<LanguageId>)> callback,
|
||||
std::vector<LanguageId> selected,
|
||||
bool multiselect,
|
||||
Fn<bool(LanguageId)> toggleCheck) {
|
||||
box->setMinHeight(st::boxWidth);
|
||||
box->setMaxHeight(st::boxWidth);
|
||||
box->setTitle(std::move(title));
|
||||
|
||||
const auto multiSelect = box->setPinnedToTopContent(
|
||||
object_ptr<MultiSelect>(
|
||||
box,
|
||||
st::defaultMultiSelect,
|
||||
tr::lng_participant_filter()));
|
||||
box->setFocusCallback([=] { multiSelect->setInnerFocus(); });
|
||||
|
||||
const auto container = box->verticalLayout();
|
||||
const auto langs = [&] {
|
||||
auto list = TranslationLanguagesList();
|
||||
for (const auto id : list) {
|
||||
LOG(("cloud_lng_language_%1").arg(id.twoLetterCode()));
|
||||
}
|
||||
const auto current = LanguageId{ QLocale(
|
||||
Lang::LanguageIdOrDefault(Lang::Id())).language() };
|
||||
if (const auto i = ranges::find(list, current); i != end(list)) {
|
||||
base::reorder(list, std::distance(begin(list), i), 0);
|
||||
}
|
||||
ranges::stable_partition(list, [&](LanguageId id) {
|
||||
return ranges::contains(selected, id);
|
||||
});
|
||||
return list;
|
||||
}();
|
||||
struct ToggleOne {
|
||||
LanguageId id;
|
||||
bool selected = false;
|
||||
};
|
||||
struct State {
|
||||
rpl::event_stream<ToggleOne> toggles;
|
||||
};
|
||||
const auto state = box->lifetime().make_state<State>();
|
||||
auto rows = std::vector<not_null<SlideWrap<Row>*>>();
|
||||
rows.reserve(langs.size());
|
||||
for (const auto &id : langs) {
|
||||
const auto button = container->add(
|
||||
object_ptr<SlideWrap<Row>>(
|
||||
container,
|
||||
object_ptr<Row>(container, id)));
|
||||
if (multiselect) {
|
||||
button->entity()->toggleOn(rpl::single(
|
||||
ranges::contains(selected, id)
|
||||
) | rpl::then(state->toggles.events(
|
||||
) | rpl::filter([=](ToggleOne one) {
|
||||
return one.id == id;
|
||||
}) | rpl::map([=](ToggleOne one) {
|
||||
return one.selected;
|
||||
})));
|
||||
|
||||
button->entity()->toggledChanges(
|
||||
) | rpl::on_next([=](bool value) {
|
||||
if (toggleCheck && !toggleCheck(id)) {
|
||||
state->toggles.fire({ .id = id, .selected = !value });
|
||||
}
|
||||
}, button->lifetime());
|
||||
} else {
|
||||
button->entity()->setClickedCallback([=] {
|
||||
callback({ id });
|
||||
box->closeBox();
|
||||
});
|
||||
}
|
||||
rows.push_back(button);
|
||||
}
|
||||
|
||||
multiSelect->setQueryChangedCallback([=](const QString &query) {
|
||||
for (const auto &row : rows) {
|
||||
const auto toggled = row->entity()->filtered(query);
|
||||
if (toggled != row->toggled()) {
|
||||
row->toggle(toggled, anim::type::instant);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
const auto label = CreateChild<FlatLabel>(
|
||||
box.get(),
|
||||
tr::lng_languages_none(),
|
||||
st::membersAbout);
|
||||
box->verticalLayout()->geometryValue(
|
||||
) | rpl::on_next([=](const QRect &geometry) {
|
||||
const auto shown = (geometry.height() <= 0);
|
||||
label->setVisible(shown);
|
||||
if (shown) {
|
||||
label->moveToLeft(
|
||||
(geometry.width() - label->width()) / 2,
|
||||
geometry.y() + st::membersAbout.style.font->height * 4);
|
||||
label->stackUnder(box->verticalLayout());
|
||||
}
|
||||
}, label->lifetime());
|
||||
}
|
||||
|
||||
if (multiselect) {
|
||||
box->addButton(tr::lng_settings_save(), [=] {
|
||||
auto result = ranges::views::all(
|
||||
rows
|
||||
) | ranges::views::filter([](const auto &row) {
|
||||
return row->entity()->toggled();
|
||||
}) | ranges::views::transform([](const auto &row) {
|
||||
return row->entity()->id();
|
||||
}) | ranges::to_vector;
|
||||
if (!result.empty()) {
|
||||
callback(std::move(result));
|
||||
}
|
||||
box->closeBox();
|
||||
});
|
||||
}
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
36
Telegram/SourceFiles/ui/boxes/choose_language_box.h
Normal file
36
Telegram/SourceFiles/ui/boxes/choose_language_box.h
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
struct LanguageId;
|
||||
|
||||
namespace tr {
|
||||
struct now_t;
|
||||
} // namespace tr
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class GenericBox;
|
||||
|
||||
[[nodiscard]] QString LanguageNameTranslated(const QString &twoLetterCode);
|
||||
[[nodiscard]] QString LanguageNameLocal(LanguageId id);
|
||||
[[nodiscard]] QString LanguageName(LanguageId id);
|
||||
[[nodiscard]] QString LanguageNameNative(LanguageId id);
|
||||
|
||||
[[nodiscard]] rpl::producer<QString> TranslateBarTo(LanguageId id);
|
||||
[[nodiscard]] QString TranslateMenuDont(tr::now_t, LanguageId id);
|
||||
|
||||
void ChooseLanguageBox(
|
||||
not_null<GenericBox*> box,
|
||||
rpl::producer<QString> title,
|
||||
Fn<void(std::vector<LanguageId>)> callback,
|
||||
std::vector<LanguageId> selected,
|
||||
bool multiselect,
|
||||
Fn<bool(LanguageId)> toggleCheck);
|
||||
|
||||
} // namespace Ui
|
||||
145
Telegram/SourceFiles/ui/boxes/choose_time.cpp
Normal file
145
Telegram/SourceFiles/ui/boxes/choose_time.cpp
Normal file
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/boxes/choose_time.h"
|
||||
|
||||
#include "base/qt_signal_producer.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/widgets/fields/time_part_input_with_placeholder.h"
|
||||
#include "ui/wrap/padding_wrap.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
ChooseTimeResult ChooseTimeWidget(
|
||||
not_null<RpWidget*> parent,
|
||||
TimeId startSeconds,
|
||||
bool hiddenDaysInput) {
|
||||
using TimeField = Ui::TimePartWithPlaceholder;
|
||||
const auto putNext = [](not_null<TimeField*> field, QChar ch) {
|
||||
field->setCursorPosition(0);
|
||||
if (ch.unicode()) {
|
||||
field->setText(ch + field->getLastText());
|
||||
field->setCursorPosition(1);
|
||||
}
|
||||
field->onTextEdited();
|
||||
field->setFocus();
|
||||
};
|
||||
|
||||
const auto erasePrevious = [](not_null<TimeField*> field) {
|
||||
const auto text = field->getLastText();
|
||||
if (!text.isEmpty()) {
|
||||
field->setCursorPosition(text.size() - 1);
|
||||
field->setText(text.mid(0, text.size() - 1));
|
||||
}
|
||||
field->setFocus();
|
||||
};
|
||||
|
||||
struct State {
|
||||
not_null<TimeField*> day;
|
||||
not_null<TimeField*> hour;
|
||||
not_null<TimeField*> minute;
|
||||
|
||||
rpl::variable<int> valueInSeconds = 0;
|
||||
};
|
||||
|
||||
auto content = object_ptr<Ui::FixedHeightWidget>(
|
||||
parent,
|
||||
st::scheduleHeight);
|
||||
|
||||
const auto startDays = startSeconds / 86400;
|
||||
startSeconds -= startDays * 86400;
|
||||
const auto startHours = startSeconds / 3600;
|
||||
startSeconds -= startHours * 3600;
|
||||
const auto startMinutes = startSeconds / 60;
|
||||
|
||||
const auto state = content->lifetime().make_state<State>(State{
|
||||
.day = Ui::CreateChild<TimeField>(
|
||||
content.data(),
|
||||
st::muteBoxTimeField,
|
||||
rpl::never<QString>(),
|
||||
QString::number(startDays)),
|
||||
.hour = Ui::CreateChild<TimeField>(
|
||||
content.data(),
|
||||
st::muteBoxTimeField,
|
||||
rpl::never<QString>(),
|
||||
QString::number(startHours)),
|
||||
.minute = Ui::CreateChild<TimeField>(
|
||||
content.data(),
|
||||
st::muteBoxTimeField,
|
||||
rpl::never<QString>(),
|
||||
QString::number(startMinutes)),
|
||||
});
|
||||
|
||||
const auto day = base::make_weak(state->day);
|
||||
const auto hour = base::make_weak(state->hour);
|
||||
const auto minute = base::make_weak(state->minute);
|
||||
|
||||
if (hiddenDaysInput) {
|
||||
day->setVisible(false);
|
||||
}
|
||||
|
||||
day->setPhrase(tr::lng_days);
|
||||
day->setMaxValue(31);
|
||||
day->setWheelStep(1);
|
||||
day->putNext() | rpl::on_next([=](QChar ch) {
|
||||
putNext(hour.get(), ch);
|
||||
}, content->lifetime());
|
||||
|
||||
hour->setPhrase(tr::lng_hours);
|
||||
hour->setMaxValue(23);
|
||||
hour->setWheelStep(1);
|
||||
hour->putNext() | rpl::on_next([=](QChar ch) {
|
||||
putNext(minute.get(), ch);
|
||||
}, content->lifetime());
|
||||
hour->erasePrevious() | rpl::on_next([=] {
|
||||
erasePrevious(day.get());
|
||||
}, content->lifetime());
|
||||
|
||||
minute->setPhrase(tr::lng_minutes);
|
||||
minute->setMaxValue(59);
|
||||
minute->setWheelStep(10);
|
||||
minute->erasePrevious() | rpl::on_next([=] {
|
||||
erasePrevious(hour.get());
|
||||
}, content->lifetime());
|
||||
|
||||
content->sizeValue(
|
||||
) | rpl::on_next([=](const QSize &s) {
|
||||
const auto inputWidth = s.width() / (hiddenDaysInput ? 2 : 3);
|
||||
auto rect = QRect(
|
||||
0,
|
||||
(s.height() - day->height()) / 2,
|
||||
inputWidth,
|
||||
day->height());
|
||||
for (const auto &input : { day, hour, minute }) {
|
||||
if (input->isHidden()) {
|
||||
continue;
|
||||
}
|
||||
input->setGeometry(rect - st::muteBoxTimeFieldPadding);
|
||||
rect.translate(inputWidth, 0);
|
||||
}
|
||||
}, content->lifetime());
|
||||
|
||||
rpl::merge(
|
||||
rpl::single(rpl::empty),
|
||||
base::qt_signal_producer(day.get(), &MaskedInputField::changed),
|
||||
base::qt_signal_producer(hour.get(), &MaskedInputField::changed),
|
||||
base::qt_signal_producer(minute.get(), &MaskedInputField::changed)
|
||||
) | rpl::on_next([=] {
|
||||
state->valueInSeconds = 0
|
||||
+ day->getLastText().toUInt() * 3600 * 24
|
||||
+ hour->getLastText().toUInt() * 3600
|
||||
+ minute->getLastText().toUInt() * 60;
|
||||
}, content->lifetime());
|
||||
return {
|
||||
object_ptr<Ui::RpWidget>::fromRaw(content.release()),
|
||||
state->valueInSeconds.value(),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
26
Telegram/SourceFiles/ui/boxes/choose_time.h
Normal file
26
Telegram/SourceFiles/ui/boxes/choose_time.h
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class RpWidget;
|
||||
|
||||
struct ChooseTimeResult {
|
||||
object_ptr<RpWidget> widget;
|
||||
rpl::producer<TimeId> secondsValue;
|
||||
};
|
||||
|
||||
ChooseTimeResult ChooseTimeWidget(
|
||||
not_null<RpWidget*> parent,
|
||||
TimeId startSeconds,
|
||||
bool hiddenDaysInput = false);
|
||||
|
||||
} // namespace Ui
|
||||
277
Telegram/SourceFiles/ui/boxes/collectible_info_box.cpp
Normal file
277
Telegram/SourceFiles/ui/boxes/collectible_info_box.cpp
Normal file
@@ -0,0 +1,277 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/boxes/collectible_info_box.h"
|
||||
|
||||
#include "base/unixtime.h"
|
||||
#include "core/file_utilities.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "lottie/lottie_icon.h"
|
||||
#include "info/channel_statistics/earn/earn_format.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/dynamic_image.h"
|
||||
#include "ui/painter.h"
|
||||
#include "settings/settings_common.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_credits.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
#include <QtCore/QRegularExpression>
|
||||
#include <QtGui/QGuiApplication>
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kTonMultiplier = uint64(1000000000);
|
||||
|
||||
[[nodiscard]] QString FormatEntity(CollectibleType type, QString entity) {
|
||||
switch (type) {
|
||||
case CollectibleType::Phone: {
|
||||
static const auto kNonDigits = QRegularExpression(u"[^\\d]"_q);
|
||||
entity.replace(kNonDigits, QString());
|
||||
} return Ui::FormatPhone(entity);
|
||||
case CollectibleType::Username:
|
||||
return entity.startsWith('@') ? entity : ('@' + entity);
|
||||
}
|
||||
Unexpected("CollectibleType in FormatEntity.");
|
||||
}
|
||||
|
||||
[[nodiscard]] QString FormatDate(TimeId date) {
|
||||
return langDateTime(base::unixtime::parse(date));
|
||||
}
|
||||
|
||||
[[nodiscard]] TextWithEntities FormatPrice(const CollectibleInfo &info) {
|
||||
auto minor = Info::ChannelEarn::MinorPart(info.cryptoAmount);
|
||||
if (minor.size() == 1 && minor.at(0) == '.') {
|
||||
minor += '0';
|
||||
}
|
||||
auto price = (info.cryptoCurrency == u"TON"_q)
|
||||
? Ui::Text::IconEmoji(
|
||||
&st::tonIconEmoji
|
||||
).append(
|
||||
Info::ChannelEarn::MajorPart(info.cryptoAmount)
|
||||
).append(minor)
|
||||
: TextWithEntities{ ('{'
|
||||
+ info.cryptoCurrency + ':' + QString::number(info.cryptoAmount)
|
||||
+ '}') };
|
||||
const auto fiat = Ui::FillAmountAndCurrency(info.amount, info.currency);
|
||||
return Ui::Text::Wrapped(
|
||||
price,
|
||||
EntityType::Bold
|
||||
).append(u" ("_q + fiat + ')');
|
||||
}
|
||||
|
||||
[[nodiscard]] object_ptr<Ui::RpWidget> MakeOwnerCell(
|
||||
not_null<QWidget*> parent,
|
||||
const CollectibleInfo &info) {
|
||||
const auto st = &st::defaultMultiSelectItem;
|
||||
const auto size = st->height;
|
||||
auto result = object_ptr<Ui::FixedHeightWidget>(parent.get(), size);
|
||||
const auto raw = result.data();
|
||||
|
||||
const auto name = info.ownerName;
|
||||
const auto userpic = info.ownerUserpic;
|
||||
const auto nameWidth = st->style.font->width(name);
|
||||
const auto added = size + st->padding.left() + st->padding.right();
|
||||
const auto subscribed = std::make_shared<bool>(false);
|
||||
raw->paintRequest() | rpl::on_next([=] {
|
||||
const auto use = std::min(nameWidth + added, raw->width());
|
||||
const auto x = (raw->width() - use) / 2;
|
||||
if (const auto available = use - added; available > 0) {
|
||||
auto p = QPainter(raw);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(st->textBg);
|
||||
p.drawRoundedRect(x, 0, use, size, size / 2., size / 2.);
|
||||
|
||||
if (!*subscribed) {
|
||||
*subscribed = true;
|
||||
userpic->subscribeToUpdates([=] { raw->update(); });
|
||||
}
|
||||
p.drawImage(QRect(x, 0, size, size), userpic->image(size));
|
||||
|
||||
const auto textx = x + size + st->padding.left();
|
||||
const auto texty = st->padding.top() + st->style.font->ascent;
|
||||
const auto text = (use == nameWidth + added)
|
||||
? name
|
||||
: st->style.font->elided(name, available);
|
||||
p.setPen(st->textFg);
|
||||
p.setFont(st->style.font);
|
||||
p.drawText(textx, texty, text);
|
||||
}
|
||||
}, raw->lifetime());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CollectibleType DetectCollectibleType(const QString &entity) {
|
||||
return entity.startsWith('+')
|
||||
? CollectibleType::Phone
|
||||
: CollectibleType::Username;
|
||||
}
|
||||
|
||||
void CollectibleInfoBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
CollectibleInfo info) {
|
||||
box->setWidth(st::boxWideWidth);
|
||||
box->setStyle(st::collectibleBox);
|
||||
|
||||
const auto type = DetectCollectibleType(info.entity);
|
||||
|
||||
const auto icon = box->addRow(
|
||||
object_ptr<Ui::FixedHeightWidget>(box, st::collectibleIconDiameter),
|
||||
st::collectibleIconPadding);
|
||||
icon->paintRequest(
|
||||
) | rpl::on_next([=](QRect clip) {
|
||||
const auto size = icon->height();
|
||||
const auto inner = QRect(
|
||||
(icon->width() - size) / 2,
|
||||
0,
|
||||
size,
|
||||
size);
|
||||
if (!inner.intersects(clip)) {
|
||||
return;
|
||||
}
|
||||
auto p = QPainter(icon);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setBrush(st::defaultActiveButton.textBg);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.drawEllipse(inner);
|
||||
}, icon->lifetime());
|
||||
const auto lottieSize = st::collectibleIcon;
|
||||
auto lottie = Settings::CreateLottieIcon(
|
||||
icon,
|
||||
{
|
||||
.name = (type == CollectibleType::Phone
|
||||
? u"collectible_phone"_q
|
||||
: u"collectible_username"_q),
|
||||
.color = &st::defaultActiveButton.textFg,
|
||||
.sizeOverride = { lottieSize, lottieSize },
|
||||
},
|
||||
QMargins());
|
||||
box->showFinishes(
|
||||
) | rpl::on_next([animate = std::move(lottie.animate)] {
|
||||
animate(anim::repeat::once);
|
||||
}, box->lifetime());
|
||||
const auto animation = lottie.widget.release();
|
||||
icon->sizeValue() | rpl::on_next([=](QSize size) {
|
||||
const auto skip = (type == CollectibleType::Phone)
|
||||
? style::ConvertScale(2)
|
||||
: 0;
|
||||
animation->move(
|
||||
(size.width() - animation->width()) / 2,
|
||||
skip + (size.height() - animation->height()) / 2);
|
||||
}, animation->lifetime());
|
||||
|
||||
const auto formatted = FormatEntity(type, info.entity);
|
||||
const auto header = (type == CollectibleType::Phone)
|
||||
? tr::lng_collectible_phone_title(
|
||||
tr::now,
|
||||
lt_phone,
|
||||
tr::link(formatted),
|
||||
tr::marked)
|
||||
: tr::lng_collectible_username_title(
|
||||
tr::now,
|
||||
lt_username,
|
||||
tr::link(formatted),
|
||||
tr::marked);
|
||||
const auto copyCallback = [box, type, formatted, text = info.copyText](
|
||||
bool copyLink) {
|
||||
QGuiApplication::clipboard()->setText((text.isEmpty() || !copyLink)
|
||||
? formatted
|
||||
: text);
|
||||
box->uiShow()->showToast((type == CollectibleType::Phone)
|
||||
? tr::lng_collectible_phone_copied(tr::now)
|
||||
: copyLink
|
||||
? tr::lng_username_copied(tr::now)
|
||||
: tr::lng_username_text_copied(tr::now));
|
||||
};
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box,
|
||||
rpl::single(header),
|
||||
st::collectibleHeader),
|
||||
st::collectibleHeaderPadding,
|
||||
style::al_top
|
||||
)->setClickHandlerFilter([copyCallback](const auto &...) {
|
||||
copyCallback(false);
|
||||
return false;
|
||||
});
|
||||
|
||||
box->addRow(MakeOwnerCell(box, info), st::collectibleOwnerPadding);
|
||||
|
||||
const auto text = ((type == CollectibleType::Phone)
|
||||
? tr::lng_collectible_phone_info
|
||||
: tr::lng_collectible_username_info)(
|
||||
tr::now,
|
||||
lt_date,
|
||||
TextWithEntities{ FormatDate(info.date) },
|
||||
lt_price,
|
||||
FormatPrice(info),
|
||||
tr::rich);
|
||||
const auto label = box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(box, st::collectibleInfo),
|
||||
st::collectibleInfoPadding,
|
||||
style::al_top);
|
||||
label->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
label->setMarkedText(text);
|
||||
|
||||
const auto more = box->addRow(
|
||||
object_ptr<Ui::RoundButton>(
|
||||
box,
|
||||
tr::lng_collectible_learn_more(),
|
||||
st::collectibleMore),
|
||||
st::collectibleMorePadding);
|
||||
more->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
|
||||
more->setClickedCallback([url = info.url] {
|
||||
File::OpenUrl(url);
|
||||
});
|
||||
|
||||
const auto phrase = (type == CollectibleType::Phone)
|
||||
? tr::lng_collectible_phone_copy
|
||||
: tr::lng_collectible_username_copy;
|
||||
auto owned = object_ptr<Ui::RoundButton>(
|
||||
box,
|
||||
phrase(),
|
||||
st::collectibleCopy);
|
||||
const auto copy = owned.data();
|
||||
copy->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
|
||||
copy->setClickedCallback([copyCallback] {
|
||||
copyCallback(true);
|
||||
});
|
||||
box->addButton(std::move(owned));
|
||||
|
||||
box->setNoContentMargin(true);
|
||||
const auto buttonsParent = box->verticalLayout().get();
|
||||
const auto close = Ui::CreateChild<Ui::IconButton>(
|
||||
buttonsParent,
|
||||
st::boxTitleClose);
|
||||
close->setClickedCallback([=] {
|
||||
box->closeBox();
|
||||
});
|
||||
box->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
close->moveToRight(0, 0);
|
||||
}, box->lifetime());
|
||||
|
||||
box->widthValue() | rpl::on_next([=](int width) {
|
||||
more->setFullWidth(width
|
||||
- st::collectibleMorePadding.left()
|
||||
- st::collectibleMorePadding.right());
|
||||
copy->setFullWidth(width
|
||||
- st::collectibleBox.buttonPadding.left()
|
||||
- st::collectibleBox.buttonPadding.right());
|
||||
}, box->lifetime());
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
37
Telegram/SourceFiles/ui/boxes/collectible_info_box.h
Normal file
37
Telegram/SourceFiles/ui/boxes/collectible_info_box.h
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class GenericBox;
|
||||
class DynamicImage;
|
||||
|
||||
enum class CollectibleType {
|
||||
Phone,
|
||||
Username,
|
||||
};
|
||||
|
||||
[[nodiscard]] CollectibleType DetectCollectibleType(const QString &entity);
|
||||
|
||||
struct CollectibleInfo {
|
||||
QString entity;
|
||||
QString copyText;
|
||||
std::shared_ptr<DynamicImage> ownerUserpic;
|
||||
QString ownerName;
|
||||
uint64 cryptoAmount = 0;
|
||||
uint64 amount = 0;
|
||||
QString cryptoCurrency;
|
||||
QString currency;
|
||||
QString url;
|
||||
TimeId date = 0;
|
||||
};
|
||||
|
||||
void CollectibleInfoBox(not_null<Ui::GenericBox*> box, CollectibleInfo info);
|
||||
|
||||
} // namespace Ui
|
||||
156
Telegram/SourceFiles/ui/boxes/confirm_box.cpp
Normal file
156
Telegram/SourceFiles/ui/boxes/confirm_box.cpp
Normal file
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
void ConfirmBox(not_null<Ui::GenericBox*> box, ConfirmBoxArgs &&args) {
|
||||
const auto weak = base::make_weak(box);
|
||||
const auto lifetime = box->lifetime().make_state<rpl::lifetime>();
|
||||
|
||||
const auto withTitle = !v::is_null(args.title);
|
||||
if (withTitle) {
|
||||
box->setTitle(v::text::take_marked(std::move(args.title)));
|
||||
}
|
||||
|
||||
if (!v::is_null(args.text)) {
|
||||
const auto padding = st::boxPadding;
|
||||
const auto use = args.labelPadding
|
||||
? *args.labelPadding
|
||||
: withTitle
|
||||
? QMargins(padding.left(), 0, padding.right(), padding.bottom())
|
||||
: padding;
|
||||
const auto label = box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
box.get(),
|
||||
v::text::take_marked(std::move(args.text)),
|
||||
args.labelStyle ? *args.labelStyle : st::boxLabel),
|
||||
use);
|
||||
if (args.labelFilter) {
|
||||
label->setClickHandlerFilter(std::move(args.labelFilter));
|
||||
}
|
||||
}
|
||||
|
||||
const auto prepareCallback = [&](ConfirmBoxArgs::Callback &callback) {
|
||||
return [=, confirmed = std::move(callback)]() {
|
||||
if (const auto callbackPtr = std::get_if<1>(&confirmed)) {
|
||||
if (auto callback = (*callbackPtr)) {
|
||||
callback();
|
||||
}
|
||||
} else if (const auto callbackPtr = std::get_if<2>(&confirmed)) {
|
||||
if (auto callback = (*callbackPtr)) {
|
||||
callback(crl::guard(weak, [=] { weak->closeBox(); }));
|
||||
}
|
||||
} else if (weak) {
|
||||
weak->closeBox();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const auto &defaultButtonStyle = box->getDelegate()->style().button;
|
||||
const auto confirmTextPlain = v::is_null(args.confirmText)
|
||||
|| v::is<rpl::producer<QString>>(args.confirmText)
|
||||
|| v::is<QString>(args.confirmText);
|
||||
const auto confirmButton = box->addButton(
|
||||
(confirmTextPlain
|
||||
? v::text::take_plain(
|
||||
std::move(args.confirmText),
|
||||
tr::lng_box_ok())
|
||||
: rpl::single(QString())),
|
||||
[=, c = prepareCallback(args.confirmed)]() {
|
||||
lifetime->destroy();
|
||||
c();
|
||||
},
|
||||
args.confirmStyle ? *args.confirmStyle : defaultButtonStyle);
|
||||
if (!confirmTextPlain) {
|
||||
confirmButton->setText(
|
||||
v::text::take_marked(std::move(args.confirmText)));
|
||||
}
|
||||
|
||||
box->events(
|
||||
) | rpl::on_next([=](not_null<QEvent*> e) {
|
||||
if ((e->type() != QEvent::KeyPress) || !confirmButton) {
|
||||
return;
|
||||
}
|
||||
const auto k = static_cast<QKeyEvent*>(e.get());
|
||||
if (k->key() == Qt::Key_Enter || k->key() == Qt::Key_Return) {
|
||||
confirmButton->clicked(Qt::KeyboardModifiers(), Qt::LeftButton);
|
||||
}
|
||||
}, box->lifetime());
|
||||
|
||||
if (!args.inform) {
|
||||
const auto cancelButton = box->addButton(
|
||||
v::text::take_plain(std::move(args.cancelText), tr::lng_cancel()),
|
||||
crl::guard(weak, [=, c = prepareCallback(args.cancelled)]() {
|
||||
lifetime->destroy();
|
||||
c();
|
||||
}),
|
||||
args.cancelStyle ? *args.cancelStyle : defaultButtonStyle);
|
||||
|
||||
box->boxClosing(
|
||||
) | rpl::on_next(crl::guard(cancelButton, [=] {
|
||||
cancelButton->clicked(Qt::KeyboardModifiers(), Qt::LeftButton);
|
||||
}), *lifetime);
|
||||
}
|
||||
|
||||
if (args.strictCancel) {
|
||||
lifetime->destroy();
|
||||
}
|
||||
}
|
||||
|
||||
object_ptr<Ui::GenericBox> MakeConfirmBox(ConfirmBoxArgs &&args) {
|
||||
return Box(ConfirmBox, std::move(args));
|
||||
}
|
||||
|
||||
void IconWithTitle(
|
||||
not_null<VerticalLayout*> container,
|
||||
not_null<RpWidget*> icon,
|
||||
not_null<RpWidget*> title,
|
||||
RpWidget *subtitle) {
|
||||
const auto line = container->add(
|
||||
object_ptr<RpWidget>(container),
|
||||
st::boxRowPadding);
|
||||
icon->setParent(line);
|
||||
title->setParent(line);
|
||||
if (subtitle) {
|
||||
subtitle->setParent(line);
|
||||
}
|
||||
|
||||
icon->heightValue(
|
||||
) | rpl::on_next([=](int height) {
|
||||
line->resize(line->width(), height);
|
||||
}, icon->lifetime());
|
||||
|
||||
line->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
icon->moveToLeft(0, 0);
|
||||
const auto skip = st::defaultBoxCheckbox.textPosition.x();
|
||||
title->resizeToWidth(width - rect::right(icon) - skip);
|
||||
if (subtitle) {
|
||||
subtitle->resizeToWidth(title->width());
|
||||
title->moveToLeft(rect::right(icon) + skip, icon->y());
|
||||
subtitle->moveToLeft(
|
||||
title->x(),
|
||||
icon->y() + icon->height() - subtitle->height());
|
||||
} else {
|
||||
title->moveToLeft(
|
||||
rect::right(icon) + skip,
|
||||
((icon->height() - title->height()) / 2));
|
||||
}
|
||||
}, title->lifetime());
|
||||
|
||||
icon->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
title->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
69
Telegram/SourceFiles/ui/boxes/confirm_box.h
Normal file
69
Telegram/SourceFiles/ui/boxes/confirm_box.h
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/text/text_variant.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
struct ConfirmBoxArgs {
|
||||
using Callback = std::variant<
|
||||
v::null_t,
|
||||
Fn<void()>,
|
||||
Fn<void(Fn<void()>)>>;
|
||||
|
||||
v::text::data text = v::null;
|
||||
Callback confirmed = v::null;
|
||||
Callback cancelled = v::null;
|
||||
|
||||
v::text::data confirmText;
|
||||
v::text::data cancelText;
|
||||
|
||||
const style::RoundButton *confirmStyle = nullptr;
|
||||
const style::RoundButton *cancelStyle = nullptr;
|
||||
|
||||
const style::FlatLabel *labelStyle = nullptr;
|
||||
Fn<bool(const ClickHandlerPtr&, Qt::MouseButton)> labelFilter;
|
||||
std::optional<QMargins> labelPadding;
|
||||
|
||||
v::text::data title = v::null;
|
||||
|
||||
bool inform = false;
|
||||
// If strict cancel is set the cancel.callback() is only called
|
||||
// if the cancel button was pressed.
|
||||
bool strictCancel = false;
|
||||
};
|
||||
|
||||
void ConfirmBox(not_null<GenericBox*> box, ConfirmBoxArgs &&args);
|
||||
|
||||
inline void InformBox(not_null<GenericBox*> box, ConfirmBoxArgs &&args) {
|
||||
args.inform = true;
|
||||
ConfirmBox(box, std::move(args));
|
||||
}
|
||||
|
||||
[[nodiscard]] object_ptr<GenericBox> MakeConfirmBox(ConfirmBoxArgs &&args);
|
||||
|
||||
[[nodiscard]] inline object_ptr<GenericBox> MakeInformBox(
|
||||
ConfirmBoxArgs &&args) {
|
||||
args.inform = true;
|
||||
return MakeConfirmBox(std::move(args));
|
||||
}
|
||||
|
||||
[[nodiscard]] inline object_ptr<GenericBox> MakeInformBox(
|
||||
v::text::data text) {
|
||||
return MakeInformBox({ .text = std::move(text) });
|
||||
}
|
||||
|
||||
void IconWithTitle(
|
||||
not_null<VerticalLayout*> container,
|
||||
not_null<RpWidget*> icon,
|
||||
not_null<RpWidget*> title,
|
||||
RpWidget *subtitle = nullptr);
|
||||
|
||||
} // namespace Ui
|
||||
192
Telegram/SourceFiles/ui/boxes/confirm_phone_box.cpp
Normal file
192
Telegram/SourceFiles/ui/boxes/confirm_phone_box.cpp
Normal file
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/boxes/confirm_phone_box.h"
|
||||
|
||||
#include "core/file_utilities.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/text/format_values.h" // Ui::FormatPhone
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_boxes.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
ConfirmPhoneBox::ConfirmPhoneBox(
|
||||
QWidget*,
|
||||
const QString &phone,
|
||||
int codeLength,
|
||||
const QString &openUrl,
|
||||
std::optional<int> timeout)
|
||||
: _phone(phone)
|
||||
, _sentCodeLength(codeLength)
|
||||
, _call([=] { sendCall(); }, [=] { update(); }) {
|
||||
if (!openUrl.isEmpty()) {
|
||||
_fragment.create(
|
||||
this,
|
||||
tr::lng_intro_fragment_button(),
|
||||
st::fragmentBoxButton);
|
||||
_fragment->setClickedCallback([=] { File::OpenUrl(openUrl); });
|
||||
_fragment->setTextTransform(
|
||||
Ui::RoundButton::TextTransform::NoTransform);
|
||||
}
|
||||
if (timeout) {
|
||||
_call.setStatus({ Ui::SentCodeCall::State::Waiting, *timeout });
|
||||
}
|
||||
}
|
||||
|
||||
void ConfirmPhoneBox::sendCall() {
|
||||
_resendRequests.fire({});
|
||||
}
|
||||
|
||||
void ConfirmPhoneBox::prepare() {
|
||||
_about.create(
|
||||
this,
|
||||
tr::lng_confirm_phone_about(
|
||||
lt_phone,
|
||||
rpl::single(tr::bold(Ui::FormatPhone(_phone))),
|
||||
tr::marked),
|
||||
st::confirmPhoneAboutLabel);
|
||||
|
||||
_code.create(this, st::confirmPhoneCodeField, tr::lng_code_ph());
|
||||
_code->setAutoSubmit(_sentCodeLength, [=] { sendCode(); });
|
||||
_code->setChangedCallback([=] { showError(QString()); });
|
||||
|
||||
setTitle(tr::lng_confirm_phone_title());
|
||||
|
||||
addButton(tr::lng_confirm_phone_send(), [=] { sendCode(); });
|
||||
addButton(tr::lng_cancel(), [=] { closeBox(); });
|
||||
|
||||
setDimensions(
|
||||
st::boxWidth,
|
||||
st::usernamePadding.top()
|
||||
+ _code->height()
|
||||
+ st::usernameSkip
|
||||
+ _about->height()
|
||||
+ st::usernameSkip
|
||||
+ (_fragment ? (_fragment->height() + fragmentSkip()) : 0));
|
||||
|
||||
_code->submits(
|
||||
) | rpl::on_next([=] { sendCode(); }, _code->lifetime());
|
||||
|
||||
showChildren();
|
||||
}
|
||||
|
||||
void ConfirmPhoneBox::sendCode() {
|
||||
if (_isWaitingCheck) {
|
||||
return;
|
||||
}
|
||||
const auto code = _code->getDigitsOnly();
|
||||
if (code.isEmpty()) {
|
||||
_code->showError();
|
||||
return;
|
||||
}
|
||||
|
||||
_code->setDisabled(true);
|
||||
setFocus();
|
||||
|
||||
showError(QString());
|
||||
|
||||
_checkRequests.fire_copy(code);
|
||||
_isWaitingCheck = true;
|
||||
}
|
||||
|
||||
void ConfirmPhoneBox::showError(const QString &error) {
|
||||
_error = error;
|
||||
if (!_error.isEmpty()) {
|
||||
_code->showError();
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void ConfirmPhoneBox::paintEvent(QPaintEvent *e) {
|
||||
BoxContent::paintEvent(e);
|
||||
|
||||
auto p = QPainter(this);
|
||||
|
||||
p.setFont(st::boxTextFont);
|
||||
const auto callText = _call.getText();
|
||||
if (!callText.isEmpty()) {
|
||||
p.setPen(st::usernameDefaultFg);
|
||||
const auto callTextRect = QRect(
|
||||
st::usernamePadding.left(),
|
||||
_about->y() + _about->height(),
|
||||
width() - 2 * st::usernamePadding.left(),
|
||||
st::usernameSkip);
|
||||
p.drawText(callTextRect, callText, style::al_left);
|
||||
}
|
||||
auto errorText = _error;
|
||||
if (errorText.isEmpty()) {
|
||||
p.setPen(st::usernameDefaultFg);
|
||||
errorText = tr::lng_confirm_phone_enter_code(tr::now);
|
||||
} else {
|
||||
p.setPen(st::boxTextFgError);
|
||||
}
|
||||
const auto errorTextRect = QRect(
|
||||
st::usernamePadding.left(),
|
||||
_code->y() + _code->height(),
|
||||
width() - 2 * st::usernamePadding.left(),
|
||||
st::usernameSkip);
|
||||
p.drawText(errorTextRect, errorText, style::al_left);
|
||||
}
|
||||
|
||||
void ConfirmPhoneBox::resizeEvent(QResizeEvent *e) {
|
||||
BoxContent::resizeEvent(e);
|
||||
|
||||
_code->resize(
|
||||
width() - st::usernamePadding.left() - st::usernamePadding.right(),
|
||||
_code->height());
|
||||
_code->moveToLeft(st::usernamePadding.left(), st::usernamePadding.top());
|
||||
|
||||
if (_fragment) {
|
||||
_fragment->setFullWidth(_code->width());
|
||||
_fragment->moveToLeft(
|
||||
(width() - _fragment->width()) / 2,
|
||||
_code->y() + _code->height() + st::usernameSkip);
|
||||
}
|
||||
|
||||
const auto aboutTop = _fragment
|
||||
? (_fragment->y() + _fragment->height() + fragmentSkip())
|
||||
: (_code->y() + _code->height() + st::usernameSkip);
|
||||
_about->moveToLeft(st::usernamePadding.left(), aboutTop);
|
||||
}
|
||||
|
||||
void ConfirmPhoneBox::setInnerFocus() {
|
||||
_code->setFocusFast();
|
||||
}
|
||||
|
||||
int ConfirmPhoneBox::fragmentSkip() const {
|
||||
return st::usernamePadding.bottom();
|
||||
}
|
||||
|
||||
rpl::producer<QString> ConfirmPhoneBox::checkRequests() const {
|
||||
return _checkRequests.events();
|
||||
}
|
||||
|
||||
rpl::producer<> ConfirmPhoneBox::resendRequests() const {
|
||||
return _resendRequests.events();
|
||||
}
|
||||
|
||||
void ConfirmPhoneBox::callDone() {
|
||||
_call.callDone();
|
||||
}
|
||||
|
||||
void ConfirmPhoneBox::showServerError(const QString &text) {
|
||||
_isWaitingCheck = false;
|
||||
_code->setDisabled(false);
|
||||
_code->setFocus();
|
||||
showError(text);
|
||||
}
|
||||
|
||||
QString ConfirmPhoneBox::getPhone() const {
|
||||
return _phone;
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
71
Telegram/SourceFiles/ui/boxes/confirm_phone_box.h
Normal file
71
Telegram/SourceFiles/ui/boxes/confirm_phone_box.h
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/layers/box_content.h"
|
||||
#include "ui/widgets/sent_code_field.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class FlatLabel;
|
||||
class RoundButton;
|
||||
|
||||
class ConfirmPhoneBox final : public Ui::BoxContent {
|
||||
public:
|
||||
ConfirmPhoneBox(
|
||||
QWidget*,
|
||||
const QString &phone,
|
||||
int codeLength,
|
||||
const QString &openUrl,
|
||||
std::optional<int> timeout);
|
||||
|
||||
[[nodiscard]] rpl::producer<QString> checkRequests() const;
|
||||
[[nodiscard]] rpl::producer<> resendRequests() const;
|
||||
|
||||
void callDone();
|
||||
void showServerError(const QString &text);
|
||||
|
||||
protected:
|
||||
void prepare() override;
|
||||
void setInnerFocus() override;
|
||||
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
private:
|
||||
void sendCode();
|
||||
void sendCall();
|
||||
void checkPhoneAndHash();
|
||||
|
||||
[[nodiscard]] int fragmentSkip() const;
|
||||
|
||||
QString getPhone() const;
|
||||
void showError(const QString &error);
|
||||
|
||||
// _hash from the link for account.sendConfirmPhoneCode call.
|
||||
// _phoneHash from auth.sentCode for account.confirmPhone call.
|
||||
const QString _phone;
|
||||
|
||||
// If we receive the code length, we autosubmit _code field when enough symbols is typed.
|
||||
const int _sentCodeLength = 0;
|
||||
|
||||
bool _isWaitingCheck = false;
|
||||
|
||||
object_ptr<Ui::FlatLabel> _about = { nullptr };
|
||||
object_ptr<Ui::SentCodeField> _code = { nullptr };
|
||||
object_ptr<Ui::RoundButton> _fragment = { nullptr };
|
||||
|
||||
QString _error;
|
||||
Ui::SentCodeCall _call;
|
||||
|
||||
rpl::event_stream<QString> _checkRequests;
|
||||
rpl::event_stream<> _resendRequests;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
487
Telegram/SourceFiles/ui/boxes/country_select_box.cpp
Normal file
487
Telegram/SourceFiles/ui/boxes/country_select_box.cpp
Normal file
@@ -0,0 +1,487 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/boxes/country_select_box.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/widgets/scroll_area.h"
|
||||
#include "ui/widgets/multi_select.h"
|
||||
#include "ui/effects/ripple_animation.h"
|
||||
#include "ui/painter.h"
|
||||
#include "countries/countries_instance.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_intro.h"
|
||||
|
||||
#include <QtCore/QRegularExpression>
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
QString LastValidISO;
|
||||
|
||||
} // namespace
|
||||
|
||||
class CountrySelectBox::Inner : public RpWidget {
|
||||
public:
|
||||
Inner(QWidget *parent, const QString &iso, Type type);
|
||||
~Inner();
|
||||
|
||||
void updateFilter(QString filter = QString());
|
||||
|
||||
void selectSkip(int32 dir);
|
||||
void selectSkipPage(int32 h, int32 dir);
|
||||
|
||||
void chooseCountry();
|
||||
|
||||
void refresh();
|
||||
|
||||
[[nodiscard]] rpl::producer<Entry> countryChosen() const {
|
||||
return _countryChosen.events();
|
||||
}
|
||||
|
||||
[[nodiscard]] rpl::producer<ScrollToRequest> mustScrollTo() const {
|
||||
return _mustScrollTo.events();
|
||||
}
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void enterEventHook(QEnterEvent *e) override;
|
||||
void leaveEventHook(QEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
|
||||
private:
|
||||
void init();
|
||||
void updateSelected() {
|
||||
updateSelected(mapFromGlobal(QCursor::pos()));
|
||||
}
|
||||
void updateSelected(QPoint localPos);
|
||||
void updateSelectedRow();
|
||||
void updateRow(int index);
|
||||
void setPressed(int pressed);
|
||||
const std::vector<Entry> ¤t() const;
|
||||
|
||||
Type _type = Type::Phones;
|
||||
int _rowHeight = 0;
|
||||
|
||||
int _selected = -1;
|
||||
int _pressed = -1;
|
||||
QString _filter;
|
||||
bool _mouseSelection = false;
|
||||
|
||||
std::vector<std::unique_ptr<RippleAnimation>> _ripples;
|
||||
|
||||
std::vector<Entry> _list;
|
||||
std::vector<Entry> _filtered;
|
||||
base::flat_map<QChar, std::vector<int>> _byLetter;
|
||||
std::vector<std::vector<QString>> _namesList;
|
||||
|
||||
rpl::event_stream<Entry> _countryChosen;
|
||||
rpl::event_stream<ScrollToRequest> _mustScrollTo;
|
||||
|
||||
};
|
||||
|
||||
CountrySelectBox::CountrySelectBox(QWidget*)
|
||||
: CountrySelectBox(nullptr, QString(), Type::Phones) {
|
||||
}
|
||||
|
||||
CountrySelectBox::CountrySelectBox(QWidget*, const QString &iso, Type type)
|
||||
: _select(this, st::defaultMultiSelect, tr::lng_country_ph())
|
||||
, _ownedInner(this, iso, type) {
|
||||
}
|
||||
|
||||
rpl::producer<QString> CountrySelectBox::countryChosen() const {
|
||||
Expects(_ownedInner != nullptr || _inner != nullptr);
|
||||
|
||||
return (_ownedInner
|
||||
? _ownedInner.data()
|
||||
: _inner.data())->countryChosen() | rpl::map([](const Entry &e) {
|
||||
return e.iso2;
|
||||
});
|
||||
}
|
||||
|
||||
rpl::producer<CountrySelectBox::Entry> CountrySelectBox::entryChosen() const {
|
||||
Expects(_ownedInner != nullptr || _inner != nullptr);
|
||||
|
||||
return (_ownedInner
|
||||
? _ownedInner.data()
|
||||
: _inner.data())->countryChosen();
|
||||
}
|
||||
|
||||
void CountrySelectBox::prepare() {
|
||||
setTitle(tr::lng_country_select());
|
||||
|
||||
_select->resizeToWidth(st::boxWidth);
|
||||
_select->setQueryChangedCallback([=](const QString &query) {
|
||||
applyFilterUpdate(query);
|
||||
});
|
||||
_select->setSubmittedCallback([=](Qt::KeyboardModifiers) {
|
||||
submit();
|
||||
});
|
||||
|
||||
_inner = setInnerWidget(
|
||||
std::move(_ownedInner),
|
||||
st::countriesScroll,
|
||||
_select->height());
|
||||
|
||||
addButton(tr::lng_close(), [=] { closeBox(); });
|
||||
|
||||
setDimensions(st::boxWidth, st::boxMaxListHeight);
|
||||
|
||||
_inner->mustScrollTo(
|
||||
) | rpl::on_next([=](ScrollToRequest request) {
|
||||
scrollToY(request.ymin, request.ymax);
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void CountrySelectBox::submit() {
|
||||
_inner->chooseCountry();
|
||||
}
|
||||
|
||||
void CountrySelectBox::keyPressEvent(QKeyEvent *e) {
|
||||
if (e->key() == Qt::Key_Down) {
|
||||
_inner->selectSkip(1);
|
||||
} else if (e->key() == Qt::Key_Up) {
|
||||
_inner->selectSkip(-1);
|
||||
} else if (e->key() == Qt::Key_PageDown) {
|
||||
_inner->selectSkipPage(height() - _select->height(), 1);
|
||||
} else if (e->key() == Qt::Key_PageUp) {
|
||||
_inner->selectSkipPage(height() - _select->height(), -1);
|
||||
} else {
|
||||
BoxContent::keyPressEvent(e);
|
||||
}
|
||||
}
|
||||
|
||||
void CountrySelectBox::resizeEvent(QResizeEvent *e) {
|
||||
BoxContent::resizeEvent(e);
|
||||
|
||||
_select->resizeToWidth(width());
|
||||
_select->moveToLeft(0, 0);
|
||||
|
||||
_inner->resizeToWidth(width());
|
||||
}
|
||||
|
||||
void CountrySelectBox::applyFilterUpdate(const QString &query) {
|
||||
scrollToY(0);
|
||||
_inner->updateFilter(query);
|
||||
}
|
||||
|
||||
void CountrySelectBox::setInnerFocus() {
|
||||
_select->setInnerFocus();
|
||||
}
|
||||
|
||||
CountrySelectBox::Inner::Inner(
|
||||
QWidget *parent,
|
||||
const QString &iso,
|
||||
Type type)
|
||||
: RpWidget(parent)
|
||||
, _type(type)
|
||||
, _rowHeight(st::countryRowHeight) {
|
||||
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
|
||||
const auto &byISO2 = Countries::Instance().byISO2();
|
||||
|
||||
if (byISO2.contains(iso)) {
|
||||
LastValidISO = iso;
|
||||
}
|
||||
|
||||
rpl::single(
|
||||
) | rpl::then(
|
||||
Countries::Instance().updated()
|
||||
) | rpl::on_next([=] {
|
||||
_mustScrollTo.fire(ScrollToRequest(0, 0));
|
||||
_list.clear();
|
||||
_namesList.clear();
|
||||
init();
|
||||
const auto filter = _filter;
|
||||
_filter = u"a"_q;
|
||||
updateFilter(filter);
|
||||
}, lifetime());
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::init() {
|
||||
const auto &byISO2 = Countries::Instance().byISO2();
|
||||
|
||||
const auto extractEntries = [&](const Countries::Info &info) {
|
||||
for (const auto &code : info.codes) {
|
||||
_list.push_back(Entry{
|
||||
.country = info.name,
|
||||
.iso2 = info.iso2,
|
||||
.code = code.callingCode,
|
||||
.alternativeName = info.alternativeName,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_list.reserve(byISO2.size());
|
||||
_namesList.reserve(byISO2.size());
|
||||
|
||||
const auto l = byISO2.constFind(LastValidISO);
|
||||
const auto lastValid = (l != byISO2.cend()) ? (*l) : nullptr;
|
||||
if (lastValid) {
|
||||
extractEntries(*lastValid);
|
||||
}
|
||||
for (const auto &entry : Countries::Instance().list()) {
|
||||
if (&entry != lastValid) {
|
||||
extractEntries(entry);
|
||||
}
|
||||
}
|
||||
auto index = 0;
|
||||
for (const auto &info : _list) {
|
||||
static const auto RegExp = QRegularExpression("[\\s\\-]");
|
||||
auto full = info.country
|
||||
+ ' '
|
||||
+ (!info.alternativeName.isEmpty()
|
||||
? info.alternativeName
|
||||
: QString());
|
||||
const auto namesList = std::move(full).toLower().split(
|
||||
RegExp,
|
||||
Qt::SkipEmptyParts);
|
||||
auto &names = _namesList.emplace_back();
|
||||
names.reserve(namesList.size());
|
||||
for (const auto &name : namesList) {
|
||||
const auto part = name.trimmed();
|
||||
if (part.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto ch = part[0];
|
||||
auto &byLetter = _byLetter[ch];
|
||||
if (byLetter.empty() || byLetter.back() != index) {
|
||||
byLetter.push_back(index);
|
||||
}
|
||||
names.push_back(part);
|
||||
}
|
||||
++index;
|
||||
}
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::paintEvent(QPaintEvent *e) {
|
||||
Painter p(this);
|
||||
QRect r(e->rect());
|
||||
p.setClipRect(r);
|
||||
|
||||
const auto &list = current();
|
||||
if (list.empty()) {
|
||||
p.fillRect(r, st::boxBg);
|
||||
p.setFont(st::noContactsFont);
|
||||
p.setPen(st::noContactsColor);
|
||||
p.drawText(QRect(0, 0, width(), st::noContactsHeight), tr::lng_country_none(tr::now), style::al_center);
|
||||
return;
|
||||
}
|
||||
const auto l = int(list.size());
|
||||
if (r.intersects(QRect(0, 0, width(), st::countriesSkip))) {
|
||||
p.fillRect(r.intersected(QRect(0, 0, width(), st::countriesSkip)), st::countryRowBg);
|
||||
}
|
||||
int32 from = std::clamp((r.y() - st::countriesSkip) / _rowHeight, 0, l);
|
||||
int32 to = std::clamp((r.y() + r.height() - st::countriesSkip + _rowHeight - 1) / _rowHeight, 0, l);
|
||||
for (int32 i = from; i < to; ++i) {
|
||||
auto selected = (i == (_pressed >= 0 ? _pressed : _selected));
|
||||
auto y = st::countriesSkip + i * _rowHeight;
|
||||
|
||||
p.fillRect(0, y, width(), _rowHeight, selected ? st::countryRowBgOver : st::countryRowBg);
|
||||
if (_ripples.size() > i && _ripples[i]) {
|
||||
_ripples[i]->paint(p, 0, y, width());
|
||||
if (_ripples[i]->empty()) {
|
||||
_ripples[i].reset();
|
||||
}
|
||||
}
|
||||
|
||||
auto code = QString("+") + list[i].code;
|
||||
auto codeWidth = st::countryRowCodeFont->width(code);
|
||||
|
||||
auto name = list[i].country;
|
||||
auto nameWidth = st::countryRowNameFont->width(name);
|
||||
auto availWidth = width() - st::countryRowPadding.left() - st::countryRowPadding.right() - codeWidth - st::boxScroll.width;
|
||||
if (nameWidth > availWidth) {
|
||||
name = st::countryRowNameFont->elided(name, availWidth);
|
||||
nameWidth = st::countryRowNameFont->width(name);
|
||||
}
|
||||
|
||||
p.setFont(st::countryRowNameFont);
|
||||
p.setPen(st::countryRowNameFg);
|
||||
p.drawTextLeft(st::countryRowPadding.left(), y + st::countryRowPadding.top(), width(), name);
|
||||
|
||||
if (_type == Type::Phones) {
|
||||
p.setFont(st::countryRowCodeFont);
|
||||
p.setPen(selected ? st::countryRowCodeFgOver : st::countryRowCodeFg);
|
||||
p.drawTextLeft(st::countryRowPadding.left() + nameWidth + st::countryRowPadding.right(), y + st::countryRowPadding.top(), width(), code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::enterEventHook(QEnterEvent *e) {
|
||||
setMouseTracking(true);
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::leaveEventHook(QEvent *e) {
|
||||
_mouseSelection = false;
|
||||
setMouseTracking(false);
|
||||
if (_selected >= 0) {
|
||||
updateSelectedRow();
|
||||
_selected = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::mouseMoveEvent(QMouseEvent *e) {
|
||||
_mouseSelection = true;
|
||||
updateSelected(e->pos());
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::mousePressEvent(QMouseEvent *e) {
|
||||
_mouseSelection = true;
|
||||
updateSelected(e->pos());
|
||||
|
||||
setPressed(_selected);
|
||||
const auto &list = current();
|
||||
if (_pressed >= 0 && _pressed < list.size()) {
|
||||
if (_ripples.size() <= _pressed) {
|
||||
_ripples.reserve(_pressed + 1);
|
||||
while (_ripples.size() <= _pressed) {
|
||||
_ripples.push_back(nullptr);
|
||||
}
|
||||
}
|
||||
if (!_ripples[_pressed]) {
|
||||
auto mask = RippleAnimation::RectMask(QSize(width(), _rowHeight));
|
||||
_ripples[_pressed] = std::make_unique<RippleAnimation>(st::countryRipple, std::move(mask), [this, index = _pressed] {
|
||||
updateRow(index);
|
||||
});
|
||||
_ripples[_pressed]->add(e->pos() - QPoint(0, st::countriesSkip + _pressed * _rowHeight));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::mouseReleaseEvent(QMouseEvent *e) {
|
||||
auto pressed = _pressed;
|
||||
setPressed(-1);
|
||||
updateSelectedRow();
|
||||
if (e->button() == Qt::LeftButton) {
|
||||
if ((pressed >= 0) && pressed == _selected) {
|
||||
chooseCountry();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::updateFilter(QString filter) {
|
||||
const auto words = TextUtilities::PrepareSearchWords(filter);
|
||||
filter = words.isEmpty() ? QString() : words.join(' ');
|
||||
if (_filter == filter) {
|
||||
return;
|
||||
}
|
||||
_filter = filter;
|
||||
|
||||
const auto findWord = [&](
|
||||
const std::vector<QString> &names,
|
||||
const QString &word) {
|
||||
for (const auto &name : names) {
|
||||
if (name.startsWith(word)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const auto hasAllWords = [&](const std::vector<QString> &names) {
|
||||
for (const auto &word : words) {
|
||||
if (!findWord(names, word)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
if (!_filter.isEmpty()) {
|
||||
_filtered.clear();
|
||||
for (const auto index : _byLetter[_filter[0].toLower()]) {
|
||||
if (hasAllWords(_namesList[index])) {
|
||||
_filtered.push_back(_list[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
refresh();
|
||||
_selected = current().empty() ? -1 : 0;
|
||||
update();
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::selectSkip(int32 dir) {
|
||||
_mouseSelection = false;
|
||||
|
||||
const auto &list = current();
|
||||
int cur = (_selected >= 0) ? _selected : -1;
|
||||
cur += dir;
|
||||
if (cur <= 0) {
|
||||
_selected = list.empty() ? -1 : 0;
|
||||
} else if (cur >= list.size()) {
|
||||
_selected = -1;
|
||||
} else {
|
||||
_selected = cur;
|
||||
}
|
||||
if (_selected >= 0) {
|
||||
_mustScrollTo.fire(ScrollToRequest(
|
||||
st::countriesSkip + _selected * _rowHeight,
|
||||
st::countriesSkip + (_selected + 1) * _rowHeight));
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::selectSkipPage(int32 h, int32 dir) {
|
||||
int32 points = h / _rowHeight;
|
||||
if (!points) return;
|
||||
selectSkip(points * dir);
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::chooseCountry() {
|
||||
const auto &list = current();
|
||||
_countryChosen.fire_copy((_selected >= 0 && _selected < list.size())
|
||||
? list[_selected]
|
||||
: Entry());
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::refresh() {
|
||||
const auto &list = current();
|
||||
resize(width(), list.empty() ? st::noContactsHeight : (list.size() * _rowHeight + st::countriesSkip));
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::updateSelected(QPoint localPos) {
|
||||
if (!_mouseSelection) return;
|
||||
|
||||
auto in = parentWidget()->rect().contains(parentWidget()->mapFromGlobal(QCursor::pos()));
|
||||
|
||||
const auto &list = current();
|
||||
auto selected = (in && localPos.y() >= st::countriesSkip && localPos.y() < st::countriesSkip + list.size() * _rowHeight) ? ((localPos.y() - st::countriesSkip) / _rowHeight) : -1;
|
||||
if (_selected != selected) {
|
||||
updateSelectedRow();
|
||||
_selected = selected;
|
||||
updateSelectedRow();
|
||||
}
|
||||
}
|
||||
|
||||
auto CountrySelectBox::Inner::current() const
|
||||
-> const std::vector<CountrySelectBox::Entry> & {
|
||||
return _filter.isEmpty() ? _list : _filtered;
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::updateSelectedRow() {
|
||||
updateRow(_selected);
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::updateRow(int index) {
|
||||
if (index >= 0) {
|
||||
update(0, st::countriesSkip + index * _rowHeight, width(), _rowHeight);
|
||||
}
|
||||
}
|
||||
|
||||
void CountrySelectBox::Inner::setPressed(int pressed) {
|
||||
if (_pressed >= 0 && _pressed < _ripples.size() && _ripples[_pressed]) {
|
||||
_ripples[_pressed]->lastStop();
|
||||
}
|
||||
_pressed = pressed;
|
||||
}
|
||||
|
||||
CountrySelectBox::Inner::~Inner() = default;
|
||||
|
||||
} // namespace Ui
|
||||
59
Telegram/SourceFiles/ui/boxes/country_select_box.h
Normal file
59
Telegram/SourceFiles/ui/boxes/country_select_box.h
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/layers/box_content.h"
|
||||
|
||||
namespace Countries {
|
||||
struct Info;
|
||||
} // namespace Countries
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class MultiSelect;
|
||||
class RippleAnimation;
|
||||
|
||||
class CountrySelectBox : public BoxContent {
|
||||
public:
|
||||
enum class Type {
|
||||
Phones,
|
||||
Countries,
|
||||
};
|
||||
struct Entry {
|
||||
QString country;
|
||||
QString iso2;
|
||||
QString code;
|
||||
QString alternativeName;
|
||||
};
|
||||
|
||||
CountrySelectBox(QWidget*);
|
||||
CountrySelectBox(QWidget*, const QString &iso, Type type);
|
||||
|
||||
[[nodiscard]] rpl::producer<QString> countryChosen() const;
|
||||
[[nodiscard]] rpl::producer<Entry> entryChosen() const;
|
||||
|
||||
protected:
|
||||
void prepare() override;
|
||||
void setInnerFocus() override;
|
||||
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
private:
|
||||
void submit();
|
||||
void applyFilterUpdate(const QString &query);
|
||||
|
||||
object_ptr<MultiSelect> _select;
|
||||
|
||||
class Inner;
|
||||
object_ptr<Inner> _ownedInner;
|
||||
QPointer<Inner> _inner;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
218
Telegram/SourceFiles/ui/boxes/edit_birthday_box.cpp
Normal file
218
Telegram/SourceFiles/ui/boxes/edit_birthday_box.cpp
Normal file
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/boxes/edit_birthday_box.h"
|
||||
|
||||
#include "base/event_filter.h"
|
||||
#include "data/data_birthday.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/widgets/vertical_drum_picker.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_settings.h"
|
||||
|
||||
#include <QtCore/QDate>
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class GenericBox;
|
||||
|
||||
void EditBirthdayBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
Data::Birthday current,
|
||||
Fn<void(Data::Birthday)> save,
|
||||
EditBirthdayType type) {
|
||||
box->setWidth(st::boxWideWidth);
|
||||
const auto content = box->addRow(object_ptr<Ui::FixedHeightWidget>(
|
||||
box,
|
||||
st::settingsWorkingHoursPicker));
|
||||
|
||||
const auto font = st::boxTextFont;
|
||||
const auto itemHeight = st::settingsWorkingHoursPickerItemHeight;
|
||||
const auto picker = [=](
|
||||
int count,
|
||||
int startIndex,
|
||||
Fn<void(QPainter &p, QRectF rect, int index)> paint) {
|
||||
return Ui::CreateChild<Ui::VerticalDrumPicker>(
|
||||
content,
|
||||
Ui::VerticalDrumPicker::DefaultPaintCallback(
|
||||
font,
|
||||
itemHeight,
|
||||
paint),
|
||||
count,
|
||||
itemHeight,
|
||||
startIndex);
|
||||
};
|
||||
|
||||
const auto nowDate = QDate::currentDate();
|
||||
const auto nowYear = nowDate.year();
|
||||
const auto nowMonth = nowDate.month();
|
||||
const auto nowDay = nowDate.day();
|
||||
const auto now = Data::Birthday(nowDay, nowMonth, nowYear);
|
||||
const auto max = current.year() ? std::max(now, current) : now;
|
||||
const auto maxYear = max.year();
|
||||
const auto minYear = Data::Birthday::kYearMin;
|
||||
const auto yearsCount = (maxYear - minYear + 2); // Last - not set.
|
||||
const auto yearsStartIndex = current.year()
|
||||
? (current.year() - minYear)
|
||||
: (yearsCount - 1);
|
||||
const auto yearsPaint = [=](QPainter &p, QRectF rect, int index) {
|
||||
p.drawText(
|
||||
rect,
|
||||
(index < yearsCount - 1
|
||||
? QString::number(minYear + index)
|
||||
: QString::fromUtf8("\xe2\x80\x94")),
|
||||
style::al_center);
|
||||
};
|
||||
const auto years = picker(yearsCount, yearsStartIndex, yearsPaint);
|
||||
|
||||
struct State {
|
||||
rpl::variable<Ui::VerticalDrumPicker*> months;
|
||||
rpl::variable<Ui::VerticalDrumPicker*> days;
|
||||
};
|
||||
const auto state = content->lifetime().make_state<State>();
|
||||
|
||||
// years->value() is valid only after size is set.
|
||||
rpl::combine(
|
||||
content->sizeValue(),
|
||||
state->months.value(),
|
||||
state->days.value()
|
||||
) | rpl::on_next([=](
|
||||
QSize s,
|
||||
Ui::VerticalDrumPicker *months,
|
||||
Ui::VerticalDrumPicker *days) {
|
||||
const auto half = s.width() / 2;
|
||||
years->setGeometry(half * 3 / 2, 0, half / 2, s.height());
|
||||
if (months) {
|
||||
months->setGeometry(half / 2, 0, half, s.height());
|
||||
}
|
||||
if (days) {
|
||||
days->setGeometry(0, 0, half / 2, s.height());
|
||||
}
|
||||
}, content->lifetime());
|
||||
|
||||
Ui::SendPendingMoveResizeEvents(years);
|
||||
|
||||
years->value() | rpl::on_next([=](int yearsIndex) {
|
||||
const auto year = (yearsIndex == yearsCount - 1)
|
||||
? 0
|
||||
: minYear + yearsIndex;
|
||||
const auto monthsCount = (year == maxYear)
|
||||
? max.month()
|
||||
: 12;
|
||||
const auto monthsStartIndex = std::clamp(
|
||||
(state->months.current()
|
||||
? state->months.current()->index()
|
||||
: current.month()
|
||||
? (current.month() - 1)
|
||||
: (now.month() - 1)),
|
||||
0,
|
||||
monthsCount - 1);
|
||||
const auto monthsPaint = [=](QPainter &p, QRectF rect, int index) {
|
||||
p.drawText(
|
||||
rect,
|
||||
Lang::Month(index + 1)(tr::now),
|
||||
style::al_center);
|
||||
};
|
||||
const auto updated = picker(
|
||||
monthsCount,
|
||||
monthsStartIndex,
|
||||
monthsPaint);
|
||||
delete state->months.current();
|
||||
state->months = updated;
|
||||
state->months.current()->show();
|
||||
}, years->lifetime());
|
||||
|
||||
Ui::SendPendingMoveResizeEvents(state->months.current());
|
||||
|
||||
state->months.value() | rpl::map([=](Ui::VerticalDrumPicker *picker) {
|
||||
return picker ? picker->value() : rpl::single(current.month()
|
||||
? (current.month() - 1)
|
||||
: (now.month() - 1));
|
||||
}) | rpl::flatten_latest() | rpl::on_next([=](int monthIndex) {
|
||||
const auto month = monthIndex + 1;
|
||||
const auto yearsIndex = years->index();
|
||||
const auto year = (yearsIndex == yearsCount - 1)
|
||||
? 0
|
||||
: minYear + yearsIndex;
|
||||
const auto daysCount = (year == maxYear && month == max.month())
|
||||
? max.day()
|
||||
: (month == 2)
|
||||
? ((!year || (!(year % 4) && ((year % 100) || !(year % 400))))
|
||||
? 29
|
||||
: 28)
|
||||
: ((month == 4) || (month == 6) || (month == 9) || (month == 11))
|
||||
? 30
|
||||
: 31;
|
||||
const auto daysStartIndex = std::clamp(
|
||||
(state->days.current()
|
||||
? state->days.current()->index()
|
||||
: current.day()
|
||||
? (current.day() - 1)
|
||||
: (now.day() - 1)),
|
||||
0,
|
||||
daysCount - 1);
|
||||
const auto daysPaint = [=](QPainter &p, QRectF rect, int index) {
|
||||
p.drawText(rect, QString::number(index + 1), style::al_center);
|
||||
};
|
||||
const auto updated = picker(
|
||||
daysCount,
|
||||
daysStartIndex,
|
||||
daysPaint);
|
||||
delete state->days.current();
|
||||
state->days = updated;
|
||||
state->days.current()->show();
|
||||
}, years->lifetime());
|
||||
|
||||
content->paintRequest(
|
||||
) | rpl::on_next([=](const QRect &r) {
|
||||
auto p = QPainter(content);
|
||||
|
||||
p.fillRect(r, Qt::transparent);
|
||||
|
||||
const auto lineRect = QRect(
|
||||
0,
|
||||
content->height() / 2,
|
||||
content->width(),
|
||||
st::defaultInputField.borderActive);
|
||||
p.fillRect(lineRect.translated(0, itemHeight / 2), st::activeLineFg);
|
||||
p.fillRect(lineRect.translated(0, -itemHeight / 2), st::activeLineFg);
|
||||
}, content->lifetime());
|
||||
|
||||
base::install_event_filter(box, [=](not_null<QEvent*> e) {
|
||||
if (e->type() == QEvent::KeyPress) {
|
||||
years->handleKeyEvent(static_cast<QKeyEvent*>(e.get()));
|
||||
}
|
||||
return base::EventFilterResult::Continue;
|
||||
});
|
||||
|
||||
auto confirmText = (type == EditBirthdayType::Suggest)
|
||||
? tr::lng_suggest_birthday_box_confirm()
|
||||
: tr::lng_settings_save();
|
||||
box->addButton(std::move(confirmText), [=] {
|
||||
const auto result = Data::Birthday(
|
||||
state->days.current()->index() + 1,
|
||||
state->months.current()->index() + 1,
|
||||
((years->index() == yearsCount - 1)
|
||||
? 0
|
||||
: minYear + years->index()));
|
||||
box->closeBox();
|
||||
save(result);
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] {
|
||||
box->closeBox();
|
||||
});
|
||||
if (current && type == EditBirthdayType::Edit) {
|
||||
box->addLeftButton(tr::lng_settings_birthday_reset(), [=] {
|
||||
box->closeBox();
|
||||
save(Data::Birthday());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
30
Telegram/SourceFiles/ui/boxes/edit_birthday_box.h
Normal file
30
Telegram/SourceFiles/ui/boxes/edit_birthday_box.h
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Data {
|
||||
class Birthday;
|
||||
} // namespace Data
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class GenericBox;
|
||||
|
||||
enum class EditBirthdayType {
|
||||
Edit,
|
||||
Suggest,
|
||||
ConfirmSuggestion,
|
||||
};
|
||||
|
||||
void EditBirthdayBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
Data::Birthday current,
|
||||
Fn<void(Data::Birthday)> save,
|
||||
EditBirthdayType type = EditBirthdayType::Edit);
|
||||
|
||||
} // namespace Ui
|
||||
89
Telegram/SourceFiles/ui/boxes/edit_factcheck_box.cpp
Normal file
89
Telegram/SourceFiles/ui/boxes/edit_factcheck_box.cpp
Normal file
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/boxes/edit_factcheck_box.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
void EditFactcheckBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
TextWithEntities current,
|
||||
int limit,
|
||||
Fn<void(TextWithEntities)> save,
|
||||
Fn<void(not_null<Ui::InputField*>)> initField) {
|
||||
box->setTitle(tr::lng_factcheck_title());
|
||||
|
||||
const auto field = box->addRow(object_ptr<Ui::InputField>(
|
||||
box,
|
||||
st::factcheckField,
|
||||
Ui::InputField::Mode::NoNewlines,
|
||||
tr::lng_factcheck_placeholder(),
|
||||
TextWithTags{
|
||||
current.text,
|
||||
TextUtilities::ConvertEntitiesToTextTags(current.entities)
|
||||
}));
|
||||
AddLengthLimitLabel(field, limit);
|
||||
initField(field);
|
||||
|
||||
enum class State {
|
||||
Initial,
|
||||
Changed,
|
||||
Removed,
|
||||
};
|
||||
const auto state = box->lifetime().make_state<rpl::variable<State>>(
|
||||
State::Initial);
|
||||
field->changes() | rpl::on_next([=] {
|
||||
const auto now = field->getLastText().trimmed();
|
||||
*state = !now.isEmpty()
|
||||
? State::Changed
|
||||
: current.empty()
|
||||
? State::Initial
|
||||
: State::Removed;
|
||||
}, field->lifetime());
|
||||
|
||||
state->value() | rpl::on_next([=](State state) {
|
||||
box->clearButtons();
|
||||
if (state == State::Removed) {
|
||||
box->addButton(tr::lng_box_remove(), [=] {
|
||||
box->closeBox();
|
||||
save({});
|
||||
}, st::attentionBoxButton);
|
||||
} else if (state == State::Initial) {
|
||||
box->addButton(tr::lng_settings_save(), [=] {
|
||||
if (current.empty()) {
|
||||
field->showError();
|
||||
} else {
|
||||
box->closeBox();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
box->addButton(tr::lng_settings_save(), [=] {
|
||||
auto result = field->getTextWithAppliedMarkdown();
|
||||
if (result.text.size() > limit) {
|
||||
field->showError();
|
||||
return;
|
||||
}
|
||||
|
||||
box->closeBox();
|
||||
save({
|
||||
result.text,
|
||||
TextUtilities::ConvertTextTagsToEntities(result.tags)
|
||||
});
|
||||
});
|
||||
}
|
||||
box->addButton(tr::lng_cancel(), [=] {
|
||||
box->closeBox();
|
||||
});
|
||||
}, box->lifetime());
|
||||
|
||||
box->setFocusCallback([=] {
|
||||
field->setFocusFast();
|
||||
});
|
||||
}
|
||||
21
Telegram/SourceFiles/ui/boxes/edit_factcheck_box.h
Normal file
21
Telegram/SourceFiles/ui/boxes/edit_factcheck_box.h
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/layers/generic_box.h"
|
||||
|
||||
namespace Ui {
|
||||
class InputField;
|
||||
} // namespace Ui
|
||||
|
||||
void EditFactcheckBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
TextWithEntities current,
|
||||
int limit,
|
||||
Fn<void(TextWithEntities)> save,
|
||||
Fn<void(not_null<Ui::InputField*>)> initField);
|
||||
391
Telegram/SourceFiles/ui/boxes/edit_invite_link.cpp
Normal file
391
Telegram/SourceFiles/ui/boxes/edit_invite_link.cpp
Normal file
@@ -0,0 +1,391 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/boxes/edit_invite_link.h"
|
||||
|
||||
#include "base/unixtime.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/boxes/choose_date_time.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/widgets/fields/number_input.h"
|
||||
#include "ui/effects/credits_graphics.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "styles/style_settings.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_info.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMaxLimit = std::numeric_limits<int>::max();
|
||||
constexpr auto kHour = 3600;
|
||||
constexpr auto kDay = 86400;
|
||||
constexpr auto kMaxLabelLength = 32;
|
||||
|
||||
[[nodiscard]] QString FormatExpireDate(TimeId date) {
|
||||
if (date > 0) {
|
||||
return langDateTime(base::unixtime::parse(date));
|
||||
} else if (-date < kDay) {
|
||||
return tr::lng_hours(tr::now, lt_count, (-date / kHour));
|
||||
} else if (-date < 7 * kDay) {
|
||||
return tr::lng_days(tr::now, lt_count, (-date / kDay));
|
||||
} else {
|
||||
return tr::lng_weeks(tr::now, lt_count, (-date / (7 * kDay)));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void EditInviteLinkBox(
|
||||
not_null<GenericBox*> box,
|
||||
Fn<InviteLinkSubscriptionToggle()> fillSubscription,
|
||||
const InviteLinkFields &data,
|
||||
Fn<void(InviteLinkFields)> done) {
|
||||
using namespace rpl::mappers;
|
||||
|
||||
const auto link = data.link;
|
||||
const auto isGroup = data.isGroup;
|
||||
const auto isPublic = data.isPublic;
|
||||
const auto subscriptionLocked = data.subscriptionCredits > 0;
|
||||
box->setTitle(link.isEmpty()
|
||||
? tr::lng_group_invite_new_title()
|
||||
: tr::lng_group_invite_edit_title());
|
||||
|
||||
const auto container = box->verticalLayout();
|
||||
const auto addTitle = [&](
|
||||
not_null<VerticalLayout*> container,
|
||||
rpl::producer<QString> text) {
|
||||
container->add(
|
||||
object_ptr<FlatLabel>(
|
||||
container,
|
||||
std::move(text),
|
||||
st::defaultSubsectionTitle),
|
||||
(st::defaultSubsectionTitlePadding
|
||||
+ style::margins(0, st::defaultVerticalListSkip, 0, 0)));
|
||||
};
|
||||
const auto addDivider = [&](
|
||||
not_null<VerticalLayout*> container,
|
||||
rpl::producer<QString> text,
|
||||
style::margins margins = style::margins()) {
|
||||
container->add(
|
||||
object_ptr<DividerLabel>(
|
||||
container,
|
||||
object_ptr<FlatLabel>(
|
||||
container,
|
||||
std::move(text),
|
||||
st::boxDividerLabel),
|
||||
st::defaultBoxDividerLabelPadding),
|
||||
margins);
|
||||
};
|
||||
|
||||
const auto now = base::unixtime::now();
|
||||
const auto expire = data.expireDate ? data.expireDate : kMaxLimit;
|
||||
const auto expireGroup = std::make_shared<RadiobuttonGroup>(expire);
|
||||
const auto usage = data.usageLimit ? data.usageLimit : kMaxLimit;
|
||||
const auto usageGroup = std::make_shared<RadiobuttonGroup>(usage);
|
||||
|
||||
using Buttons = base::flat_map<int, base::unique_qptr<Radiobutton>>;
|
||||
struct State {
|
||||
Buttons expireButtons;
|
||||
Buttons usageButtons;
|
||||
int expireValue = 0;
|
||||
int usageValue = 0;
|
||||
rpl::variable<bool> requestApproval = false;
|
||||
rpl::variable<bool> subscription = false;
|
||||
};
|
||||
const auto state = box->lifetime().make_state<State>(State{
|
||||
.expireValue = expire,
|
||||
.usageValue = usage,
|
||||
.requestApproval = (data.requestApproval && !isPublic),
|
||||
.subscription = false,
|
||||
});
|
||||
|
||||
const auto requestApproval = (isPublic || subscriptionLocked)
|
||||
? nullptr
|
||||
: container->add(
|
||||
object_ptr<SettingsButton>(
|
||||
container,
|
||||
tr::lng_group_invite_request_approve(),
|
||||
st::settingsButtonNoIcon),
|
||||
style::margins{ 0, 0, 0, st::defaultVerticalListSkip });
|
||||
if (requestApproval) {
|
||||
requestApproval->toggleOn(state->requestApproval.value(), true);
|
||||
requestApproval->setClickedCallback([=] {
|
||||
state->requestApproval.force_assign(!requestApproval->toggled());
|
||||
state->subscription.force_assign(false);
|
||||
});
|
||||
addDivider(container, rpl::conditional(
|
||||
state->requestApproval.value(),
|
||||
(isGroup
|
||||
? tr::lng_group_invite_about_approve()
|
||||
: tr::lng_group_invite_about_approve_channel()),
|
||||
(isGroup
|
||||
? tr::lng_group_invite_about_no_approve()
|
||||
: tr::lng_group_invite_about_no_approve_channel())));
|
||||
}
|
||||
auto credits = (Ui::NumberInput*)(nullptr);
|
||||
if (!isPublic && fillSubscription) {
|
||||
Ui::AddSkip(container);
|
||||
const auto &[subscription, input] = fillSubscription();
|
||||
credits = input.get();
|
||||
subscription->toggleOn(state->subscription.value(), true);
|
||||
if (subscriptionLocked) {
|
||||
input->setText(QString::number(data.subscriptionCredits));
|
||||
input->setReadOnly(true);
|
||||
state->subscription.force_assign(true);
|
||||
state->requestApproval.force_assign(false);
|
||||
subscription->setToggleLocked(true);
|
||||
subscription->finishAnimating();
|
||||
}
|
||||
subscription->setClickedCallback([=, show = box->uiShow()] {
|
||||
if (subscriptionLocked) {
|
||||
show->showToast(
|
||||
tr::lng_group_invite_subscription_toast(tr::now));
|
||||
return;
|
||||
}
|
||||
state->subscription.force_assign(!subscription->toggled());
|
||||
state->requestApproval.force_assign(false);
|
||||
});
|
||||
}
|
||||
|
||||
const auto labelField = container->add(
|
||||
object_ptr<Ui::InputField>(
|
||||
container,
|
||||
st::defaultInputField,
|
||||
tr::lng_group_invite_label_header(),
|
||||
data.label),
|
||||
style::margins(
|
||||
st::defaultSubsectionTitlePadding.left(),
|
||||
st::defaultVerticalListSkip,
|
||||
st::defaultSubsectionTitlePadding.right(),
|
||||
st::defaultVerticalListSkip * 2));
|
||||
labelField->setMaxLength(kMaxLabelLength);
|
||||
addDivider(container, tr::lng_group_invite_label_about());
|
||||
|
||||
const auto &saveLabel = link.isEmpty()
|
||||
? tr::lng_formatting_link_create
|
||||
: tr::lng_settings_save;
|
||||
box->addButton(saveLabel(), [=] {
|
||||
const auto label = labelField->getLastText();
|
||||
const auto expireDate = (state->expireValue == kMaxLimit)
|
||||
? 0
|
||||
: (state->expireValue < 0)
|
||||
? (base::unixtime::now() - state->expireValue)
|
||||
: state->expireValue;
|
||||
const auto usageLimit = (state->usageValue == kMaxLimit)
|
||||
? 0
|
||||
: state->usageValue;
|
||||
done(InviteLinkFields{
|
||||
.link = link,
|
||||
.label = label,
|
||||
.expireDate = expireDate,
|
||||
.usageLimit = usageLimit,
|
||||
.subscriptionCredits = credits
|
||||
? credits->getLastText().toInt()
|
||||
: 0,
|
||||
.requestApproval = state->requestApproval.current(),
|
||||
.isGroup = isGroup,
|
||||
.isPublic = isPublic,
|
||||
});
|
||||
});
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
|
||||
if (subscriptionLocked) {
|
||||
return;
|
||||
}
|
||||
addTitle(container, tr::lng_group_invite_expire_title());
|
||||
const auto expiresWrap = container->add(
|
||||
object_ptr<VerticalLayout>(container),
|
||||
style::margins(0, 0, 0, st::defaultVerticalListSkip));
|
||||
addDivider(
|
||||
container,
|
||||
tr::lng_group_invite_expire_about());
|
||||
|
||||
const auto usagesSlide = container->add(
|
||||
object_ptr<SlideWrap<VerticalLayout>>(
|
||||
container,
|
||||
object_ptr<VerticalLayout>(container)));
|
||||
const auto usagesInner = usagesSlide->entity();
|
||||
addTitle(usagesInner, tr::lng_group_invite_usage_title());
|
||||
const auto usagesWrap = usagesInner->add(
|
||||
object_ptr<VerticalLayout>(usagesInner),
|
||||
style::margins(0, 0, 0, st::defaultVerticalListSkip));
|
||||
addDivider(usagesInner, tr::lng_group_invite_usage_about());
|
||||
|
||||
static const auto addButton = [](
|
||||
not_null<VerticalLayout*> container,
|
||||
const std::shared_ptr<RadiobuttonGroup> &group,
|
||||
int value,
|
||||
const QString &text) {
|
||||
return container->add(
|
||||
object_ptr<Radiobutton>(
|
||||
container,
|
||||
group,
|
||||
value,
|
||||
text),
|
||||
st::inviteLinkLimitMargin);
|
||||
};
|
||||
|
||||
const auto regenerate = [=] {
|
||||
expireGroup->setValue(state->expireValue);
|
||||
usageGroup->setValue(state->usageValue);
|
||||
|
||||
auto expires = std::vector{ kMaxLimit, -kHour, -kDay, -kDay * 7, 0 };
|
||||
auto usages = std::vector{ kMaxLimit, 1, 10, 100, 0 };
|
||||
auto defaults = State();
|
||||
for (auto i = begin(expires); i != end(expires); ++i) {
|
||||
if (*i == state->expireValue) {
|
||||
break;
|
||||
} else if (*i == kMaxLimit) {
|
||||
continue;
|
||||
} else if (!*i || (now - *i >= state->expireValue)) {
|
||||
expires.insert(i, state->expireValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (auto i = begin(usages); i != end(usages); ++i) {
|
||||
if (*i == state->usageValue) {
|
||||
break;
|
||||
} else if (*i == kMaxLimit) {
|
||||
continue;
|
||||
} else if (!*i || *i > state->usageValue) {
|
||||
usages.insert(i, state->usageValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
state->expireButtons.clear();
|
||||
state->usageButtons.clear();
|
||||
for (const auto limit : expires) {
|
||||
const auto text = (limit == kMaxLimit)
|
||||
? tr::lng_group_invite_expire_never(tr::now)
|
||||
: !limit
|
||||
? tr::lng_group_invite_expire_custom(tr::now)
|
||||
: FormatExpireDate(limit);
|
||||
state->expireButtons.emplace(
|
||||
limit,
|
||||
addButton(expiresWrap, expireGroup, limit, text));
|
||||
}
|
||||
for (const auto limit : usages) {
|
||||
const auto text = (limit == kMaxLimit)
|
||||
? tr::lng_group_invite_usage_any(tr::now)
|
||||
: !limit
|
||||
? tr::lng_group_invite_usage_custom(tr::now)
|
||||
: Lang::FormatCountDecimal(limit);
|
||||
state->usageButtons.emplace(
|
||||
limit,
|
||||
addButton(usagesWrap, usageGroup, limit, text));
|
||||
}
|
||||
};
|
||||
|
||||
const auto guard = base::make_weak(box);
|
||||
expireGroup->setChangedCallback([=](int value) {
|
||||
if (value) {
|
||||
state->expireValue = value;
|
||||
return;
|
||||
}
|
||||
expireGroup->setValue(state->expireValue);
|
||||
box->getDelegate()->show(Box([=](not_null<GenericBox*> box) {
|
||||
const auto save = [=](TimeId result) {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
if (guard) {
|
||||
state->expireValue = result;
|
||||
regenerate();
|
||||
}
|
||||
box->closeBox();
|
||||
};
|
||||
const auto now = base::unixtime::now();
|
||||
const auto time = (state->expireValue == kMaxLimit)
|
||||
? (now + kDay)
|
||||
: (state->expireValue > now)
|
||||
? state->expireValue
|
||||
: (state->expireValue < 0)
|
||||
? (now - state->expireValue)
|
||||
: (now + kDay);
|
||||
ChooseDateTimeBox(box, {
|
||||
.title = tr::lng_group_invite_expire_after(),
|
||||
.submit = tr::lng_settings_save(),
|
||||
.done = save,
|
||||
.time = time,
|
||||
});
|
||||
}));
|
||||
});
|
||||
usageGroup->setChangedCallback([=](int value) {
|
||||
if (value) {
|
||||
state->usageValue = value;
|
||||
return;
|
||||
}
|
||||
usageGroup->setValue(state->usageValue);
|
||||
box->getDelegate()->show(Box([=](not_null<GenericBox*> box) {
|
||||
const auto height = st::boxPadding.bottom()
|
||||
+ st::defaultInputField.heightMin
|
||||
+ st::boxPadding.bottom();
|
||||
box->setTitle(tr::lng_group_invite_expire_after());
|
||||
const auto wrap = box->addRow(object_ptr<FixedHeightWidget>(
|
||||
box,
|
||||
height));
|
||||
const auto input = CreateChild<NumberInput>(
|
||||
wrap,
|
||||
st::defaultInputField,
|
||||
tr::lng_group_invite_custom_limit(),
|
||||
(state->usageValue == kMaxLimit
|
||||
? QString()
|
||||
: QString::number(state->usageValue)),
|
||||
200'000);
|
||||
wrap->widthValue(
|
||||
) | rpl::on_next([=](int width) {
|
||||
input->resize(width, input->height());
|
||||
input->moveToLeft(0, st::boxPadding.bottom());
|
||||
}, input->lifetime());
|
||||
box->setFocusCallback([=] {
|
||||
input->setFocusFast();
|
||||
});
|
||||
|
||||
const auto save = [=] {
|
||||
const auto value = input->getLastText().toInt();
|
||||
if (value <= 0) {
|
||||
input->showError();
|
||||
return;
|
||||
}
|
||||
if (guard) {
|
||||
state->usageValue = value;
|
||||
regenerate();
|
||||
}
|
||||
box->closeBox();
|
||||
};
|
||||
QObject::connect(input, &NumberInput::submitted, save);
|
||||
box->addButton(tr::lng_settings_save(), save);
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}));
|
||||
});
|
||||
|
||||
regenerate();
|
||||
|
||||
usagesSlide->toggleOn(state->requestApproval.value() | rpl::map(!_1));
|
||||
usagesSlide->finishAnimating();
|
||||
}
|
||||
|
||||
void CreateInviteLinkBox(
|
||||
not_null<GenericBox*> box,
|
||||
Fn<InviteLinkSubscriptionToggle()> fillSubscription,
|
||||
bool isGroup,
|
||||
bool isPublic,
|
||||
Fn<void(InviteLinkFields)> done) {
|
||||
EditInviteLinkBox(
|
||||
box,
|
||||
std::move(fillSubscription),
|
||||
InviteLinkFields{ .isGroup = isGroup, .isPublic = isPublic },
|
||||
std::move(done));
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
45
Telegram/SourceFiles/ui/boxes/edit_invite_link.h
Normal file
45
Telegram/SourceFiles/ui/boxes/edit_invite_link.h
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class GenericBox;
|
||||
class NumberInput;
|
||||
class SettingsButton;
|
||||
|
||||
struct InviteLinkFields {
|
||||
QString link;
|
||||
QString label;
|
||||
TimeId expireDate = 0;
|
||||
int usageLimit = 0;
|
||||
int subscriptionCredits = 0;
|
||||
bool requestApproval = false;
|
||||
bool isGroup = false;
|
||||
bool isPublic = false;
|
||||
};
|
||||
|
||||
struct InviteLinkSubscriptionToggle final {
|
||||
not_null<Ui::SettingsButton*> button;
|
||||
not_null<Ui::NumberInput*> amount;
|
||||
};
|
||||
|
||||
void EditInviteLinkBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
Fn<InviteLinkSubscriptionToggle()> fillSubscription,
|
||||
const InviteLinkFields &data,
|
||||
Fn<void(InviteLinkFields)> done);
|
||||
|
||||
void CreateInviteLinkBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
Fn<InviteLinkSubscriptionToggle()> fillSubscription,
|
||||
bool isGroup,
|
||||
bool isPublic,
|
||||
Fn<void(InviteLinkFields)> done);
|
||||
|
||||
} // namespace Ui
|
||||
155
Telegram/SourceFiles/ui/boxes/edit_invite_link_session.cpp
Normal file
155
Telegram/SourceFiles/ui/boxes/edit_invite_link_session.cpp
Normal file
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/boxes/edit_invite_link_session.h"
|
||||
|
||||
#include "core/ui_integration.h" // TextContext
|
||||
#include "data/components/credits.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/stickers/data_custom_emoji.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_app_config.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/boxes/edit_invite_link.h" // InviteLinkSubscriptionToggle
|
||||
#include "ui/effects/credits_graphics.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/fields/number_input.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "styles/style_channel_earn.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_settings.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_info.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
InviteLinkSubscriptionToggle FillCreateInviteLinkSubscriptionToggle(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<PeerData*> peer) {
|
||||
struct State final {
|
||||
rpl::variable<float64> usdRate = 0;
|
||||
};
|
||||
const auto state = box->lifetime().make_state<State>();
|
||||
const auto currency = u"USD"_q;
|
||||
|
||||
const auto container = box->verticalLayout();
|
||||
const auto toggle = container->add(
|
||||
object_ptr<SettingsButton>(
|
||||
container,
|
||||
tr::lng_group_invite_subscription(),
|
||||
st::settingsButtonNoIconLocked),
|
||||
style::margins{ 0, 0, 0, st::defaultVerticalListSkip });
|
||||
|
||||
const auto maxCredits = peer->session().appConfig().get<int>(
|
||||
u"stars_subscription_amount_max"_q,
|
||||
2500);
|
||||
|
||||
const auto &st = st::inviteLinkCreditsField;
|
||||
const auto skip = st.textMargins.top() / 2;
|
||||
const auto wrap = container->add(
|
||||
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||
container,
|
||||
object_ptr<Ui::VerticalLayout>(container)));
|
||||
box->setShowFinishedCallback([=] {
|
||||
wrap->toggleOn(toggle->toggledValue());
|
||||
wrap->finishAnimating();
|
||||
});
|
||||
const auto inputContainer = wrap->entity()->add(
|
||||
CreateSkipWidget(container, st.heightMin - skip));
|
||||
const auto input = CreateChild<NumberInput>(
|
||||
inputContainer,
|
||||
st,
|
||||
tr::lng_group_invite_subscription_ph(),
|
||||
QString(),
|
||||
std::pow(QString::number(maxCredits).size(), 10));
|
||||
wrap->toggledValue() | rpl::on_next([=](bool shown) {
|
||||
if (shown) {
|
||||
input->setFocus();
|
||||
}
|
||||
}, input->lifetime());
|
||||
const auto icon = CreateSingleStarWidget(
|
||||
inputContainer,
|
||||
st.style.font->height);
|
||||
const auto priceOverlay = Ui::CreateChild<Ui::RpWidget>(inputContainer);
|
||||
priceOverlay->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
inputContainer->sizeValue(
|
||||
) | rpl::on_next([=](const QSize &size) {
|
||||
input->resize(
|
||||
size.width() - rect::m::sum::h(st::boxRowPadding),
|
||||
st.heightMin);
|
||||
input->moveToLeft(st::boxRowPadding.left(), -skip);
|
||||
icon->moveToLeft(
|
||||
st::boxRowPadding.left(),
|
||||
input->pos().y() + st.textMargins.top());
|
||||
priceOverlay->resize(size);
|
||||
}, input->lifetime());
|
||||
ToggleChildrenVisibility(inputContainer, true);
|
||||
QObject::connect(input, &Ui::MaskedInputField::changed, [=] {
|
||||
const auto amount = input->getLastText().toDouble();
|
||||
if (amount > maxCredits) {
|
||||
input->setText(QString::number(maxCredits));
|
||||
}
|
||||
priceOverlay->update();
|
||||
});
|
||||
priceOverlay->paintRequest(
|
||||
) | rpl::on_next([=, right = st::boxRowPadding.right()] {
|
||||
if (state->usdRate.current() <= 0) {
|
||||
return;
|
||||
}
|
||||
const auto amount = input->getLastText().toDouble();
|
||||
if (amount <= 0) {
|
||||
return;
|
||||
}
|
||||
const auto text = tr::lng_group_invite_subscription_price(
|
||||
tr::now,
|
||||
lt_cost,
|
||||
Ui::FillAmountAndCurrency(
|
||||
amount * state->usdRate.current(),
|
||||
currency));
|
||||
auto p = QPainter(priceOverlay);
|
||||
p.setFont(st.placeholderFont);
|
||||
p.setPen(st.placeholderFg);
|
||||
p.setBrush(Qt::NoBrush);
|
||||
const auto m = QMargins(0, skip, right, 0);
|
||||
p.drawText(priceOverlay->rect() - m, text, style::al_right);
|
||||
}, priceOverlay->lifetime());
|
||||
|
||||
state->usdRate = peer->session().credits().rateValue(peer);
|
||||
|
||||
auto about = object_ptr<Ui::FlatLabel>(
|
||||
container,
|
||||
tr::lng_group_invite_subscription_about(
|
||||
lt_link,
|
||||
tr::lng_group_invite_subscription_about_link(
|
||||
lt_emoji,
|
||||
rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)),
|
||||
tr::rich
|
||||
) | rpl::map([](TextWithEntities text) {
|
||||
return tr::link(
|
||||
std::move(text),
|
||||
tr::lng_group_invite_subscription_about_url(tr::now));
|
||||
}),
|
||||
tr::rich),
|
||||
st::boxDividerLabel);
|
||||
Ui::AddSkip(wrap->entity());
|
||||
Ui::AddSkip(wrap->entity());
|
||||
container->add(object_ptr<Ui::DividerLabel>(
|
||||
container,
|
||||
std::move(about),
|
||||
st::defaultBoxDividerLabelPadding));
|
||||
return { toggle, input };
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
23
Telegram/SourceFiles/ui/boxes/edit_invite_link_session.h
Normal file
23
Telegram/SourceFiles/ui/boxes/edit_invite_link_session.h
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
class PeerData;
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class GenericBox;
|
||||
class SettingsButton;
|
||||
|
||||
struct InviteLinkSubscriptionToggle;
|
||||
|
||||
InviteLinkSubscriptionToggle FillCreateInviteLinkSubscriptionToggle(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
not_null<PeerData*> peer);
|
||||
|
||||
} // namespace Ui
|
||||
997
Telegram/SourceFiles/ui/boxes/peer_qr_box.cpp
Normal file
997
Telegram/SourceFiles/ui/boxes/peer_qr_box.cpp
Normal file
@@ -0,0 +1,997 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/boxes/peer_qr_box.h"
|
||||
|
||||
#include "core/application.h"
|
||||
#include "data/data_cloud_themes.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
#include "info/channel_statistics/boosts/giveaway/boost_badge.h" // InfiniteRadialAnimationWidget.
|
||||
#include "info/profile/info_profile_values.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "main/main_session.h"
|
||||
#include "qr/qr_generate.h"
|
||||
#include "ui/controls/userpic_button.h"
|
||||
#include "ui/dynamic_image.h"
|
||||
#include "ui/dynamic_thumbnails.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/image/image_prepare.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "ui/widgets/box_content_divider.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/continuous_sliders.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "window/window_controller.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_giveaway.h"
|
||||
#include "styles/style_credits.h"
|
||||
#include "styles/style_intro.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_settings.h"
|
||||
#include "styles/style_widgets.h"
|
||||
#include "styles/style_window.h"
|
||||
|
||||
#include <QtCore/QMimeData>
|
||||
#include <QtGui/QGuiApplication>
|
||||
#include <QtSvg/QSvgRenderer>
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
using Colors = std::vector<QColor>;
|
||||
|
||||
[[nodiscard]] QMargins NoPhotoBackgroundMargins() {
|
||||
return QMargins(
|
||||
st::profileQrBackgroundMargins.left(),
|
||||
st::profileQrBackgroundMargins.left(),
|
||||
st::profileQrBackgroundMargins.right(),
|
||||
st::profileQrBackgroundMargins.bottom());
|
||||
}
|
||||
|
||||
[[nodiscard]] style::font CreateFont(int size, int scale) {
|
||||
return style::font(
|
||||
style::ConvertScale(size, scale),
|
||||
st::profileQrFont->flags(),
|
||||
st::profileQrFont->family());
|
||||
}
|
||||
|
||||
[[nodiscard]] QImage TelegramQr(
|
||||
const Qr::Data &data,
|
||||
int pixel,
|
||||
int max,
|
||||
bool hasWhiteBackground) {
|
||||
Expects(data.size > 0);
|
||||
|
||||
constexpr auto kCenterRatio = 0.175;
|
||||
|
||||
if (max > 0 && data.size * pixel > max) {
|
||||
pixel = std::max(max / data.size, 1);
|
||||
}
|
||||
auto qr = Qr::Generate(
|
||||
data,
|
||||
pixel * style::DevicePixelRatio(),
|
||||
hasWhiteBackground ? Qt::transparent : Qt::black,
|
||||
hasWhiteBackground ? Qt::white : Qt::transparent);
|
||||
{
|
||||
auto p = QPainter(&qr);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
auto svg = QSvgRenderer(u":/gui/plane_white.svg"_q);
|
||||
const auto size = qr.rect().size();
|
||||
const auto centerRect = Rect(size)
|
||||
- Margins((size.width() - (size.width() * kCenterRatio)) / 2);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(Qt::white);
|
||||
if (hasWhiteBackground) {
|
||||
p.setCompositionMode(QPainter::CompositionMode_Clear);
|
||||
p.drawEllipse(centerRect);
|
||||
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
|
||||
svg.render(&p, centerRect);
|
||||
} else {
|
||||
p.drawEllipse(centerRect);
|
||||
p.setCompositionMode(QPainter::CompositionMode_Clear);
|
||||
svg.render(&p, centerRect);
|
||||
}
|
||||
}
|
||||
return qr;
|
||||
}
|
||||
|
||||
[[nodiscard]] QMargins RoundedMargins(
|
||||
const QMargins &backgroundMargins,
|
||||
int photoSize,
|
||||
int textMaxHeight) {
|
||||
return (textMaxHeight
|
||||
? (backgroundMargins + QMargins(0, photoSize / 2, 0, textMaxHeight))
|
||||
: photoSize
|
||||
? backgroundMargins + QMargins(0, photoSize / 2, 0, photoSize / 2)
|
||||
: Margins(backgroundMargins.left()));
|
||||
}
|
||||
|
||||
void Paint(
|
||||
QPainter &p,
|
||||
const style::font &font,
|
||||
const QString &text,
|
||||
const Colors &backgroundColors,
|
||||
const QMargins &backgroundMargins,
|
||||
const QImage &qrImage,
|
||||
const QRect &qrRect,
|
||||
int qrMaxSize,
|
||||
int qrPixel,
|
||||
int radius,
|
||||
int textMaxHeight,
|
||||
int photoSize,
|
||||
bool hasWhiteBackground) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(hasWhiteBackground ? Qt::white : Qt::transparent);
|
||||
const auto roundedRect = qrRect
|
||||
+ RoundedMargins(backgroundMargins, photoSize, textMaxHeight);
|
||||
p.drawRoundedRect(roundedRect, radius, radius);
|
||||
if (!qrImage.isNull() && !backgroundColors.empty()) {
|
||||
constexpr auto kDuration = crl::time(10000);
|
||||
const auto angle = (crl::now() % kDuration)
|
||||
/ float64(kDuration) * 360.0;
|
||||
const auto gradientRotation = int(angle / 45.) * 45;
|
||||
const auto gradientRotationAdd = angle - gradientRotation;
|
||||
|
||||
const auto textAdditionalWidth = backgroundMargins.left();
|
||||
auto back = Images::GenerateGradient(
|
||||
qrRect.size() + QSize(textAdditionalWidth, 0),
|
||||
backgroundColors,
|
||||
gradientRotation,
|
||||
1. - (gradientRotationAdd / 45.));
|
||||
if (hasWhiteBackground) {
|
||||
p.drawImage(qrRect, back);
|
||||
}
|
||||
const auto coloredSize = QSize(back.width(), textMaxHeight);
|
||||
auto colored = QImage(
|
||||
coloredSize * style::DevicePixelRatio(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
colored.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
colored.fill(Qt::transparent);
|
||||
if (textMaxHeight) {
|
||||
// '@' + QString(32, 'W');
|
||||
auto p = QPainter(&colored);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setPen(Qt::black);
|
||||
p.setFont(font);
|
||||
auto option = QTextOption(style::al_center);
|
||||
option.setWrapMode(QTextOption::WrapAnywhere);
|
||||
p.drawText(Rect(coloredSize), text, option);
|
||||
p.setCompositionMode(QPainter::CompositionMode_SourceIn);
|
||||
p.drawImage(0, -back.height() + textMaxHeight, back);
|
||||
}
|
||||
if (!hasWhiteBackground) {
|
||||
auto copy = qrImage;
|
||||
{
|
||||
auto p = QPainter(©);
|
||||
p.setCompositionMode(QPainter::CompositionMode_SourceIn);
|
||||
p.drawImage(Rect(copy.size()), back);
|
||||
}
|
||||
p.drawImage(qrRect, copy);
|
||||
} else {
|
||||
p.drawImage(qrRect, qrImage);
|
||||
}
|
||||
if (textMaxHeight) {
|
||||
p.drawImage(
|
||||
qrRect.x() - textAdditionalWidth / 2,
|
||||
rect::bottom(qrRect)
|
||||
+ ((rect::bottom(roundedRect) - rect::bottom(qrRect))
|
||||
- textMaxHeight) / 2,
|
||||
colored);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
not_null<Ui::RpWidget*> PrepareQrWidget(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
not_null<Ui::RpWidget*> topWidget,
|
||||
rpl::producer<int> fontSizeValue,
|
||||
rpl::producer<bool> userpicToggled,
|
||||
rpl::producer<bool> backgroundToggled,
|
||||
rpl::producer<QString> username,
|
||||
rpl::producer<QString> links,
|
||||
rpl::producer<Colors> bgs,
|
||||
rpl::producer<QString> about) {
|
||||
const auto divider = container->add(
|
||||
object_ptr<Ui::BoxContentDivider>(container));
|
||||
struct State final {
|
||||
explicit State(Fn<void()> callback) : updating(callback) {
|
||||
updating.start();
|
||||
}
|
||||
|
||||
Ui::Animations::Basic updating;
|
||||
style::font font;
|
||||
QImage qrImage;
|
||||
Colors backgroundColors;
|
||||
QString text;
|
||||
QMargins backgroundMargins;
|
||||
int textWidth = 0;
|
||||
int textMaxHeight = 0;
|
||||
int photoSize = 0;
|
||||
bool backgroundToggled = false;
|
||||
};
|
||||
const auto result = Ui::CreateChild<Ui::RpWidget>(divider);
|
||||
topWidget->setParent(result);
|
||||
topWidget->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
const auto state = result->lifetime().make_state<State>(
|
||||
[=] { result->update(); });
|
||||
const auto qrMaxSize = st::boxWideWidth
|
||||
- rect::m::sum::h(st::boxRowPadding)
|
||||
- rect::m::sum::h(st::profileQrBackgroundMargins);
|
||||
const auto aboutLabel = Ui::CreateChild<Ui::FlatLabel>(
|
||||
divider,
|
||||
st::creditsBoxAboutDivider);
|
||||
rpl::combine(
|
||||
std::move(fontSizeValue),
|
||||
std::move(userpicToggled),
|
||||
std::move(backgroundToggled),
|
||||
std::move(username),
|
||||
std::move(bgs),
|
||||
std::move(links),
|
||||
std::move(about),
|
||||
rpl::single(rpl::empty) | rpl::then(style::PaletteChanged())
|
||||
) | rpl::on_next([=](
|
||||
int fontSize,
|
||||
bool userpicToggled,
|
||||
bool backgroundToggled,
|
||||
const QString &username,
|
||||
const Colors &backgroundColors,
|
||||
const QString &link,
|
||||
const QString &about,
|
||||
const auto &) {
|
||||
state->font = CreateFont(fontSize, style::Scale());
|
||||
state->backgroundToggled = backgroundToggled;
|
||||
state->backgroundMargins = userpicToggled
|
||||
? st::profileQrBackgroundMargins
|
||||
: NoPhotoBackgroundMargins();
|
||||
state->photoSize = userpicToggled
|
||||
? st::defaultUserpicButton.photoSize
|
||||
: 0;
|
||||
state->backgroundColors = backgroundColors;
|
||||
state->text = username.toUpper();
|
||||
state->textWidth = state->font->width(state->text);
|
||||
if (!link.isEmpty()) {
|
||||
const auto remainder = qrMaxSize % st::introQrPixel;
|
||||
const auto downTo = remainder
|
||||
? qrMaxSize - remainder
|
||||
: qrMaxSize;
|
||||
state->qrImage = TelegramQr(
|
||||
Qr::Encode(link.toUtf8(), Qr::Redundancy::Default),
|
||||
st::introQrPixel,
|
||||
downTo,
|
||||
backgroundToggled).scaled(
|
||||
Size(qrMaxSize * style::DevicePixelRatio()),
|
||||
Qt::IgnoreAspectRatio,
|
||||
Qt::SmoothTransformation);
|
||||
} else {
|
||||
auto image = QImage(
|
||||
Size(qrMaxSize * style::DevicePixelRatio()),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
image.fill(Qt::white);
|
||||
image.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
state->qrImage = std::move(image);
|
||||
}
|
||||
const auto resultWidth = qrMaxSize
|
||||
+ rect::m::sum::h(state->backgroundMargins);
|
||||
{
|
||||
aboutLabel->setText(about);
|
||||
aboutLabel->resizeToWidth(resultWidth);
|
||||
}
|
||||
const auto textMaxWidth = state->backgroundMargins.left()
|
||||
+ (state->qrImage.width() / style::DevicePixelRatio());
|
||||
const auto lines = int(state->textWidth / textMaxWidth) + 1;
|
||||
state->textMaxHeight = state->textWidth
|
||||
? (state->font->height * lines)
|
||||
: 0;
|
||||
const auto whiteMargins = RoundedMargins(
|
||||
state->backgroundMargins,
|
||||
state->photoSize,
|
||||
state->textMaxHeight);
|
||||
result->resize(
|
||||
qrMaxSize + rect::m::sum::h(whiteMargins),
|
||||
qrMaxSize
|
||||
+ rect::m::sum::v(whiteMargins) // White.
|
||||
+ rect::m::sum::v(st::profileQrBackgroundPadding) // Gray.
|
||||
+ state->photoSize / 2
|
||||
+ aboutLabel->height());
|
||||
|
||||
divider->resize(container->width(), result->height());
|
||||
result->moveToLeft((container->width() - result->width()) / 2, 0);
|
||||
topWidget->setVisible(userpicToggled);
|
||||
topWidget->moveToLeft(0, std::numeric_limits<int>::min());
|
||||
topWidget->raise();
|
||||
|
||||
aboutLabel->raise();
|
||||
aboutLabel->moveToLeft(
|
||||
result->x(),
|
||||
divider->height()
|
||||
- aboutLabel->height()
|
||||
- st::defaultBoxDividerLabelPadding.top());
|
||||
}, container->lifetime());
|
||||
result->paintRequest(
|
||||
) | rpl::on_next([=](QRect clip) {
|
||||
auto p = QPainter(result);
|
||||
const auto size = (state->qrImage.size() / style::DevicePixelRatio());
|
||||
const auto qrRect = Rect(
|
||||
(result->width() - size.width()) / 2,
|
||||
state->backgroundMargins.top() + state->photoSize / 2,
|
||||
size);
|
||||
p.translate(
|
||||
0,
|
||||
st::profileQrBackgroundPadding.top() + state->photoSize / 2);
|
||||
Paint(
|
||||
p,
|
||||
state->font,
|
||||
state->text,
|
||||
state->backgroundColors,
|
||||
state->backgroundMargins,
|
||||
state->qrImage,
|
||||
qrRect,
|
||||
qrMaxSize,
|
||||
st::introQrPixel,
|
||||
st::profileQrBackgroundRadius,
|
||||
state->textMaxHeight,
|
||||
state->photoSize,
|
||||
state->backgroundToggled);
|
||||
if (!state->photoSize) {
|
||||
return;
|
||||
}
|
||||
const auto photoSize = state->photoSize;
|
||||
const auto top = Ui::GrabWidget(
|
||||
topWidget,
|
||||
QRect(),
|
||||
Qt::transparent).scaled(
|
||||
Size(photoSize * style::DevicePixelRatio()),
|
||||
Qt::IgnoreAspectRatio,
|
||||
Qt::SmoothTransformation);
|
||||
p.drawPixmap((result->width() - photoSize) / 2, -photoSize / 2, top);
|
||||
}, result->lifetime());
|
||||
return result;
|
||||
}
|
||||
|
||||
[[nodiscard]] Fn<void(int)> AddDotsToSlider(
|
||||
not_null<Ui::ContinuousSlider*> slider,
|
||||
const style::MediaSlider &st,
|
||||
int count) {
|
||||
const auto lineWidth = st::lineWidth;
|
||||
const auto smallSize = Size(st.seekSize.height() - st.width);
|
||||
auto smallDots = std::vector<not_null<Ui::RpWidget*>>();
|
||||
smallDots.reserve(count - 1);
|
||||
const auto paintSmall = [=](QPainter &p, const QBrush &brush) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
auto pen = st::boxBg->p;
|
||||
pen.setWidth(st.width);
|
||||
p.setPen(pen);
|
||||
p.setBrush(brush);
|
||||
p.drawEllipse(Rect(smallSize) - Margins(lineWidth));
|
||||
};
|
||||
for (auto i = 0; i < count - 1; i++) {
|
||||
smallDots.push_back(
|
||||
Ui::CreateChild<Ui::RpWidget>(slider->parentWidget()));
|
||||
const auto dot = smallDots.back();
|
||||
dot->resize(smallSize);
|
||||
dot->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
dot->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(dot);
|
||||
const auto fg = (slider->value() > (i / float64(count - 1)))
|
||||
? st.activeFg
|
||||
: st.inactiveFg;
|
||||
paintSmall(p, fg);
|
||||
}, dot->lifetime());
|
||||
}
|
||||
const auto bigDot = Ui::CreateChild<Ui::RpWidget>(slider->parentWidget());
|
||||
bigDot->resize(st.seekSize);
|
||||
bigDot->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
bigDot->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(bigDot);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
auto pen = st::boxBg->p;
|
||||
pen.setWidth(st.width);
|
||||
p.setPen(pen);
|
||||
p.setBrush(st.activeFg);
|
||||
p.drawEllipse(Rect(st.seekSize) - Margins(lineWidth));
|
||||
}, bigDot->lifetime());
|
||||
|
||||
return [=](int index) {
|
||||
const auto g = slider->geometry();
|
||||
const auto bigTop = g.y() + (g.height() - bigDot->height()) / 2;
|
||||
const auto smallTop = g.y()
|
||||
+ (g.height() - smallSize.height()) / 2;
|
||||
for (auto i = 0; i < count; ++i) {
|
||||
if (index == i) {
|
||||
const auto x = ((g.width() - bigDot->width()) * i)
|
||||
/ float64(count - 1);
|
||||
bigDot->move(g.x() + std::round(x), bigTop);
|
||||
} else {
|
||||
const auto k = (i < index) ? i : i - 1;
|
||||
const auto w = smallDots[k]->width();
|
||||
smallDots[k]->move(
|
||||
g.x() + ((g.width() - w) * i) / (count - 1),
|
||||
smallTop);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void FillPeerQrBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
PeerData *peer,
|
||||
std::optional<QString> customLink,
|
||||
rpl::producer<QString> about) {
|
||||
const auto window = Core::App().findWindow(box);
|
||||
const auto controller = window ? window->sessionController() : nullptr;
|
||||
if (!controller) {
|
||||
return;
|
||||
}
|
||||
box->setStyle(st::giveawayGiftCodeBox);
|
||||
box->setNoContentMargin(true);
|
||||
box->setWidth(st::aboutWidth);
|
||||
box->setTitle(tr::lng_group_invite_context_qr());
|
||||
box->verticalLayout()->resizeToWidth(box->width());
|
||||
struct State {
|
||||
Ui::RpWidget* saveButton = nullptr;
|
||||
rpl::variable<bool> saveButtonBusy = false;
|
||||
rpl::variable<bool> userpicToggled = true;
|
||||
rpl::variable<bool> backgroundToggled = true;
|
||||
rpl::variable<Colors> bgs;
|
||||
Ui::Animations::Simple animation;
|
||||
rpl::variable<int> chosen = 0;
|
||||
rpl::variable<int> scaleValue = 0;
|
||||
rpl::variable<int> fontSizeValue = 28;
|
||||
};
|
||||
const auto state = box->lifetime().make_state<State>();
|
||||
state->userpicToggled = !(customLink || !peer);
|
||||
|
||||
const auto usernameValue = [=] {
|
||||
return (customLink || !peer)
|
||||
? (rpl::single(QString()) | rpl::type_erased)
|
||||
: Info::Profile::UsernameValue(peer, true) | rpl::map(
|
||||
[](const auto &username) { return username.text; });
|
||||
};
|
||||
const auto linkValue = [=] {
|
||||
return customLink
|
||||
? rpl::single(*customLink)
|
||||
: peer
|
||||
? Info::Profile::LinkValue(peer, true) | rpl::map(
|
||||
[](const auto &link) { return link.text; })
|
||||
: (rpl::single(QString()) | rpl::type_erased);
|
||||
};
|
||||
|
||||
const auto userpic = Ui::CreateChild<Ui::RpWidget>(box);
|
||||
const auto userpicSize = st::defaultUserpicButton.photoSize;
|
||||
userpic->resize(Size(userpicSize));
|
||||
const auto userpicMedia = Ui::MakeUserpicThumbnail(peer
|
||||
? peer
|
||||
: controller->session().user().get());
|
||||
userpicMedia->subscribeToUpdates(
|
||||
crl::guard(userpic, [=] { userpic->update(); }));
|
||||
userpic->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(userpic);
|
||||
p.drawImage(0, 0, userpicMedia->image(userpicSize));
|
||||
}, userpic->lifetime());
|
||||
|
||||
linkValue() | rpl::on_next([=](const QString &link) {
|
||||
if (link.isEmpty()) {
|
||||
box->showFinishes() | rpl::on_next([=] {
|
||||
box->closeBox();
|
||||
}, box->lifetime());
|
||||
box->closeBox();
|
||||
}
|
||||
}, box->lifetime());
|
||||
|
||||
userpic->setVisible(peer != nullptr);
|
||||
PrepareQrWidget(
|
||||
box->verticalLayout(),
|
||||
userpic,
|
||||
state->fontSizeValue.value(),
|
||||
state->userpicToggled.value(),
|
||||
state->backgroundToggled.value(),
|
||||
usernameValue(),
|
||||
linkValue(),
|
||||
state->bgs.value(),
|
||||
about ? std::move(about) : rpl::single(QString()));
|
||||
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
Ui::AddSubsectionTitle(
|
||||
box->verticalLayout(),
|
||||
tr::lng_userpic_builder_color_subtitle());
|
||||
|
||||
const auto themesContainer = box->addRow(
|
||||
object_ptr<Ui::VerticalLayout>(box));
|
||||
|
||||
const auto activewidth = int(
|
||||
(st::defaultInputField.borderActive + st::lineWidth) * 0.9);
|
||||
const auto size = st::chatThemePreviewSize.width();
|
||||
|
||||
const auto fill = [=](const std::vector<Data::CloudTheme> &cloudThemes) {
|
||||
while (themesContainer->count()) {
|
||||
delete themesContainer->widgetAt(0);
|
||||
}
|
||||
|
||||
struct State {
|
||||
Colors colors;
|
||||
QImage image;
|
||||
};
|
||||
constexpr auto kMaxInRow = 4;
|
||||
constexpr auto kMaxColors = 4;
|
||||
auto row = (Ui::RpWidget*)(nullptr);
|
||||
auto counter = 0;
|
||||
const auto spacing = (0
|
||||
+ (box->width() - rect::m::sum::h(st::boxRowPadding))
|
||||
- (kMaxInRow * size)) / (kMaxInRow + 1);
|
||||
|
||||
auto colorsCollection = ranges::views::all(
|
||||
cloudThemes
|
||||
) | ranges::views::transform([](const auto &cloudTheme) -> Colors {
|
||||
const auto it = cloudTheme.settings.find(
|
||||
Data::CloudThemeType::Light);
|
||||
if (it == end(cloudTheme.settings)) {
|
||||
return Colors();
|
||||
}
|
||||
const auto colors = it->second.paper
|
||||
? it->second.paper->backgroundColors()
|
||||
: Colors();
|
||||
if (colors.size() != kMaxColors) {
|
||||
return Colors();
|
||||
}
|
||||
return colors;
|
||||
}) | ranges::views::filter([](const Colors &colors) {
|
||||
return !colors.empty();
|
||||
}) | ranges::to_vector;
|
||||
Expects(!colorsCollection.empty());
|
||||
colorsCollection[0] = Colors{
|
||||
st::premiumButtonBg1->c,
|
||||
st::premiumButtonBg1->c,
|
||||
st::premiumButtonBg2->c,
|
||||
st::premiumButtonBg3->c,
|
||||
};
|
||||
// colorsCollection.push_back(Colors{
|
||||
// st::creditsBg1->c,
|
||||
// st::creditsBg2->c,
|
||||
// st::creditsBg1->c,
|
||||
// st::creditsBg2->c,
|
||||
// });
|
||||
|
||||
for (const auto &colors : colorsCollection) {
|
||||
if (state->bgs.current().empty()) {
|
||||
state->bgs = colors;
|
||||
}
|
||||
|
||||
if (counter % kMaxInRow == 0) {
|
||||
Ui::AddSkip(themesContainer);
|
||||
row = themesContainer->add(
|
||||
object_ptr<Ui::RpWidget>(themesContainer));
|
||||
row->resize(size, size);
|
||||
}
|
||||
const auto widget = Ui::CreateChild<Ui::AbstractButton>(row);
|
||||
widget->setClickedCallback([=] {
|
||||
state->chosen = counter;
|
||||
widget->update();
|
||||
state->animation.stop();
|
||||
state->animation.start([=](float64 value) {
|
||||
const auto was = state->bgs.current();
|
||||
const auto &now = colors;
|
||||
if (was.size() == now.size()
|
||||
&& was.size() == kMaxColors) {
|
||||
state->bgs = Colors({
|
||||
anim::color(was[0], now[0], value),
|
||||
anim::color(was[1], now[1], value),
|
||||
anim::color(was[2], now[2], value),
|
||||
anim::color(was[3], now[3], value),
|
||||
});
|
||||
}
|
||||
},
|
||||
0.,
|
||||
1.,
|
||||
st::shakeDuration);
|
||||
});
|
||||
state->chosen.value() | rpl::combine_previous(
|
||||
) | rpl::filter([=](int i, int k) {
|
||||
return i == counter || k == counter;
|
||||
}) | rpl::on_next([=] {
|
||||
widget->update();
|
||||
}, widget->lifetime());
|
||||
widget->resize(size, size);
|
||||
widget->moveToLeft(
|
||||
spacing + ((counter % kMaxInRow) * (size + spacing)),
|
||||
0);
|
||||
widget->show();
|
||||
|
||||
const auto cornersMask = Images::CornersMask(
|
||||
st::roundRadiusLarge * style::DevicePixelRatio());
|
||||
const auto back = [&] {
|
||||
auto gradient = Images::GenerateGradient(
|
||||
Size(size - activewidth * 5) * style::DevicePixelRatio(),
|
||||
colors,
|
||||
0,
|
||||
0);
|
||||
gradient.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
auto result = Images::Round(std::move(gradient), cornersMask);
|
||||
const auto rect = Rect(
|
||||
result.size() / style::DevicePixelRatio());
|
||||
auto colored = result;
|
||||
colored.fill(Qt::transparent);
|
||||
{
|
||||
auto p = QPainter(&colored);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
st::profileQrIcon.paintInCenter(p, rect);
|
||||
p.setCompositionMode(QPainter::CompositionMode_SourceIn);
|
||||
p.drawImage(0, 0, result);
|
||||
}
|
||||
auto temp = result;
|
||||
temp.fill(Qt::transparent);
|
||||
{
|
||||
auto p = QPainter(&temp);
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setPen(st::premiumButtonFg);
|
||||
p.setBrush(st::premiumButtonFg);
|
||||
const auto size = st::profileQrIcon.width() * 1.5;
|
||||
const auto margins = Margins((rect.width() - size) / 2);
|
||||
const auto inner = rect - margins;
|
||||
p.drawRoundedRect(
|
||||
inner,
|
||||
st::roundRadiusLarge,
|
||||
st::roundRadiusLarge);
|
||||
p.drawImage(0, 0, colored);
|
||||
}
|
||||
{
|
||||
auto p = QPainter(&result);
|
||||
p.drawImage(0, 0, temp);
|
||||
}
|
||||
return result;
|
||||
}();
|
||||
widget->paintRequest() | rpl::on_next([=] {
|
||||
auto p = QPainter(widget);
|
||||
const auto rect = widget->rect() - Margins(activewidth * 2.5);
|
||||
p.drawImage(rect.x(), rect.y(), back);
|
||||
if (state->chosen.current() == counter) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
auto pen = st::activeLineFg->p;
|
||||
pen.setWidth(st::defaultInputField.borderActive);
|
||||
p.setPen(pen);
|
||||
const auto r = st::roundRadiusLarge
|
||||
+ activewidth * 2.1 * style::DevicePixelRatio();
|
||||
p.drawRoundedRect(
|
||||
widget->rect() - Margins(pen.width()),
|
||||
r,
|
||||
r);
|
||||
}
|
||||
}, widget->lifetime());
|
||||
counter++;
|
||||
}
|
||||
Ui::AddSkip(themesContainer);
|
||||
Ui::AddSkip(themesContainer);
|
||||
themesContainer->resizeToWidth(box->width());
|
||||
};
|
||||
|
||||
const auto themes = &controller->session().data().cloudThemes();
|
||||
const auto &list = themes->chatThemes();
|
||||
if (!list.empty()) {
|
||||
fill(list);
|
||||
} else {
|
||||
themes->refreshChatThemes();
|
||||
themes->chatThemesUpdated(
|
||||
) | rpl::take(1) | rpl::on_next([=] {
|
||||
fill(themes->chatThemes());
|
||||
}, box->lifetime());
|
||||
}
|
||||
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
Ui::AddDivider(box->verticalLayout());
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
Ui::AddSubsectionTitle(
|
||||
box->verticalLayout(),
|
||||
tr::lng_qr_box_quality());
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
constexpr auto kMaxQualities = 3;
|
||||
{
|
||||
const auto seekSize = st::settingsScale.seekSize.height();
|
||||
const auto &labelSt = st::defaultFlatLabel;
|
||||
const auto labels = box->verticalLayout()->add(
|
||||
Ui::CreateSkipWidget(
|
||||
box,
|
||||
labelSt.style.font->height + labelSt.style.font->descent),
|
||||
st::boxRowPadding);
|
||||
const auto left = Ui::CreateChild<Ui::FlatLabel>(
|
||||
labels,
|
||||
tr::lng_qr_box_quality1(),
|
||||
labelSt);
|
||||
const auto middle = Ui::CreateChild<Ui::FlatLabel>(
|
||||
labels,
|
||||
tr::lng_qr_box_quality2(),
|
||||
labelSt);
|
||||
const auto right = Ui::CreateChild<Ui::FlatLabel>(
|
||||
labels,
|
||||
tr::lng_qr_box_quality3(),
|
||||
labelSt);
|
||||
labels->sizeValue(
|
||||
) | rpl::on_next([=](const QSize &size) {
|
||||
left->moveToLeft(0, 0);
|
||||
middle->moveToLeft((size.width() - middle->width()) / 2, 0);
|
||||
right->moveToRight(0, 0);
|
||||
}, labels->lifetime());
|
||||
|
||||
const auto slider = box->verticalLayout()->add(
|
||||
object_ptr<Ui::MediaSliderWheelless>(
|
||||
box->verticalLayout(),
|
||||
st::settingsScale),
|
||||
st::boxRowPadding);
|
||||
slider->resize(slider->width(), seekSize);
|
||||
const auto active = st::windowActiveTextFg->c;
|
||||
const auto inactive = st::windowSubTextFg->c;
|
||||
const auto colorize = [=](int index) {
|
||||
if (index == 0) {
|
||||
left->setTextColorOverride(active);
|
||||
middle->setTextColorOverride(inactive);
|
||||
right->setTextColorOverride(inactive);
|
||||
} else if (index == 1) {
|
||||
left->setTextColorOverride(inactive);
|
||||
middle->setTextColorOverride(active);
|
||||
right->setTextColorOverride(inactive);
|
||||
} else if (index == 2) {
|
||||
left->setTextColorOverride(inactive);
|
||||
middle->setTextColorOverride(inactive);
|
||||
right->setTextColorOverride(active);
|
||||
}
|
||||
};
|
||||
const auto updateGeometry = AddDotsToSlider(
|
||||
slider,
|
||||
st::settingsScale,
|
||||
kMaxQualities);
|
||||
slider->geometryValue(
|
||||
) | rpl::on_next([=](const QRect &rect) {
|
||||
updateGeometry(int(slider->value() * (kMaxQualities - 1)));
|
||||
}, box->lifetime());
|
||||
|
||||
box->setShowFinishedCallback([=] {
|
||||
colorize(0);
|
||||
updateGeometry(0);
|
||||
});
|
||||
slider->setPseudoDiscrete(
|
||||
kMaxQualities,
|
||||
[=](int index) { return index; },
|
||||
0,
|
||||
[=](int scale) {
|
||||
state->scaleValue = scale;
|
||||
colorize(scale);
|
||||
updateGeometry(scale);
|
||||
},
|
||||
[](int) {});
|
||||
}
|
||||
{
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
Ui::AddSubsectionTitle(
|
||||
box->verticalLayout(),
|
||||
tr::lng_qr_box_font_size());
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
const auto seekSize = st::settingsScale.seekSize.height();
|
||||
|
||||
const auto slider = box->verticalLayout()->add(
|
||||
object_ptr<Ui::MediaSliderWheelless>(
|
||||
box->verticalLayout(),
|
||||
st::settingsScale),
|
||||
st::boxRowPadding);
|
||||
slider->resize(slider->width(), seekSize);
|
||||
const auto kSizeAmount = 8;
|
||||
const auto kMinSize = 20;
|
||||
const auto kMaxSize = 36;
|
||||
const auto kStep = (kMaxSize - kMinSize) / (kSizeAmount - 1);
|
||||
const auto updateGeometry = AddDotsToSlider(
|
||||
slider,
|
||||
st::settingsScale,
|
||||
kSizeAmount);
|
||||
const auto fontSizeToIndex = [=](int fontSize) {
|
||||
return (fontSize - kMinSize) / kStep;
|
||||
};
|
||||
const auto indexToFontSize = [=](int index) {
|
||||
return kMinSize + index * kStep;
|
||||
};
|
||||
slider->geometryValue(
|
||||
) | rpl::on_next([=](const QRect &rect) {
|
||||
updateGeometry(fontSizeToIndex(state->fontSizeValue.current()));
|
||||
}, box->lifetime());
|
||||
|
||||
box->setShowFinishedCallback([=] {
|
||||
updateGeometry(fontSizeToIndex(state->fontSizeValue.current()));
|
||||
});
|
||||
slider->setPseudoDiscrete(
|
||||
kSizeAmount,
|
||||
[=](int index) { return indexToFontSize(index); },
|
||||
state->fontSizeValue.current(),
|
||||
[=](int fontSize) {
|
||||
state->fontSizeValue = fontSize;
|
||||
updateGeometry(fontSizeToIndex(fontSize));
|
||||
},
|
||||
[](int) {});
|
||||
}
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
if (peer) {
|
||||
const auto userpicToggle = box->verticalLayout()->add(
|
||||
object_ptr<Ui::SettingsButton>(
|
||||
box->verticalLayout(),
|
||||
(peer->isUser()
|
||||
? tr::lng_mediaview_profile_photo
|
||||
: (peer->isChannel() && !peer->isMegagroup())
|
||||
? tr::lng_mediaview_channel_photo
|
||||
: tr::lng_mediaview_group_photo)(),
|
||||
st::settingsButtonNoIcon));
|
||||
userpicToggle->toggleOn(state->userpicToggled.value(), true);
|
||||
userpicToggle->setClickedCallback([=] {
|
||||
state->userpicToggled = !state->userpicToggled.current();
|
||||
});
|
||||
}
|
||||
{
|
||||
const auto backgroundToggle = box->verticalLayout()->add(
|
||||
object_ptr<Ui::SettingsButton>(
|
||||
box->verticalLayout(),
|
||||
tr::lng_qr_box_transparent_background(),
|
||||
st::settingsButtonNoIcon));
|
||||
backgroundToggle->toggleOn(
|
||||
state->backgroundToggled.value() | rpl::map(!rpl::mappers::_1),
|
||||
true);
|
||||
backgroundToggle->setClickedCallback([=] {
|
||||
state->backgroundToggled = !state->backgroundToggled.current();
|
||||
});
|
||||
}
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
|
||||
auto buttonText = rpl::conditional(
|
||||
state->saveButtonBusy.value() | rpl::map(rpl::mappers::_1),
|
||||
rpl::single(QString()),
|
||||
tr::lng_chat_link_copy());
|
||||
const auto show = controller->uiShow();
|
||||
state->saveButton = box->addButton(std::move(buttonText), [=] {
|
||||
if (state->saveButtonBusy.current()) {
|
||||
return;
|
||||
}
|
||||
state->saveButtonBusy = true;
|
||||
const auto userpicToggled = state->userpicToggled.current();
|
||||
const auto backgroundToggled = state->backgroundToggled.current();
|
||||
const auto scale = style::kScaleDefault
|
||||
* (kMaxQualities + int(state->scaleValue.current() * 2));
|
||||
const auto divider = std::max(100, style::Scale())
|
||||
/ style::kScaleDefault;
|
||||
const auto profileQrBackgroundRadius = style::ConvertScale(
|
||||
st::profileQrBackgroundRadius / divider,
|
||||
scale);
|
||||
const auto introQrPixel = style::ConvertScale(
|
||||
st::introQrPixel / divider,
|
||||
scale);
|
||||
const auto lineWidth = style::ConvertScale(
|
||||
st::lineWidth / divider,
|
||||
scale);
|
||||
const auto boxWideWidth = style::ConvertScale(
|
||||
st::boxWideWidth / divider,
|
||||
scale);
|
||||
const auto createMargins = [&](const style::margins &margins) {
|
||||
return QMargins(
|
||||
style::ConvertScale(margins.left() / divider, scale),
|
||||
style::ConvertScale(margins.top() / divider, scale),
|
||||
style::ConvertScale(margins.right() / divider, scale),
|
||||
style::ConvertScale(margins.bottom() / divider, scale));
|
||||
};
|
||||
const auto boxRowPadding = createMargins(st::boxRowPadding);
|
||||
const auto backgroundMargins = userpicToggled
|
||||
? createMargins(st::profileQrBackgroundMargins)
|
||||
: createMargins(NoPhotoBackgroundMargins());
|
||||
const auto qrMaxSize = boxWideWidth
|
||||
- rect::m::sum::h(boxRowPadding)
|
||||
- rect::m::sum::h(backgroundMargins);
|
||||
const auto photoSize = userpicToggled
|
||||
? style::ConvertScale(
|
||||
st::defaultUserpicButton.photoSize / divider,
|
||||
scale)
|
||||
: 0;
|
||||
|
||||
const auto font = CreateFont(state->fontSizeValue.current(), scale);
|
||||
const auto username = rpl::variable<QString>(
|
||||
usernameValue()).current().toUpper();
|
||||
const auto link = rpl::variable<QString>(linkValue());
|
||||
const auto textWidth = font->width(username);
|
||||
const auto top = photoSize
|
||||
? userpicMedia->image(photoSize)
|
||||
: QImage();
|
||||
const auto weak = base::make_weak(box);
|
||||
|
||||
crl::async([=] {
|
||||
const auto qrImage = TelegramQr(
|
||||
Qr::Encode(
|
||||
link.current().toUtf8(),
|
||||
Qr::Redundancy::Default),
|
||||
introQrPixel,
|
||||
qrMaxSize,
|
||||
backgroundToggled);
|
||||
const auto textMaxWidth = backgroundMargins.left()
|
||||
+ (qrImage.width() / style::DevicePixelRatio());
|
||||
const auto lines = int(textWidth / textMaxWidth) + 1;
|
||||
const auto textMaxHeight = textWidth ? font->height * lines : 0;
|
||||
|
||||
const auto whiteMargins = RoundedMargins(
|
||||
backgroundMargins,
|
||||
photoSize,
|
||||
textMaxHeight);
|
||||
const auto resultSize = QSize(
|
||||
qrMaxSize + rect::m::sum::h(whiteMargins),
|
||||
qrMaxSize + rect::m::sum::v(whiteMargins) + photoSize / 2);
|
||||
|
||||
const auto qrImageSize = qrImage.size()
|
||||
/ style::DevicePixelRatio();
|
||||
const auto qrRect = Rect(
|
||||
(resultSize.width() - qrImageSize.width()) / 2,
|
||||
whiteMargins.top() + photoSize / 2,
|
||||
qrImageSize);
|
||||
|
||||
auto image = QImage(
|
||||
resultSize * style::DevicePixelRatio(),
|
||||
QImage::Format_ARGB32_Premultiplied);
|
||||
image.fill(Qt::transparent);
|
||||
image.setDevicePixelRatio(style::DevicePixelRatio());
|
||||
{
|
||||
auto p = QPainter(&image);
|
||||
p.translate(0, lineWidth); // Bad.
|
||||
Paint(
|
||||
p,
|
||||
font,
|
||||
username,
|
||||
state->bgs.current(),
|
||||
backgroundMargins,
|
||||
qrImage,
|
||||
qrRect,
|
||||
qrMaxSize,
|
||||
introQrPixel,
|
||||
profileQrBackgroundRadius,
|
||||
textMaxHeight,
|
||||
photoSize,
|
||||
backgroundToggled);
|
||||
|
||||
if (userpicToggled) {
|
||||
p.drawImage((resultSize.width() - photoSize) / 2, 0, top);
|
||||
}
|
||||
}
|
||||
crl::on_main(weak, [=] {
|
||||
state->saveButtonBusy = false;
|
||||
auto mime = std::make_unique<QMimeData>();
|
||||
mime->setImageData(std::move(image));
|
||||
QGuiApplication::clipboard()->setMimeData(mime.release());
|
||||
show->showToast(tr::lng_group_invite_qr_copied(tr::now));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (const auto saveButton = state->saveButton) {
|
||||
using namespace Info::Statistics;
|
||||
const auto loadingAnimation = InfiniteRadialAnimationWidget(
|
||||
saveButton,
|
||||
saveButton->height() / 2);
|
||||
AddChildToWidgetCenter(saveButton, loadingAnimation);
|
||||
loadingAnimation->showOn(state->saveButtonBusy.value());
|
||||
}
|
||||
|
||||
box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
void DefaultShowFillPeerQrBoxCallback(
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
PeerData *peer) {
|
||||
if (peer && !peer->username().isEmpty()) {
|
||||
show->show(Box(Ui::FillPeerQrBox, peer, std::nullopt, nullptr));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
27
Telegram/SourceFiles/ui/boxes/peer_qr_box.h
Normal file
27
Telegram/SourceFiles/ui/boxes/peer_qr_box.h
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
class PeerData;
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class GenericBox;
|
||||
class Show;
|
||||
|
||||
void DefaultShowFillPeerQrBoxCallback(
|
||||
std::shared_ptr<Ui::Show> show,
|
||||
PeerData *peer);
|
||||
|
||||
void FillPeerQrBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
PeerData *peer,
|
||||
std::optional<QString> customLink,
|
||||
rpl::producer<QString> about);
|
||||
|
||||
} // namespace Ui
|
||||
149
Telegram/SourceFiles/ui/boxes/rate_call_box.cpp
Normal file
149
Telegram/SourceFiles/ui/boxes/rate_call_box.cpp
Normal file
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/boxes/rate_call_box.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_calls.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kMaxRating = 5;
|
||||
constexpr auto kRateCallCommentLengthMax = 200;
|
||||
|
||||
} // namespace
|
||||
|
||||
RateCallBox::RateCallBox(QWidget*, InputSubmitSettings sendWay)
|
||||
: _sendWay(sendWay) {
|
||||
}
|
||||
|
||||
void RateCallBox::prepare() {
|
||||
setTitle(tr::lng_call_rate_label());
|
||||
addButton(tr::lng_cancel(), [=] { closeBox(); });
|
||||
|
||||
for (auto i = 0; i < kMaxRating; ++i) {
|
||||
_stars.emplace_back(this, st::callRatingStar);
|
||||
_stars.back()->setClickedCallback([this, value = i + 1] {
|
||||
ratingChanged(value);
|
||||
});
|
||||
_stars.back()->show();
|
||||
}
|
||||
|
||||
updateMaxHeight();
|
||||
}
|
||||
|
||||
void RateCallBox::resizeEvent(QResizeEvent *e) {
|
||||
BoxContent::resizeEvent(e);
|
||||
|
||||
const auto starsWidth = (_stars.size() * st::callRatingStar.width);
|
||||
auto starLeft = (width() - starsWidth) / 2;
|
||||
const auto starTop = st::callRatingStarTop;
|
||||
for (auto &star : _stars) {
|
||||
star->moveToLeft(starLeft, starTop);
|
||||
starLeft += star->width();
|
||||
}
|
||||
if (_comment) {
|
||||
_comment->moveToLeft(
|
||||
st::callRatingPadding.left(),
|
||||
_stars.back()->bottomNoMargins() + st::callRatingCommentTop);
|
||||
}
|
||||
}
|
||||
|
||||
void RateCallBox::ratingChanged(int value) {
|
||||
Expects(value > 0 && value <= kMaxRating);
|
||||
if (!_rating) {
|
||||
clearButtons();
|
||||
addButton(tr::lng_send_button(), [=] { send(); });
|
||||
addButton(tr::lng_cancel(), [=] { closeBox(); });
|
||||
}
|
||||
_rating = value;
|
||||
|
||||
for (auto i = 0; i < kMaxRating; ++i) {
|
||||
_stars[i]->setIconOverride((i < value)
|
||||
? &st::callRatingStarFilled
|
||||
: nullptr);
|
||||
_stars[i]->setRippleColorOverride((i < value)
|
||||
? &st::lightButtonBgOver
|
||||
: nullptr);
|
||||
}
|
||||
if (value < kMaxRating) {
|
||||
if (!_comment) {
|
||||
_comment.create(
|
||||
this,
|
||||
st::callRatingComment,
|
||||
Ui::InputField::Mode::MultiLine,
|
||||
tr::lng_call_rate_comment());
|
||||
_comment->show();
|
||||
_comment->setSubmitSettings(_sendWay);
|
||||
_comment->setMaxLength(kRateCallCommentLengthMax);
|
||||
_comment->resize(
|
||||
width()
|
||||
- st::callRatingPadding.left()
|
||||
- st::callRatingPadding.right(),
|
||||
_comment->height());
|
||||
|
||||
updateMaxHeight();
|
||||
_comment->heightChanges(
|
||||
) | rpl::on_next([=] {
|
||||
commentResized();
|
||||
}, _comment->lifetime());
|
||||
_comment->submits(
|
||||
) | rpl::on_next([=] { send(); }, _comment->lifetime());
|
||||
_comment->cancelled(
|
||||
) | rpl::on_next([=] {
|
||||
closeBox();
|
||||
}, _comment->lifetime());
|
||||
}
|
||||
_comment->setFocusFast();
|
||||
} else if (_comment) {
|
||||
_comment.destroy();
|
||||
updateMaxHeight();
|
||||
}
|
||||
}
|
||||
|
||||
void RateCallBox::setInnerFocus() {
|
||||
if (_comment) {
|
||||
_comment->setFocusFast();
|
||||
} else {
|
||||
BoxContent::setInnerFocus();
|
||||
}
|
||||
}
|
||||
|
||||
void RateCallBox::commentResized() {
|
||||
updateMaxHeight();
|
||||
update();
|
||||
}
|
||||
|
||||
void RateCallBox::send() {
|
||||
Expects(_rating > 0 && _rating <= kMaxRating);
|
||||
_sends.fire({
|
||||
.rating = _rating,
|
||||
.comment = _comment ? _comment->getLastText().trimmed() : QString(),
|
||||
});
|
||||
}
|
||||
|
||||
void RateCallBox::updateMaxHeight() {
|
||||
auto newHeight = st::callRatingPadding.top()
|
||||
+ st::callRatingStarTop
|
||||
+ _stars.back()->heightNoMargins()
|
||||
+ st::callRatingPadding.bottom();
|
||||
if (_comment) {
|
||||
newHeight += st::callRatingCommentTop + _comment->height();
|
||||
}
|
||||
setDimensions(st::boxWideWidth, newHeight);
|
||||
}
|
||||
|
||||
rpl::producer<RateCallBox::Result> RateCallBox::sends() const {
|
||||
return _sends.events();
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
51
Telegram/SourceFiles/ui/boxes/rate_call_box.h
Normal file
51
Telegram/SourceFiles/ui/boxes/rate_call_box.h
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/layers/box_content.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class InputField;
|
||||
class IconButton;
|
||||
enum class InputSubmitSettings;
|
||||
|
||||
class RateCallBox : public Ui::BoxContent {
|
||||
public:
|
||||
RateCallBox(QWidget*, InputSubmitSettings sendWay);
|
||||
|
||||
struct Result {
|
||||
int rating = 0;
|
||||
QString comment;
|
||||
};
|
||||
|
||||
[[nodiscard]] rpl::producer<Result> sends() const;
|
||||
|
||||
protected:
|
||||
void prepare() override;
|
||||
void setInnerFocus() override;
|
||||
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
private:
|
||||
void updateMaxHeight();
|
||||
void ratingChanged(int value);
|
||||
void send();
|
||||
void commentResized();
|
||||
|
||||
const InputSubmitSettings _sendWay;
|
||||
int _rating = 0;
|
||||
|
||||
std::vector<object_ptr<Ui::IconButton>> _stars;
|
||||
object_ptr<Ui::InputField> _comment = { nullptr };
|
||||
|
||||
rpl::event_stream<Result> _sends;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Ui
|
||||
223
Telegram/SourceFiles/ui/boxes/report_box_graphics.cpp
Normal file
223
Telegram/SourceFiles/ui/boxes/report_box_graphics.cpp
Normal file
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/boxes/report_box_graphics.h"
|
||||
|
||||
#include "info/profile/info_profile_icon.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "lottie/lottie_icon.h"
|
||||
#include "settings/settings_common.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_info.h"
|
||||
#include "styles/style_channel_earn.h"
|
||||
#include "styles/style_settings.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kReportReasonLengthMax = 512;
|
||||
|
||||
using Source = ReportSource;
|
||||
using Reason = ReportReason;
|
||||
|
||||
} // namespace
|
||||
|
||||
void ReportReasonBox(
|
||||
not_null<GenericBox*> box,
|
||||
const style::ReportBox &st,
|
||||
ReportSource source,
|
||||
Fn<void(Reason)> done) {
|
||||
box->setTitle([&] {
|
||||
switch (source) {
|
||||
case Source::Message: return tr::lng_report_message_title();
|
||||
case Source::Channel: return tr::lng_report_title();
|
||||
case Source::Group: return tr::lng_report_group_title();
|
||||
case Source::Bot: return tr::lng_report_bot_title();
|
||||
case Source::ProfilePhoto:
|
||||
return tr::lng_report_profile_photo_title();
|
||||
case Source::ProfileVideo:
|
||||
return tr::lng_report_profile_video_title();
|
||||
case Source::GroupPhoto: return tr::lng_report_group_photo_title();
|
||||
case Source::GroupVideo: return tr::lng_report_group_video_title();
|
||||
case Source::ChannelPhoto:
|
||||
return tr::lng_report_channel_photo_title();
|
||||
case Source::ChannelVideo:
|
||||
return tr::lng_report_channel_video_title();
|
||||
case Source::Story:
|
||||
return tr::lng_report_story();
|
||||
}
|
||||
Unexpected("'source' in ReportReasonBox.");
|
||||
}());
|
||||
auto margin = style::margins{ 0, st::reportReasonTopSkip, 0, 0 };
|
||||
const auto add = [&](
|
||||
Reason reason,
|
||||
tr::phrase<> text,
|
||||
const style::icon &icon) {
|
||||
const auto layout = box->verticalLayout();
|
||||
const auto button = layout->add(
|
||||
object_ptr<Ui::SettingsButton>(layout.get(), text(), st.button),
|
||||
margin);
|
||||
margin = {};
|
||||
button->setClickedCallback([=] {
|
||||
done(reason);
|
||||
});
|
||||
const auto height = st.button.padding.top()
|
||||
+ st.button.height
|
||||
+ st.button.padding.bottom();
|
||||
object_ptr<Info::Profile::FloatingIcon>(
|
||||
button,
|
||||
icon,
|
||||
QPoint{
|
||||
st::infoSharedMediaButtonIconPosition.x(),
|
||||
(height - icon.height()) / 2,
|
||||
});
|
||||
};
|
||||
add(Reason::Spam, tr::lng_report_reason_spam, st.spam);
|
||||
if (source == Source::Channel
|
||||
|| source == Source::Group
|
||||
|| source == Source::Bot) {
|
||||
add(Reason::Fake, tr::lng_report_reason_fake, st.fake);
|
||||
}
|
||||
add(
|
||||
Reason::Violence,
|
||||
tr::lng_report_reason_violence,
|
||||
st.violence);
|
||||
add(
|
||||
Reason::ChildAbuse,
|
||||
tr::lng_report_reason_child_abuse,
|
||||
st.children);
|
||||
add(
|
||||
Reason::Pornography,
|
||||
tr::lng_report_reason_pornography,
|
||||
st.pornography);
|
||||
add(
|
||||
Reason::Copyright,
|
||||
tr::lng_report_reason_copyright,
|
||||
st.copyright);
|
||||
if (source == Source::Message || source == Source::Story) {
|
||||
add(
|
||||
Reason::IllegalDrugs,
|
||||
tr::lng_report_reason_illegal_drugs,
|
||||
st.drugs);
|
||||
add(
|
||||
Reason::PersonalDetails,
|
||||
tr::lng_report_reason_personal_details,
|
||||
st.personal);
|
||||
}
|
||||
add(Reason::Other, tr::lng_report_reason_other, st.other);
|
||||
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
void ReportDetailsBox(
|
||||
not_null<GenericBox*> box,
|
||||
const style::ReportBox &st,
|
||||
Fn<void(QString)> done) {
|
||||
box->setTitle(tr::lng_profile_report());
|
||||
AddReportDetailsIconButton(box);
|
||||
Ui::AddSkip(
|
||||
box->verticalLayout(),
|
||||
st::settingsBlockedListIconPadding.bottom());
|
||||
|
||||
box->addRow(
|
||||
object_ptr<FlatLabel>(
|
||||
box, // #TODO reports
|
||||
tr::lng_report_details_about(),
|
||||
st.label),
|
||||
{
|
||||
st::boxRowPadding.left(),
|
||||
st::boxPadding.top(),
|
||||
st::boxRowPadding.right(),
|
||||
st::boxPadding.bottom(),
|
||||
});
|
||||
const auto details = box->addRow(
|
||||
object_ptr<InputField>(
|
||||
box,
|
||||
st.field,
|
||||
InputField::Mode::MultiLine,
|
||||
tr::lng_report_details(),
|
||||
QString()));
|
||||
details->setMaxLength(kReportReasonLengthMax);
|
||||
box->setFocusCallback([=] {
|
||||
details->setFocusFast();
|
||||
});
|
||||
|
||||
const auto submit = [=] {
|
||||
const auto text = details->getLastText();
|
||||
done(text);
|
||||
};
|
||||
details->submits() | rpl::on_next(submit, details->lifetime());
|
||||
box->addButton(tr::lng_report_button(), submit);
|
||||
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
not_null<Ui::AbstractButton*> AddReportOptionButton(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
const QString &text,
|
||||
const style::ReportBox *stOverride) {
|
||||
const auto button = container->add(
|
||||
object_ptr<Ui::SettingsButton>(
|
||||
container,
|
||||
rpl::single(QString()),
|
||||
(stOverride ? stOverride : &st::defaultReportBox)->noIconButton));
|
||||
const auto textFg = (stOverride
|
||||
? stOverride->label
|
||||
: st::sponsoredReportLabel).textFg->c;
|
||||
const auto label = Ui::CreateChild<Ui::FlatLabel>(
|
||||
button,
|
||||
rpl::single(text),
|
||||
st::sponsoredReportLabel);
|
||||
label->setTextColorOverride(textFg);
|
||||
const auto icon = Ui::CreateChild<Ui::RpWidget>(button);
|
||||
icon->resize(st::settingsPremiumArrow.size());
|
||||
icon->paintRequest() | rpl::on_next([=, w = icon->width()] {
|
||||
auto p = Painter(icon);
|
||||
st::settingsPremiumArrow.paint(p, 0, 0, w, textFg);
|
||||
}, icon->lifetime());
|
||||
button->sizeValue() | rpl::on_next([=](const QSize &size) {
|
||||
const auto left = button->st().padding.left();
|
||||
const auto right = button->st().padding.right();
|
||||
icon->moveToRight(right, (size.height() - icon->height()) / 2);
|
||||
label->resizeToWidth(size.width()
|
||||
- icon->width()
|
||||
- left
|
||||
- st::settingsButtonRightSkip
|
||||
- right);
|
||||
label->moveToLeft(left, (size.height() - label->height()) / 2);
|
||||
button->resize(
|
||||
button->width(),
|
||||
rect::m::sum::v(button->st().padding) + label->height());
|
||||
}, button->lifetime());
|
||||
label->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
icon->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
return button;
|
||||
}
|
||||
|
||||
void AddReportDetailsIconButton(not_null<GenericBox*> box) {
|
||||
auto icon = Settings::CreateLottieIcon(
|
||||
box->verticalLayout(),
|
||||
{
|
||||
.name = u"blocked_peers_empty"_q,
|
||||
.sizeOverride = st::normalBoxLottieSize,
|
||||
},
|
||||
{});
|
||||
box->setShowFinishedCallback([animate = std::move(icon.animate)] {
|
||||
animate(anim::repeat::once);
|
||||
});
|
||||
box->addRow(std::move(icon.widget));
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
64
Telegram/SourceFiles/ui/boxes/report_box_graphics.h
Normal file
64
Telegram/SourceFiles/ui/boxes/report_box_graphics.h
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace style {
|
||||
struct ReportBox;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class AbstractButton;
|
||||
class GenericBox;
|
||||
class VerticalLayout;
|
||||
|
||||
enum class ReportSource {
|
||||
Message,
|
||||
Channel,
|
||||
Group,
|
||||
Bot,
|
||||
ProfilePhoto,
|
||||
ProfileVideo,
|
||||
GroupPhoto,
|
||||
GroupVideo,
|
||||
ChannelPhoto,
|
||||
ChannelVideo,
|
||||
Story,
|
||||
};
|
||||
|
||||
enum class ReportReason {
|
||||
Spam,
|
||||
Fake,
|
||||
Violence,
|
||||
ChildAbuse,
|
||||
Pornography,
|
||||
Copyright,
|
||||
IllegalDrugs,
|
||||
PersonalDetails,
|
||||
Other,
|
||||
};
|
||||
|
||||
void ReportReasonBox(
|
||||
not_null<GenericBox*> box,
|
||||
const style::ReportBox &st,
|
||||
ReportSource source,
|
||||
Fn<void(ReportReason)> done);
|
||||
|
||||
void ReportDetailsBox(
|
||||
not_null<GenericBox*> box,
|
||||
const style::ReportBox &st,
|
||||
Fn<void(QString)> done);
|
||||
|
||||
[[nodiscard]] not_null<Ui::AbstractButton*> AddReportOptionButton(
|
||||
not_null<Ui::VerticalLayout*> container,
|
||||
const QString &text,
|
||||
const style::ReportBox *stOverride);
|
||||
|
||||
void AddReportDetailsIconButton(not_null<GenericBox*> box);
|
||||
|
||||
} // namespace Ui
|
||||
219
Telegram/SourceFiles/ui/boxes/show_or_premium_box.cpp
Normal file
219
Telegram/SourceFiles/ui/boxes/show_or_premium_box.cpp
Normal file
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/boxes/show_or_premium_box.h"
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "lottie/lottie_icon.h"
|
||||
#include "settings/settings_common.h"
|
||||
#include "ui/effects/premium_graphics.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/gradient_round_button.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_premium.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_settings.h"
|
||||
|
||||
namespace Ui {
|
||||
namespace {
|
||||
|
||||
constexpr auto kShowOrLineOpacity = 0.3;
|
||||
|
||||
} // namespace
|
||||
|
||||
object_ptr<RpWidget> MakeShowOrLabel(
|
||||
not_null<RpWidget*> parent,
|
||||
rpl::producer<QString> text) {
|
||||
auto result = object_ptr<FlatLabel>(
|
||||
parent,
|
||||
std::move(text),
|
||||
st::showOrLabel);
|
||||
const auto raw = result.data();
|
||||
|
||||
raw->paintRequest(
|
||||
) | rpl::on_next([=] {
|
||||
auto p = QPainter(raw);
|
||||
|
||||
const auto full = st::showOrLineWidth;
|
||||
const auto left = (raw->width() - full) / 2;
|
||||
const auto text = raw->naturalWidth() + 2 * st::showOrLabelSkip;
|
||||
const auto fill = (full - text) / 2;
|
||||
const auto stroke = st::lineWidth;
|
||||
const auto top = st::showOrLineTop;
|
||||
p.setOpacity(kShowOrLineOpacity);
|
||||
p.fillRect(left, top, fill, stroke, st::windowSubTextFg);
|
||||
const auto start = left + full - fill;
|
||||
p.fillRect(start, top, fill, stroke, st::windowSubTextFg);
|
||||
}, raw->lifetime());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void ShowOrPremiumBox(
|
||||
not_null<GenericBox*> box,
|
||||
ShowOrPremium type,
|
||||
QString shortName,
|
||||
Fn<void()> justShow,
|
||||
Fn<void()> toPremium) {
|
||||
struct Skin {
|
||||
rpl::producer<QString> showTitle;
|
||||
rpl::producer<TextWithEntities> showAbout;
|
||||
rpl::producer<QString> showButton;
|
||||
rpl::producer<QString> orPremium;
|
||||
rpl::producer<QString> premiumTitle;
|
||||
rpl::producer<TextWithEntities> premiumAbout;
|
||||
rpl::producer<QString> premiumButton;
|
||||
QString toast;
|
||||
QString lottie;
|
||||
};
|
||||
auto skin = (type == ShowOrPremium::LastSeen)
|
||||
? Skin{
|
||||
tr::lng_lastseen_show_title(),
|
||||
tr::lng_lastseen_show_about(
|
||||
lt_user,
|
||||
rpl::single(TextWithEntities{ shortName }),
|
||||
tr::rich),
|
||||
tr::lng_lastseen_show_button(),
|
||||
tr::lng_lastseen_or(),
|
||||
tr::lng_lastseen_premium_title(),
|
||||
tr::lng_lastseen_premium_about(
|
||||
lt_user,
|
||||
rpl::single(TextWithEntities{ shortName }),
|
||||
tr::rich),
|
||||
tr::lng_lastseen_premium_button(),
|
||||
tr::lng_lastseen_shown_toast(tr::now),
|
||||
u"show_or_premium_lastseen"_q,
|
||||
}
|
||||
: Skin{
|
||||
tr::lng_readtime_show_title(),
|
||||
tr::lng_readtime_show_about(
|
||||
lt_user,
|
||||
rpl::single(TextWithEntities{ shortName }),
|
||||
tr::rich),
|
||||
tr::lng_readtime_show_button(),
|
||||
tr::lng_readtime_or(),
|
||||
tr::lng_readtime_premium_title(),
|
||||
tr::lng_readtime_premium_about(
|
||||
lt_user,
|
||||
rpl::single(TextWithEntities{ shortName }),
|
||||
tr::rich),
|
||||
tr::lng_readtime_premium_button(),
|
||||
tr::lng_readtime_shown_toast(tr::now),
|
||||
u"show_or_premium_readtime"_q,
|
||||
};
|
||||
|
||||
box->setStyle(st::showOrBox);
|
||||
box->setWidth(st::boxWideWidth);
|
||||
box->addTopButton(st::boxTitleClose, [=] {
|
||||
box->closeBox();
|
||||
});
|
||||
|
||||
const auto buttonPadding = QMargins(
|
||||
st::showOrBox.buttonPadding.left(),
|
||||
0,
|
||||
st::showOrBox.buttonPadding.right(),
|
||||
0);
|
||||
|
||||
auto icon = Settings::CreateLottieIcon(
|
||||
box,
|
||||
{
|
||||
.name = skin.lottie,
|
||||
.sizeOverride = st::normalBoxLottieSize
|
||||
- Size(st::showOrTitleIconMargin * 2),
|
||||
},
|
||||
{ 0, st::showOrTitleIconMargin, 0, st::showOrTitleIconMargin });
|
||||
Settings::AddLottieIconWithCircle(
|
||||
box->verticalLayout(),
|
||||
std::move(icon.widget),
|
||||
st::settingsBlockedListIconPadding,
|
||||
st::normalBoxLottieSize);
|
||||
Ui::AddSkip(box->verticalLayout());
|
||||
box->addRow(
|
||||
object_ptr<FlatLabel>(
|
||||
box,
|
||||
std::move(skin.showTitle),
|
||||
st::boostCenteredTitle),
|
||||
st::showOrTitlePadding + buttonPadding,
|
||||
style::al_top);
|
||||
box->addRow(
|
||||
object_ptr<FlatLabel>(
|
||||
box,
|
||||
std::move(skin.showAbout),
|
||||
st::boostText),
|
||||
st::showOrAboutPadding + buttonPadding,
|
||||
style::al_top);
|
||||
const auto show = box->addRow(
|
||||
object_ptr<RoundButton>(
|
||||
box,
|
||||
std::move(skin.showButton),
|
||||
st::showOrShowButton),
|
||||
buttonPadding);
|
||||
show->setTextTransform(RoundButton::TextTransform::NoTransform);
|
||||
box->addRow(
|
||||
MakeShowOrLabel(box, std::move(skin.orPremium)),
|
||||
st::showOrLabelPadding + buttonPadding,
|
||||
style::al_justify);
|
||||
box->addRow(
|
||||
object_ptr<FlatLabel>(
|
||||
box,
|
||||
std::move(skin.premiumTitle),
|
||||
st::boostCenteredTitle),
|
||||
st::showOrTitlePadding + buttonPadding,
|
||||
style::al_top);
|
||||
box->addRow(
|
||||
object_ptr<FlatLabel>(
|
||||
box,
|
||||
std::move(skin.premiumAbout),
|
||||
st::boostText),
|
||||
st::showOrPremiumAboutPadding + buttonPadding,
|
||||
style::al_top);
|
||||
|
||||
const auto premium = CreateChild<GradientButton>(
|
||||
box.get(),
|
||||
Premium::ButtonGradientStops());
|
||||
|
||||
premium->resize(st::showOrShowButton.width, st::showOrShowButton.height);
|
||||
|
||||
const auto label = CreateChild<FlatLabel>(
|
||||
premium,
|
||||
std::move(skin.premiumButton),
|
||||
st::premiumPreviewButtonLabel);
|
||||
label->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
rpl::combine(
|
||||
premium->widthValue(),
|
||||
label->widthValue()
|
||||
) | rpl::on_next([=](int outer, int width) {
|
||||
label->moveToLeft(
|
||||
(outer - width) / 2,
|
||||
st::premiumPreviewBox.button.textTop,
|
||||
outer);
|
||||
}, label->lifetime());
|
||||
|
||||
box->setShowFinishedCallback([=, animate = std::move(icon.animate)] {
|
||||
premium->startGlareAnimation();
|
||||
animate(anim::repeat::once);
|
||||
});
|
||||
|
||||
box->addButton(
|
||||
object_ptr<AbstractButton>::fromRaw(premium));
|
||||
|
||||
show->setClickedCallback([box, justShow, toast = skin.toast] {
|
||||
justShow();
|
||||
box->uiShow()->showToast(toast);
|
||||
box->closeBox();
|
||||
});
|
||||
premium->setClickedCallback(std::move(toPremium));
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
32
Telegram/SourceFiles/ui/boxes/show_or_premium_box.h
Normal file
32
Telegram/SourceFiles/ui/boxes/show_or_premium_box.h
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/object_ptr.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class RpWidget;
|
||||
class GenericBox;
|
||||
|
||||
enum class ShowOrPremium : uchar {
|
||||
LastSeen,
|
||||
ReadTime,
|
||||
};
|
||||
void ShowOrPremiumBox(
|
||||
not_null<GenericBox*> box,
|
||||
ShowOrPremium type,
|
||||
QString shortName,
|
||||
Fn<void()> justShow,
|
||||
Fn<void()> toPremium);
|
||||
|
||||
[[nodiscard]] object_ptr<RpWidget> MakeShowOrLabel(
|
||||
not_null<RpWidget*> parent,
|
||||
rpl::producer<QString> text);
|
||||
|
||||
} // namespace Ui
|
||||
55
Telegram/SourceFiles/ui/boxes/single_choice_box.cpp
Normal file
55
Telegram/SourceFiles/ui/boxes/single_choice_box.cpp
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/boxes/single_choice_box.h"
|
||||
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "ui/wrap/padding_wrap.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
void SingleChoiceBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
SingleChoiceBoxArgs &&args) {
|
||||
box->setTitle(std::move(args.title));
|
||||
|
||||
box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); });
|
||||
|
||||
const auto group = std::make_shared<Ui::RadiobuttonGroup>(
|
||||
args.initialSelection);
|
||||
|
||||
const auto layout = box->verticalLayout();
|
||||
layout->add(object_ptr<Ui::FixedHeightWidget>(
|
||||
layout,
|
||||
st::boxOptionListPadding.top() + st::autolockButton.margin.top()));
|
||||
auto &&ints = ranges::views::ints(0, ranges::unreachable);
|
||||
for (const auto &[i, text] : ranges::views::zip(ints, args.options)) {
|
||||
layout->add(
|
||||
object_ptr<Ui::Radiobutton>(
|
||||
layout,
|
||||
group,
|
||||
i,
|
||||
text,
|
||||
args.st ? *args.st : st::defaultBoxCheckbox,
|
||||
args.radioSt ? *args.radioSt : st::defaultRadio),
|
||||
QMargins(
|
||||
st::boxPadding.left() + st::boxOptionListPadding.left(),
|
||||
0,
|
||||
st::boxPadding.right(),
|
||||
st::boxOptionListSkip));
|
||||
}
|
||||
const auto callback = args.callback.value();
|
||||
group->setChangedCallback([=](int value) {
|
||||
const auto weak = base::make_weak(box);
|
||||
callback(value);
|
||||
if (weak) {
|
||||
box->closeBox();
|
||||
}
|
||||
});
|
||||
}
|
||||
32
Telegram/SourceFiles/ui/boxes/single_choice_box.h
Normal file
32
Telegram/SourceFiles/ui/boxes/single_choice_box.h
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "base/required.h"
|
||||
|
||||
namespace style {
|
||||
struct Checkbox;
|
||||
struct Radio;
|
||||
} // namespace style
|
||||
|
||||
struct SingleChoiceBoxArgs {
|
||||
template <typename T>
|
||||
using required = base::required<T>;
|
||||
|
||||
required<rpl::producer<QString>> title;
|
||||
const std::vector<QString> &options;
|
||||
int initialSelection = 0;
|
||||
required<Fn<void(int)>> callback;
|
||||
const style::Checkbox *st = nullptr;
|
||||
const style::Radio *radioSt = nullptr;
|
||||
};
|
||||
|
||||
void SingleChoiceBox(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
SingleChoiceBoxArgs &&args);
|
||||
133
Telegram/SourceFiles/ui/boxes/time_picker_box.cpp
Normal file
133
Telegram/SourceFiles/ui/boxes/time_picker_box.cpp
Normal file
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "ui/boxes/time_picker_box.h"
|
||||
|
||||
#include "base/event_filter.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/effects/animation_value.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/widgets/vertical_drum_picker.h"
|
||||
#include "styles/style_chat_helpers.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
std::vector<TimeId> DefaultTimePickerValues() {
|
||||
return {
|
||||
(60 * 15),
|
||||
(60 * 30),
|
||||
(3600 * 1),
|
||||
(3600 * 2),
|
||||
(3600 * 3),
|
||||
(3600 * 4),
|
||||
(3600 * 8),
|
||||
(3600 * 12),
|
||||
(86400 * 1),
|
||||
(86400 * 2),
|
||||
(86400 * 3),
|
||||
(86400 * 7 * 1),
|
||||
(86400 * 7 * 2),
|
||||
(86400 * 31 * 1),
|
||||
(86400 * 31 * 2),
|
||||
(86400 * 31 * 3),
|
||||
};
|
||||
}
|
||||
|
||||
Fn<TimeId()> TimePickerBox(
|
||||
not_null<GenericBox*> box,
|
||||
std::vector<TimeId> values,
|
||||
std::vector<QString> phrases,
|
||||
TimeId startValue) {
|
||||
Expects(phrases.size() == values.size());
|
||||
|
||||
const auto startIndex = [&, &v = startValue] {
|
||||
const auto it = ranges::lower_bound(values, v);
|
||||
if (it == begin(values)) {
|
||||
return 0;
|
||||
}
|
||||
const auto left = *(it - 1);
|
||||
const auto right = *it;
|
||||
const auto shift = (std::abs(v - left) < std::abs(v - right))
|
||||
? -1
|
||||
: 0;
|
||||
return int(std::distance(begin(values), it - shift));
|
||||
}();
|
||||
|
||||
const auto content = box->addRow(object_ptr<Ui::FixedHeightWidget>(
|
||||
box,
|
||||
st::historyMessagesTTLPickerHeight));
|
||||
|
||||
const auto font = st::boxTextFont;
|
||||
const auto maxPhraseWidth = [&] {
|
||||
// We have to use QFontMetricsF instead of
|
||||
// FontData::width for more precise calculation.
|
||||
const auto mf = QFontMetricsF(font->f);
|
||||
const auto maxPhrase = ranges::max_element(
|
||||
phrases,
|
||||
std::less<>(),
|
||||
[&](const QString &s) { return mf.horizontalAdvance(s); });
|
||||
return std::ceil(mf.horizontalAdvance(*maxPhrase));
|
||||
}();
|
||||
const auto itemHeight = st::historyMessagesTTLPickerItemHeight;
|
||||
auto paintCallback = Ui::VerticalDrumPicker::DefaultPaintCallback(
|
||||
font,
|
||||
itemHeight,
|
||||
[=](QPainter &p, QRectF r, int index) {
|
||||
p.drawText(r, phrases[index], style::al_center);
|
||||
});
|
||||
|
||||
const auto picker = Ui::CreateChild<Ui::VerticalDrumPicker>(
|
||||
content,
|
||||
std::move(paintCallback),
|
||||
phrases.size(),
|
||||
itemHeight,
|
||||
startIndex);
|
||||
|
||||
content->sizeValue(
|
||||
) | rpl::on_next([=](const QSize &s) {
|
||||
picker->resize(maxPhraseWidth, s.height());
|
||||
picker->moveToLeft((s.width() - picker->width()) / 2, 0);
|
||||
}, content->lifetime());
|
||||
|
||||
content->paintRequest(
|
||||
) | rpl::on_next([=](const QRect &r) {
|
||||
auto p = QPainter(content);
|
||||
|
||||
p.fillRect(r, Qt::transparent);
|
||||
|
||||
const auto lineRect = QRect(
|
||||
0,
|
||||
content->height() / 2,
|
||||
content->width(),
|
||||
st::defaultInputField.borderActive);
|
||||
p.fillRect(lineRect.translated(0, itemHeight / 2), st::activeLineFg);
|
||||
p.fillRect(lineRect.translated(0, -itemHeight / 2), st::activeLineFg);
|
||||
}, content->lifetime());
|
||||
|
||||
base::install_event_filter(content, [=](not_null<QEvent*> e) {
|
||||
if ((e->type() == QEvent::MouseButtonPress)
|
||||
|| (e->type() == QEvent::MouseButtonRelease)
|
||||
|| (e->type() == QEvent::MouseMove)) {
|
||||
picker->handleMouseEvent(static_cast<QMouseEvent*>(e.get()));
|
||||
} else if (e->type() == QEvent::Wheel) {
|
||||
picker->handleWheelEvent(static_cast<QWheelEvent*>(e.get()));
|
||||
}
|
||||
return base::EventFilterResult::Continue;
|
||||
});
|
||||
base::install_event_filter(box, [=](not_null<QEvent*> e) {
|
||||
if (e->type() == QEvent::KeyPress) {
|
||||
picker->handleKeyEvent(static_cast<QKeyEvent*>(e.get()));
|
||||
}
|
||||
return base::EventFilterResult::Continue;
|
||||
});
|
||||
|
||||
return [=] { return values[picker->index()]; };
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
22
Telegram/SourceFiles/ui/boxes/time_picker_box.h
Normal file
22
Telegram/SourceFiles/ui/boxes/time_picker_box.h
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class GenericBox;
|
||||
|
||||
[[nodiscard]] std::vector<TimeId> DefaultTimePickerValues();
|
||||
|
||||
[[nodiscard]] Fn<TimeId()> TimePickerBox(
|
||||
not_null<GenericBox*> box,
|
||||
std::vector<TimeId> values,
|
||||
std::vector<QString> phrases,
|
||||
TimeId startValue);
|
||||
|
||||
} // namespace Ui
|
||||
Reference in New Issue
Block a user