mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-06-07 23:53:58 +02:00
Implement filtering of resale gifts.
This commit is contained in:
parent
904e531113
commit
e629460942
8 changed files with 964 additions and 1 deletions
|
@ -3590,6 +3590,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
"lng_gift_wear_end_toast" = "You took off {name}";
|
"lng_gift_wear_end_toast" = "You took off {name}";
|
||||||
"lng_gift_many_pinned_title" = "Too Many Pinned Gifts";
|
"lng_gift_many_pinned_title" = "Too Many Pinned Gifts";
|
||||||
"lng_gift_many_pinned_choose" = "Select a gift to unpin below";
|
"lng_gift_many_pinned_choose" = "Select a gift to unpin below";
|
||||||
|
"lng_gift_resale_price" = "Price";
|
||||||
|
"lng_gift_resale_number" = "Number";
|
||||||
|
"lng_gift_resale_date" = "Date";
|
||||||
|
"lng_gift_resale_sort_price" = "Sort by Price";
|
||||||
|
"lng_gift_resale_sort_date" = "Sort by Date";
|
||||||
|
"lng_gift_resale_sort_number" = "Sort by Number";
|
||||||
|
"lng_gift_resale_filter_all" = "Select All";
|
||||||
|
"lng_gift_resale_model" = "Model";
|
||||||
|
"lng_gift_resale_models#one" = "{count} Model";
|
||||||
|
"lng_gift_resale_models#other" = "{count} Models";
|
||||||
|
"lng_gift_resale_backdrop" = "Backdrop";
|
||||||
|
"lng_gift_resale_backdrops#one" = "{count} Backdrop";
|
||||||
|
"lng_gift_resale_backdrops#other" = "{count} Backdrops";
|
||||||
|
"lng_gift_resale_symbol" = "Symbol";
|
||||||
|
"lng_gift_resale_symbols#one" = "{count} Symbol";
|
||||||
|
"lng_gift_resale_symbols#other" = "{count} Symbols";
|
||||||
|
|
||||||
"lng_accounts_limit_title" = "Limit Reached";
|
"lng_accounts_limit_title" = "Limit Reached";
|
||||||
"lng_accounts_limit1#one" = "You have reached the limit of **{count}** connected account.";
|
"lng_accounts_limit1#one" = "You have reached the limit of **{count}** connected account.";
|
||||||
|
|
|
@ -812,6 +812,7 @@ std::optional<Data::StarGift> FromTL(
|
||||||
.starsToUpgrade = int64(data.vupgrade_stars().value_or_empty()),
|
.starsToUpgrade = int64(data.vupgrade_stars().value_or_empty()),
|
||||||
.starsResellMin = int64(resellPrice),
|
.starsResellMin = int64(resellPrice),
|
||||||
.document = document,
|
.document = document,
|
||||||
|
.resellTitle = qs(data.vtitle().value_or_empty()),
|
||||||
.resellCount = int(data.vavailability_resale().value_or_empty()),
|
.resellCount = int(data.vavailability_resale().value_or_empty()),
|
||||||
.limitedLeft = remaining.value_or_empty(),
|
.limitedLeft = remaining.value_or_empty(),
|
||||||
.limitedCount = total.value_or_empty(),
|
.limitedCount = total.value_or_empty(),
|
||||||
|
@ -939,7 +940,7 @@ Data::UniqueGiftPattern FromTL(
|
||||||
}
|
}
|
||||||
|
|
||||||
Data::UniqueGiftBackdrop FromTL(const MTPDstarGiftAttributeBackdrop &data) {
|
Data::UniqueGiftBackdrop FromTL(const MTPDstarGiftAttributeBackdrop &data) {
|
||||||
auto result = Data::UniqueGiftBackdrop();
|
auto result = Data::UniqueGiftBackdrop{ .id = data.vbackdrop_id().v };
|
||||||
result.name = qs(data.vname());
|
result.name = qs(data.vname());
|
||||||
result.rarityPermille = data.vrarity_permille().v;
|
result.rarityPermille = data.vrarity_permille().v;
|
||||||
result.centerColor = Ui::ColorFromSerialized(
|
result.centerColor = Ui::ColorFromSerialized(
|
||||||
|
|
|
@ -57,6 +57,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "lottie/lottie_single_player.h"
|
#include "lottie/lottie_single_player.h"
|
||||||
#include "main/main_app_config.h"
|
#include "main/main_app_config.h"
|
||||||
#include "main/main_session.h"
|
#include "main/main_session.h"
|
||||||
|
#include "menu/gift_resale_filter.h"
|
||||||
#include "payments/payments_form.h"
|
#include "payments/payments_form.h"
|
||||||
#include "payments/payments_checkout_process.h"
|
#include "payments/payments_checkout_process.h"
|
||||||
#include "payments/payments_non_panel_process.h"
|
#include "payments/payments_non_panel_process.h"
|
||||||
|
@ -117,6 +118,8 @@ constexpr auto kSwitchUpgradeCoverInterval = 3 * crl::time(1000);
|
||||||
constexpr auto kCrossfadeDuration = crl::time(400);
|
constexpr auto kCrossfadeDuration = crl::time(400);
|
||||||
constexpr auto kUpgradeDoneToastDuration = 4 * crl::time(1000);
|
constexpr auto kUpgradeDoneToastDuration = 4 * crl::time(1000);
|
||||||
constexpr auto kGiftsPreloadTimeout = 3 * crl::time(1000);
|
constexpr auto kGiftsPreloadTimeout = 3 * crl::time(1000);
|
||||||
|
constexpr auto kResaleGiftsPerPage = 50;
|
||||||
|
constexpr auto kFiltersCount = 4;
|
||||||
|
|
||||||
using namespace HistoryView;
|
using namespace HistoryView;
|
||||||
using namespace Info::PeerGifts;
|
using namespace Info::PeerGifts;
|
||||||
|
@ -128,6 +131,20 @@ enum class PickType {
|
||||||
};
|
};
|
||||||
using PickCallback = Fn<void(not_null<PeerData*>, PickType)>;
|
using PickCallback = Fn<void(not_null<PeerData*>, PickType)>;
|
||||||
|
|
||||||
|
enum class AttributeIdType {
|
||||||
|
Model,
|
||||||
|
Pattern,
|
||||||
|
Backdrop,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AttributeId {
|
||||||
|
uint64 value = 0;
|
||||||
|
AttributeIdType type = AttributeIdType::Model;
|
||||||
|
|
||||||
|
friend inline auto operator<=>(AttributeId, AttributeId) = default;
|
||||||
|
friend inline bool operator==(AttributeId, AttributeId) = default;
|
||||||
|
};
|
||||||
|
|
||||||
struct PremiumGiftsDescriptor {
|
struct PremiumGiftsDescriptor {
|
||||||
std::vector<GiftTypePremium> list;
|
std::vector<GiftTypePremium> list;
|
||||||
std::shared_ptr<Api::PremiumGiftCodeOptions> api;
|
std::shared_ptr<Api::PremiumGiftCodeOptions> api;
|
||||||
|
@ -138,6 +155,53 @@ struct MyGiftsDescriptor {
|
||||||
QString offset;
|
QString offset;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct ModelCount {
|
||||||
|
Data::UniqueGiftModel model;
|
||||||
|
int count = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BackdropCount {
|
||||||
|
Data::UniqueGiftBackdrop backdrop;
|
||||||
|
int count = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PatternCount {
|
||||||
|
Data::UniqueGiftPattern pattern;
|
||||||
|
int count = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ResaleSort {
|
||||||
|
Date,
|
||||||
|
Price,
|
||||||
|
Number,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ResaleGiftsDescriptor {
|
||||||
|
uint64 giftId = 0;
|
||||||
|
QString title;
|
||||||
|
QString offset;
|
||||||
|
std::vector<Data::StarGift> list;
|
||||||
|
std::vector<ModelCount> models;
|
||||||
|
std::vector<BackdropCount> backdrops;
|
||||||
|
std::vector<PatternCount> patterns;
|
||||||
|
uint64 attributesHash = 0;
|
||||||
|
int count = 0;
|
||||||
|
ResaleSort sort = ResaleSort::Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ResaleFilter {
|
||||||
|
uint64 attributesHash = 0;
|
||||||
|
base::flat_set<AttributeId> attributes;
|
||||||
|
ResaleSort sort = ResaleSort::Date;
|
||||||
|
|
||||||
|
friend inline auto operator<=>(
|
||||||
|
const ResaleFilter &,
|
||||||
|
const ResaleFilter &) = default;
|
||||||
|
friend inline bool operator==(
|
||||||
|
const ResaleFilter &,
|
||||||
|
const ResaleFilter &) = default;
|
||||||
|
};
|
||||||
|
|
||||||
struct GiftsDescriptor {
|
struct GiftsDescriptor {
|
||||||
std::vector<GiftDescriptor> list;
|
std::vector<GiftDescriptor> list;
|
||||||
std::shared_ptr<Api::PremiumGiftCodeOptions> api;
|
std::shared_ptr<Api::PremiumGiftCodeOptions> api;
|
||||||
|
@ -267,6 +331,53 @@ private:
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] AttributeId FromTL(const MTPStarGiftAttributeId &id) {
|
||||||
|
return id.match([&](const MTPDstarGiftAttributeIdBackdrop &data) {
|
||||||
|
return AttributeId{
|
||||||
|
.value = uint64(uint32(data.vbackdrop_id().v)),
|
||||||
|
.type = AttributeIdType::Backdrop,
|
||||||
|
};
|
||||||
|
}, [&](const MTPDstarGiftAttributeIdModel &data) {
|
||||||
|
return AttributeId{
|
||||||
|
.value = data.vdocument_id().v,
|
||||||
|
.type = AttributeIdType::Model,
|
||||||
|
};
|
||||||
|
}, [&](const MTPDstarGiftAttributeIdPattern &data) {
|
||||||
|
return AttributeId{
|
||||||
|
.value = data.vdocument_id().v,
|
||||||
|
.type = AttributeIdType::Pattern,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] MTPStarGiftAttributeId AttributeToTL(AttributeId id) {
|
||||||
|
switch (id.type) {
|
||||||
|
case AttributeIdType::Backdrop:
|
||||||
|
return MTP_starGiftAttributeIdBackdrop(
|
||||||
|
MTP_int(int32(uint32(id.value))));
|
||||||
|
case AttributeIdType::Model:
|
||||||
|
return MTP_starGiftAttributeIdModel(MTP_long(id.value));
|
||||||
|
case AttributeIdType::Pattern:
|
||||||
|
return MTP_starGiftAttributeIdPattern(MTP_long(id.value));
|
||||||
|
}
|
||||||
|
Unexpected("Invalid attribute id type");
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] AttributeId IdFor(const Data::UniqueGiftBackdrop &value) {
|
||||||
|
return {
|
||||||
|
.value = uint64(uint32(value.id)),
|
||||||
|
.type = AttributeIdType::Backdrop,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] AttributeId IdFor(const Data::UniqueGiftModel &value) {
|
||||||
|
return { .value = value.document->id, .type = AttributeIdType::Model };
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] AttributeId IdFor(const Data::UniqueGiftPattern &value) {
|
||||||
|
return { .value = value.document->id, .type = AttributeIdType::Pattern };
|
||||||
|
}
|
||||||
|
|
||||||
[[nodiscard]] bool SortForBirthday(not_null<PeerData*> peer) {
|
[[nodiscard]] bool SortForBirthday(not_null<PeerData*> peer) {
|
||||||
const auto user = peer->asUser();
|
const auto user = peer->asUser();
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
@ -948,6 +1059,405 @@ void PreviewWrap::paintEvent(QPaintEvent *e) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] Text::String ResaleTabText(QString text) {
|
||||||
|
auto result = Text::String();
|
||||||
|
result.setMarkedText(
|
||||||
|
st::semiboldTextStyle,
|
||||||
|
TextWithEntities{ text }.append(
|
||||||
|
Ui::Text::IconEmoji(&st::giftBoxResaleTabsDropdown)),
|
||||||
|
kMarkupTextOptions);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] Text::String SortModeText(ResaleSort mode) {
|
||||||
|
if (mode == ResaleSort::Number) {
|
||||||
|
return ResaleTabText(tr::lng_gift_resale_number(tr::now));
|
||||||
|
} else if (mode == ResaleSort::Price) {
|
||||||
|
return ResaleTabText(tr::lng_gift_resale_price(tr::now));
|
||||||
|
}
|
||||||
|
return ResaleTabText(tr::lng_gift_resale_date(tr::now));
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ResaleTabs {
|
||||||
|
rpl::producer<ResaleFilter> filter;
|
||||||
|
object_ptr<RpWidget> widget;
|
||||||
|
};
|
||||||
|
[[nodiscard]] ResaleTabs MakeResaleTabs(
|
||||||
|
not_null<Window::SessionController*> window,
|
||||||
|
not_null<PeerData*> peer,
|
||||||
|
const ResaleGiftsDescriptor &info,
|
||||||
|
rpl::producer<ResaleFilter> filter) {
|
||||||
|
auto widget = object_ptr<RpWidget>((QWidget*)nullptr);
|
||||||
|
const auto raw = widget.data();
|
||||||
|
|
||||||
|
struct Button {
|
||||||
|
QRect geometry;
|
||||||
|
Text::String text;
|
||||||
|
};
|
||||||
|
struct State {
|
||||||
|
rpl::variable<ResaleFilter> filter;
|
||||||
|
rpl::variable<int> fullWidth;
|
||||||
|
std::vector<Button> buttons;
|
||||||
|
base::unique_qptr<Ui::PopupMenu> menu;
|
||||||
|
ResaleGiftsDescriptor lists;
|
||||||
|
int dragx = 0;
|
||||||
|
int pressx = 0;
|
||||||
|
float64 dragscroll = 0.;
|
||||||
|
float64 scroll = 0.;
|
||||||
|
int scrollMax = 0;
|
||||||
|
int selected = -1;
|
||||||
|
int pressed = -1;
|
||||||
|
};
|
||||||
|
const auto state = raw->lifetime().make_state<State>();
|
||||||
|
state->filter = std::move(filter);
|
||||||
|
state->lists.backdrops = info.backdrops;
|
||||||
|
state->lists.models = info.models;
|
||||||
|
state->lists.patterns = info.patterns;
|
||||||
|
|
||||||
|
const auto scroll = [=] {
|
||||||
|
return QPoint(int(base::SafeRound(state->scroll)), 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr auto IndexToType = [](int index) {
|
||||||
|
Expects(index > 0 && index < 4);
|
||||||
|
|
||||||
|
return (index == 1)
|
||||||
|
? AttributeIdType::Model
|
||||||
|
: (index == 2)
|
||||||
|
? AttributeIdType::Backdrop
|
||||||
|
: AttributeIdType::Pattern;
|
||||||
|
};
|
||||||
|
static constexpr auto TypeToIndex = [](AttributeIdType type) {
|
||||||
|
return (type == AttributeIdType::Model)
|
||||||
|
? 1
|
||||||
|
: (type == AttributeIdType::Backdrop)
|
||||||
|
? 2
|
||||||
|
: 3;
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto setSelected = [=](int index) {
|
||||||
|
const auto was = (state->selected >= 0);
|
||||||
|
const auto now = (index >= 0);
|
||||||
|
state->selected = index;
|
||||||
|
if (was != now) {
|
||||||
|
raw->setCursor(now ? style::cur_pointer : style::cur_default);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const auto showMenu = [=](int index) {
|
||||||
|
if (state->menu) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state->menu = base::make_unique_q<Ui::PopupMenu>(
|
||||||
|
raw,
|
||||||
|
st::giftBoxResaleFilter);
|
||||||
|
const auto menu = state->menu.get();
|
||||||
|
const auto modify = [=](Fn<void(ResaleFilter&)> modifier) {
|
||||||
|
auto now = state->filter.current();
|
||||||
|
modifier(now);
|
||||||
|
state->filter = now;
|
||||||
|
};
|
||||||
|
const auto actionWithIcon = [=](
|
||||||
|
QString text,
|
||||||
|
Fn<void()> callback,
|
||||||
|
not_null<const style::icon*> icon,
|
||||||
|
bool checked = false) {
|
||||||
|
auto action = base::make_unique_q<Ui::GiftResaleFilterAction>(
|
||||||
|
menu,
|
||||||
|
menu->st().menu,
|
||||||
|
TextWithEntities{ text },
|
||||||
|
Ui::Text::MarkedContext(),
|
||||||
|
QString(),
|
||||||
|
icon);
|
||||||
|
action->setChecked(checked);
|
||||||
|
action->setClickedCallback(std::move(callback));
|
||||||
|
menu->addAction(std::move(action));
|
||||||
|
};
|
||||||
|
auto context = Core::TextContext({ .session = &window->session() });
|
||||||
|
context.customEmojiFactory = [original = context.customEmojiFactory](
|
||||||
|
QStringView data,
|
||||||
|
const Ui::Text::MarkedContext &context) {
|
||||||
|
return Ui::GiftResaleColorEmoji::Owns(data)
|
||||||
|
? std::make_unique<Ui::GiftResaleColorEmoji>(data)
|
||||||
|
: original(data, context);
|
||||||
|
};
|
||||||
|
const auto actionWithEmoji = [=](
|
||||||
|
QString text,
|
||||||
|
Fn<void()> callback,
|
||||||
|
QString data,
|
||||||
|
bool checked) {
|
||||||
|
auto action = base::make_unique_q<Ui::GiftResaleFilterAction>(
|
||||||
|
menu,
|
||||||
|
menu->st().menu,
|
||||||
|
TextWithEntities{ text },
|
||||||
|
context,
|
||||||
|
data,
|
||||||
|
nullptr);
|
||||||
|
action->setChecked(checked);
|
||||||
|
action->setClickedCallback(std::move(callback));
|
||||||
|
menu->addAction(std::move(action));
|
||||||
|
};
|
||||||
|
const auto actionWithDocument = [=](
|
||||||
|
QString text,
|
||||||
|
Fn<void()> callback,
|
||||||
|
DocumentId id,
|
||||||
|
bool checked) {
|
||||||
|
actionWithEmoji(
|
||||||
|
std::move(text),
|
||||||
|
std::move(callback),
|
||||||
|
Data::SerializeCustomEmojiId(id),
|
||||||
|
checked);
|
||||||
|
};
|
||||||
|
const auto actionWithColor = [=](
|
||||||
|
QString text,
|
||||||
|
Fn<void()> callback,
|
||||||
|
const QColor &color,
|
||||||
|
bool checked) {
|
||||||
|
actionWithEmoji(
|
||||||
|
std::move(text),
|
||||||
|
std::move(callback),
|
||||||
|
Ui::GiftResaleColorEmoji::DataFor(color),
|
||||||
|
checked);
|
||||||
|
};
|
||||||
|
if (!index) {
|
||||||
|
const auto sort = [=](ResaleSort value) {
|
||||||
|
modify([&](ResaleFilter &filter) {
|
||||||
|
filter.sort = value;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const auto is = [&](ResaleSort value) {
|
||||||
|
return state->filter.current().sort == value;
|
||||||
|
};
|
||||||
|
actionWithIcon(tr::lng_gift_resale_sort_price(tr::now), [=] {
|
||||||
|
sort(ResaleSort::Price);
|
||||||
|
}, &st::menuIconAbove, is(ResaleSort::Price)); AssertIsDebug(icons);
|
||||||
|
actionWithIcon(tr::lng_gift_resale_sort_date(tr::now), [=] {
|
||||||
|
sort(ResaleSort::Date);
|
||||||
|
}, &st::menuIconBelow, is(ResaleSort::Date));
|
||||||
|
actionWithIcon(tr::lng_gift_resale_sort_number(tr::now), [=] {
|
||||||
|
sort(ResaleSort::Number);
|
||||||
|
}, &st::menuIconFactcheck, is(ResaleSort::Number));
|
||||||
|
} else {
|
||||||
|
const auto now = state->filter.current().attributes;
|
||||||
|
const auto type = IndexToType(index);
|
||||||
|
const auto has = ranges::contains(now, type, &AttributeId::type);
|
||||||
|
if (has) {
|
||||||
|
actionWithIcon(tr::lng_gift_resale_filter_all(tr::now), [=] {
|
||||||
|
modify([&](ResaleFilter &filter) {
|
||||||
|
auto &list = filter.attributes;
|
||||||
|
for (auto i = begin(list); i != end(list);) {
|
||||||
|
if (i->type == type) {
|
||||||
|
i = list.erase(i);
|
||||||
|
} else {
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, &st::menuIconAbove);
|
||||||
|
}
|
||||||
|
const auto toggle = [=](AttributeId id) {
|
||||||
|
modify([&](ResaleFilter &filter) {
|
||||||
|
auto &list = filter.attributes;
|
||||||
|
if (ranges::contains(list, id)) {
|
||||||
|
list.remove(id);
|
||||||
|
} else {
|
||||||
|
list.emplace(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const auto checked = [=](AttributeId id) {
|
||||||
|
return !has || ranges::contains(now, id);
|
||||||
|
};
|
||||||
|
if (type == AttributeIdType::Model) {
|
||||||
|
for (auto &entry : state->lists.models) {
|
||||||
|
const auto id = IdFor(entry.model);
|
||||||
|
const auto text = entry.model.name
|
||||||
|
+ u" (%1)"_q.arg(entry.count);
|
||||||
|
actionWithDocument(text, [=] {
|
||||||
|
toggle(id);
|
||||||
|
}, id.value, checked(id));
|
||||||
|
}
|
||||||
|
} else if (type == AttributeIdType::Backdrop) {
|
||||||
|
for (auto &entry : state->lists.backdrops) {
|
||||||
|
const auto id = IdFor(entry.backdrop);
|
||||||
|
const auto text = entry.backdrop.name
|
||||||
|
+ u" (%1)"_q.arg(entry.count);
|
||||||
|
actionWithColor(text, [=] {
|
||||||
|
toggle(id);
|
||||||
|
}, entry.backdrop.centerColor, checked(id));
|
||||||
|
}
|
||||||
|
} else if (type == AttributeIdType::Pattern) {
|
||||||
|
for (auto &entry : state->lists.patterns) {
|
||||||
|
const auto id = IdFor(entry.pattern);
|
||||||
|
const auto text = entry.pattern.name
|
||||||
|
+ u" (%1)"_q.arg(entry.count);
|
||||||
|
actionWithDocument(text, [=] {
|
||||||
|
toggle(id);
|
||||||
|
}, id.value, checked(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
menu->popup(QCursor::pos());
|
||||||
|
};
|
||||||
|
|
||||||
|
state->filter.value(
|
||||||
|
) | rpl::start_with_next([=](const ResaleFilter &fields) {
|
||||||
|
auto x = st::giftBoxResaleTabsMargin.left();
|
||||||
|
auto y = st::giftBoxResaleTabsMargin.top();
|
||||||
|
|
||||||
|
setSelected(-1);
|
||||||
|
state->buttons.resize(kFiltersCount);
|
||||||
|
const auto &list = fields.attributes;
|
||||||
|
const auto setForIndex = [&](int i, auto many, auto one) {
|
||||||
|
const auto type = IndexToType(i);
|
||||||
|
const auto count = ranges::count(list, type, &AttributeId::type);
|
||||||
|
state->buttons[i].text = ResaleTabText((count > 0)
|
||||||
|
? many(tr::now, lt_count, count)
|
||||||
|
: one(tr::now));
|
||||||
|
};
|
||||||
|
state->buttons[0].text = SortModeText(fields.sort);
|
||||||
|
setForIndex(
|
||||||
|
1,
|
||||||
|
tr::lng_gift_resale_models,
|
||||||
|
tr::lng_gift_resale_model);
|
||||||
|
setForIndex(
|
||||||
|
2,
|
||||||
|
tr::lng_gift_resale_backdrops,
|
||||||
|
tr::lng_gift_resale_backdrop);
|
||||||
|
setForIndex(
|
||||||
|
3,
|
||||||
|
tr::lng_gift_resale_symbols,
|
||||||
|
tr::lng_gift_resale_symbol);
|
||||||
|
|
||||||
|
const auto padding = st::giftBoxTabPadding;
|
||||||
|
for (auto &button : state->buttons) {
|
||||||
|
const auto width = button.text.maxWidth();
|
||||||
|
const auto height = st::giftBoxTabStyle.font->height;
|
||||||
|
const auto r = QRect(0, 0, width, height).marginsAdded(padding);
|
||||||
|
button.geometry = QRect(QPoint(x, y), r.size());
|
||||||
|
x += r.width() + st::giftBoxResaleTabSkip;
|
||||||
|
}
|
||||||
|
state->fullWidth = x
|
||||||
|
- st::giftBoxTabSkip
|
||||||
|
+ st::giftBoxTabsMargin.right();
|
||||||
|
const auto height = state->buttons.empty()
|
||||||
|
? 0
|
||||||
|
: (y
|
||||||
|
+ state->buttons.back().geometry.height()
|
||||||
|
+ st::giftBoxTabsMargin.bottom());
|
||||||
|
raw->resize(raw->width(), height);
|
||||||
|
raw->update();
|
||||||
|
}, raw->lifetime());
|
||||||
|
|
||||||
|
rpl::combine(
|
||||||
|
raw->widthValue(),
|
||||||
|
state->fullWidth.value()
|
||||||
|
) | rpl::start_with_next([=](int outer, int inner) {
|
||||||
|
state->scrollMax = std::max(0, inner - outer);
|
||||||
|
}, raw->lifetime());
|
||||||
|
|
||||||
|
raw->setMouseTracking(true);
|
||||||
|
raw->events() | rpl::start_with_next([=](not_null<QEvent*> e) {
|
||||||
|
const auto type = e->type();
|
||||||
|
switch (type) {
|
||||||
|
case QEvent::Leave: setSelected(-1); break;
|
||||||
|
case QEvent::MouseMove: {
|
||||||
|
const auto me = static_cast<QMouseEvent*>(e.get());
|
||||||
|
const auto mousex = me->pos().x();
|
||||||
|
const auto drag = QApplication::startDragDistance();
|
||||||
|
if (state->dragx > 0) {
|
||||||
|
state->scroll = std::clamp(
|
||||||
|
state->dragscroll + state->dragx - mousex,
|
||||||
|
0.,
|
||||||
|
state->scrollMax * 1.);
|
||||||
|
raw->update();
|
||||||
|
break;
|
||||||
|
} else if (state->pressx > 0
|
||||||
|
&& std::abs(state->pressx - mousex) > drag) {
|
||||||
|
state->dragx = state->pressx;
|
||||||
|
state->dragscroll = state->scroll;
|
||||||
|
}
|
||||||
|
const auto position = me->pos() + scroll();
|
||||||
|
for (auto i = 0, c = int(state->buttons.size()); i != c; ++i) {
|
||||||
|
if (state->buttons[i].geometry.contains(position)) {
|
||||||
|
setSelected(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case QEvent::Wheel: {
|
||||||
|
const auto me = static_cast<QWheelEvent*>(e.get());
|
||||||
|
state->scroll = std::clamp(
|
||||||
|
state->scroll - ScrollDeltaF(me).x(),
|
||||||
|
0.,
|
||||||
|
state->scrollMax * 1.);
|
||||||
|
raw->update();
|
||||||
|
} break;
|
||||||
|
case QEvent::MouseButtonPress: {
|
||||||
|
const auto me = static_cast<QMouseEvent*>(e.get());
|
||||||
|
if (me->button() != Qt::LeftButton) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
state->pressed = state->selected;
|
||||||
|
state->pressx = me->pos().x();
|
||||||
|
} break;
|
||||||
|
case QEvent::MouseButtonRelease: {
|
||||||
|
const auto me = static_cast<QMouseEvent*>(e.get());
|
||||||
|
if (me->button() != Qt::LeftButton) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const auto dragx = std::exchange(state->dragx, 0);
|
||||||
|
const auto pressed = std::exchange(state->pressed, -1);
|
||||||
|
state->pressx = 0;
|
||||||
|
if (!dragx && pressed >= 0 && state->selected == pressed) {
|
||||||
|
showMenu(pressed);
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
}, raw->lifetime());
|
||||||
|
|
||||||
|
raw->paintRequest() | rpl::start_with_next([=] {
|
||||||
|
auto p = QPainter(raw);
|
||||||
|
auto hq = PainterHighQualityEnabler(p);
|
||||||
|
const auto padding = st::giftBoxTabPadding;
|
||||||
|
const auto shift = -scroll();
|
||||||
|
for (const auto &button : state->buttons) {
|
||||||
|
const auto geometry = button.geometry.translated(shift);
|
||||||
|
|
||||||
|
p.setBrush(st::giftBoxTabBgActive);
|
||||||
|
p.setPen(Qt::NoPen);
|
||||||
|
const auto radius = geometry.height() / 2.;
|
||||||
|
p.drawRoundedRect(geometry, radius, radius);
|
||||||
|
p.setPen(st::giftBoxTabFgActive);
|
||||||
|
|
||||||
|
button.text.draw(p, {
|
||||||
|
.position = geometry.marginsRemoved(padding).topLeft(),
|
||||||
|
.availableWidth = button.text.maxWidth(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const auto &icon = st::defaultEmojiSuggestions;
|
||||||
|
const auto w = icon.fadeRight.width();
|
||||||
|
const auto &c = st::boxDividerBg->c;
|
||||||
|
const auto r = QRect(0, 0, w, raw->height());
|
||||||
|
const auto s = std::abs(float64(shift.x()));
|
||||||
|
constexpr auto kF = 0.5;
|
||||||
|
const auto opacityRight = (state->scrollMax - s)
|
||||||
|
/ (icon.fadeRight.width() * kF);
|
||||||
|
p.setOpacity(std::clamp(std::abs(opacityRight), 0., 1.));
|
||||||
|
icon.fadeRight.fill(p, r.translated(raw->width() - w, 0), c);
|
||||||
|
|
||||||
|
const auto opacityLeft = s / (icon.fadeLeft.width() * kF);
|
||||||
|
p.setOpacity(std::clamp(std::abs(opacityLeft), 0., 1.));
|
||||||
|
icon.fadeLeft.fill(p, r, c);
|
||||||
|
}
|
||||||
|
}, raw->lifetime());
|
||||||
|
|
||||||
|
return {
|
||||||
|
.filter = state->filter.value(),
|
||||||
|
.widget = std::move(widget),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
struct GiftPriceTabs {
|
struct GiftPriceTabs {
|
||||||
rpl::producer<int> priceTab;
|
rpl::producer<int> priceTab;
|
||||||
object_ptr<RpWidget> widget;
|
object_ptr<RpWidget> widget;
|
||||||
|
@ -1911,6 +2421,100 @@ void SendGiftBox(
|
||||||
}, button->lifetime());
|
}, button->lifetime());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] rpl::producer<ResaleGiftsDescriptor> ResaleGiftsSlice(
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
uint64 giftId,
|
||||||
|
ResaleFilter filter = {},
|
||||||
|
QString offset = QString()) {
|
||||||
|
return [=](auto consumer) {
|
||||||
|
using Flag = MTPpayments_GetResaleStarGifts::Flag;
|
||||||
|
const auto requestId = session->api().request(
|
||||||
|
MTPpayments_GetResaleStarGifts(
|
||||||
|
MTP_flags(Flag::f_attributes_hash
|
||||||
|
| ((filter.sort == ResaleSort::Price)
|
||||||
|
? Flag::f_sort_by_price
|
||||||
|
: (filter.sort == ResaleSort::Number)
|
||||||
|
? Flag::f_sort_by_num
|
||||||
|
: Flag())
|
||||||
|
| (filter.attributes.empty()
|
||||||
|
? Flag()
|
||||||
|
: Flag::f_attributes)),
|
||||||
|
MTP_long(filter.attributesHash),
|
||||||
|
MTP_long(giftId),
|
||||||
|
MTP_vector_from_range(filter.attributes
|
||||||
|
| ranges::views::transform(AttributeToTL)),
|
||||||
|
MTP_string(offset),
|
||||||
|
MTP_int(kResaleGiftsPerPage)
|
||||||
|
)).done([=](const MTPpayments_ResaleStarGifts &result) {
|
||||||
|
const auto &data = result.data();
|
||||||
|
session->data().processUsers(data.vusers());
|
||||||
|
session->data().processChats(data.vchats());
|
||||||
|
|
||||||
|
auto info = ResaleGiftsDescriptor{
|
||||||
|
.giftId = giftId,
|
||||||
|
.offset = qs(data.vnext_offset().value_or_empty()),
|
||||||
|
.count = data.vcount().v,
|
||||||
|
};
|
||||||
|
const auto &list = data.vgifts().v;
|
||||||
|
info.list.reserve(list.size());
|
||||||
|
for (const auto &entry : list) {
|
||||||
|
if (auto gift = Api::FromTL(session, entry)) {
|
||||||
|
info.list.push_back(std::move(*gift));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info.attributesHash = data.vattributes_hash().value_or_empty();
|
||||||
|
const auto &attributes = data.vattributes()
|
||||||
|
? data.vattributes()->v
|
||||||
|
: QVector<MTPStarGiftAttribute>();
|
||||||
|
const auto &counters = data.vcounters()
|
||||||
|
? data.vcounters()->v
|
||||||
|
: QVector<MTPStarGiftAttributeCounter>();
|
||||||
|
auto counts = base::flat_map<AttributeId, int>();
|
||||||
|
counts.reserve(counters.size());
|
||||||
|
for (const auto &counter : counters) {
|
||||||
|
const auto &data = counter.data();
|
||||||
|
counts.emplace(FromTL(data.vattribute()), data.vcount().v);
|
||||||
|
}
|
||||||
|
const auto count = [&](AttributeId id) {
|
||||||
|
const auto i = counts.find(id);
|
||||||
|
return i != end(counts) ? i->second : 0;
|
||||||
|
};
|
||||||
|
info.models.reserve(attributes.size());
|
||||||
|
info.patterns.reserve(attributes.size());
|
||||||
|
info.backdrops.reserve(attributes.size());
|
||||||
|
for (const auto &attribute : attributes) {
|
||||||
|
attribute.match([&](const MTPDstarGiftAttributeModel &data) {
|
||||||
|
const auto parsed = Api::FromTL(session, data);
|
||||||
|
info.models.push_back({ parsed, count(IdFor(parsed)) });
|
||||||
|
}, [&](const MTPDstarGiftAttributePattern &data) {
|
||||||
|
const auto parsed = Api::FromTL(session, data);
|
||||||
|
info.patterns.push_back({ parsed, count(IdFor(parsed)) });
|
||||||
|
}, [&](const MTPDstarGiftAttributeBackdrop &data) {
|
||||||
|
const auto parsed = Api::FromTL(data);
|
||||||
|
info.backdrops.push_back({ parsed, count(IdFor(parsed)) });
|
||||||
|
}, [](const MTPDstarGiftAttributeOriginalDetails &data) {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
consumer.put_next(std::move(info));
|
||||||
|
consumer.put_done();
|
||||||
|
}).fail([=](const MTP::Error &error) {
|
||||||
|
consumer.put_next({});
|
||||||
|
consumer.put_done();
|
||||||
|
}).send();
|
||||||
|
|
||||||
|
auto lifetime = rpl::lifetime();
|
||||||
|
lifetime.add([=] { session->api().request(requestId).cancel(); });
|
||||||
|
return lifetime;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] rpl::lifetime ShowStarGiftResale(
|
||||||
|
not_null<Window::SessionController*> controller,
|
||||||
|
not_null<PeerData*> peer,
|
||||||
|
uint64 giftId,
|
||||||
|
QString title,
|
||||||
|
Fn<void()> finishRequesting);
|
||||||
|
|
||||||
[[nodiscard]] object_ptr<RpWidget> MakeGiftsList(
|
[[nodiscard]] object_ptr<RpWidget> MakeGiftsList(
|
||||||
not_null<Window::SessionController*> window,
|
not_null<Window::SessionController*> window,
|
||||||
not_null<PeerData*> peer,
|
not_null<PeerData*> peer,
|
||||||
|
@ -1927,6 +2531,8 @@ void SendGiftBox(
|
||||||
std::vector<std::unique_ptr<GiftButton>> buttons;
|
std::vector<std::unique_ptr<GiftButton>> buttons;
|
||||||
std::shared_ptr<Api::PremiumGiftCodeOptions> api;
|
std::shared_ptr<Api::PremiumGiftCodeOptions> api;
|
||||||
rpl::variable<VisibleRange> visibleRange;
|
rpl::variable<VisibleRange> visibleRange;
|
||||||
|
uint64 resaleRequestingId = 0;
|
||||||
|
rpl::lifetime resaleLifetime;
|
||||||
bool sending = false;
|
bool sending = false;
|
||||||
int perRow = 1;
|
int perRow = 1;
|
||||||
};
|
};
|
||||||
|
@ -2018,6 +2624,18 @@ void SendGiftBox(
|
||||||
star->info.unique,
|
star->info.unique,
|
||||||
star->transferId,
|
star->transferId,
|
||||||
done);
|
done);
|
||||||
|
} else if (star->resale) {
|
||||||
|
const auto id = star->info.id;
|
||||||
|
if (state->resaleRequestingId == id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state->resaleRequestingId = id;
|
||||||
|
state->resaleLifetime = ShowStarGiftResale(
|
||||||
|
window,
|
||||||
|
peer,
|
||||||
|
id,
|
||||||
|
star->info.resellTitle,
|
||||||
|
[=] { state->resaleRequestingId = 0; });
|
||||||
} else if (star && IsSoldOut(star->info)) {
|
} else if (star && IsSoldOut(star->info)) {
|
||||||
window->show(Box(SoldOutBox, window, *star));
|
window->show(Box(SoldOutBox, window, *star));
|
||||||
} else {
|
} else {
|
||||||
|
@ -2366,6 +2984,113 @@ void GiftBox(
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GiftResaleBox(
|
||||||
|
not_null<GenericBox*> box,
|
||||||
|
not_null<Window::SessionController*> window,
|
||||||
|
not_null<PeerData*> peer,
|
||||||
|
ResaleGiftsDescriptor descriptor) {
|
||||||
|
box->setWidth(st::boxWideWidth);
|
||||||
|
box->addButton(tr::lng_create_group_back(), [=] { box->closeBox(); });
|
||||||
|
|
||||||
|
box->setTitle(rpl::single(descriptor.title
|
||||||
|
+ u" (%1)"_q.arg(descriptor.count)));
|
||||||
|
|
||||||
|
const auto content = box->verticalLayout();
|
||||||
|
content->paintRequest() | rpl::start_with_next([=](QRect clip) {
|
||||||
|
QPainter(content).fillRect(clip, st::boxDividerBg);
|
||||||
|
}, content->lifetime());
|
||||||
|
|
||||||
|
struct State {
|
||||||
|
rpl::event_stream<> updated;
|
||||||
|
ResaleGiftsDescriptor data;
|
||||||
|
rpl::variable<ResaleFilter> filter;
|
||||||
|
rpl::lifetime loading;
|
||||||
|
};
|
||||||
|
const auto state = content->lifetime().make_state<State>();
|
||||||
|
state->data = std::move(descriptor);
|
||||||
|
|
||||||
|
auto tabs = MakeResaleTabs(
|
||||||
|
window,
|
||||||
|
peer,
|
||||||
|
state->data,
|
||||||
|
state->filter.value());
|
||||||
|
state->filter = std::move(tabs.filter);
|
||||||
|
content->add(std::move(tabs.widget));
|
||||||
|
|
||||||
|
state->filter.changes() | rpl::start_with_next([=](ResaleFilter value) {
|
||||||
|
state->data.offset = QString();
|
||||||
|
state->loading = ResaleGiftsSlice(
|
||||||
|
&peer->session(),
|
||||||
|
state->data.giftId,
|
||||||
|
value,
|
||||||
|
QString()
|
||||||
|
) | rpl::start_with_next([=](ResaleGiftsDescriptor &&slice) {
|
||||||
|
state->loading.destroy();
|
||||||
|
state->data.offset = slice.list.empty()
|
||||||
|
? QString()
|
||||||
|
: slice.offset;
|
||||||
|
state->data.list = std::move(slice.list);
|
||||||
|
state->updated.fire({});
|
||||||
|
});
|
||||||
|
}, content->lifetime());
|
||||||
|
|
||||||
|
content->add(MakeGiftsList(window, peer, rpl::single(
|
||||||
|
rpl::empty
|
||||||
|
) | rpl::then(
|
||||||
|
state->updated.events()
|
||||||
|
) | rpl::map([=] {
|
||||||
|
auto result = GiftsDescriptor();
|
||||||
|
for (const auto &gift : state->data.list) {
|
||||||
|
result.list.push_back(GiftTypeStars{ .info = gift });
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}), [=] {
|
||||||
|
if (!state->data.offset.isEmpty()
|
||||||
|
&& !state->loading) {
|
||||||
|
state->loading = ResaleGiftsSlice(
|
||||||
|
&peer->session(),
|
||||||
|
state->data.giftId,
|
||||||
|
state->filter.current(),
|
||||||
|
state->data.offset
|
||||||
|
) | rpl::start_with_next([=](ResaleGiftsDescriptor &&slice) {
|
||||||
|
state->loading.destroy();
|
||||||
|
state->data.offset = slice.list.empty()
|
||||||
|
? QString()
|
||||||
|
: slice.offset;
|
||||||
|
state->data.list.insert(
|
||||||
|
end(state->data.list),
|
||||||
|
std::make_move_iterator(begin(slice.list)),
|
||||||
|
std::make_move_iterator(end(slice.list)));
|
||||||
|
state->updated.fire({});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] rpl::lifetime ShowStarGiftResale(
|
||||||
|
not_null<Window::SessionController*> controller,
|
||||||
|
not_null<PeerData*> peer,
|
||||||
|
uint64 giftId,
|
||||||
|
QString title,
|
||||||
|
Fn<void()> finishRequesting) {
|
||||||
|
const auto weak = base::make_weak(controller);
|
||||||
|
const auto session = &controller->session();
|
||||||
|
return ResaleGiftsSlice(
|
||||||
|
session,
|
||||||
|
giftId
|
||||||
|
) | rpl::start_with_next([=](ResaleGiftsDescriptor &&info) {
|
||||||
|
if (const auto onstack = finishRequesting) {
|
||||||
|
onstack();
|
||||||
|
}
|
||||||
|
if (!info.giftId || !info.count) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
info.title = title;
|
||||||
|
controller->show(
|
||||||
|
Box(GiftResaleBox, controller, peer, std::move(info)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
struct CustomList {
|
struct CustomList {
|
||||||
object_ptr<Ui::RpWidget> content = { nullptr };
|
object_ptr<Ui::RpWidget> content = { nullptr };
|
||||||
Fn<bool(int, int, int)> overrideKey;
|
Fn<bool(int, int, int)> overrideKey;
|
||||||
|
|
|
@ -27,6 +27,7 @@ struct UniqueGiftBackdrop : UniqueGiftAttribute {
|
||||||
QColor edgeColor;
|
QColor edgeColor;
|
||||||
QColor patternColor;
|
QColor patternColor;
|
||||||
QColor textColor;
|
QColor textColor;
|
||||||
|
int id = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct UniqueGiftOriginalDetails {
|
struct UniqueGiftOriginalDetails {
|
||||||
|
@ -64,6 +65,7 @@ struct StarGift {
|
||||||
int64 starsToUpgrade = 0;
|
int64 starsToUpgrade = 0;
|
||||||
int64 starsResellMin = 0;
|
int64 starsResellMin = 0;
|
||||||
not_null<DocumentData*> document;
|
not_null<DocumentData*> document;
|
||||||
|
QString resellTitle;
|
||||||
int resellCount = 0;
|
int resellCount = 0;
|
||||||
int limitedLeft = 0;
|
int limitedLeft = 0;
|
||||||
int limitedCount = 0;
|
int limitedCount = 0;
|
||||||
|
|
147
Telegram/SourceFiles/menu/gift_resale_filter.cpp
Normal file
147
Telegram/SourceFiles/menu/gift_resale_filter.cpp
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
/*
|
||||||
|
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 "menu/gift_resale_filter.h"
|
||||||
|
|
||||||
|
#include "ui/effects/ripple_animation.h"
|
||||||
|
#include "ui/text/text_custom_emoji.h"
|
||||||
|
#include "ui/painter.h"
|
||||||
|
#include "styles/style_credits.h" // giftBoxResaleColorSize
|
||||||
|
#include "styles/style_media_player.h" // mediaPlayerMenuCheck
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
[[nodiscard]] Text::MarkedContext WithRepaint(
|
||||||
|
const Text::MarkedContext &context,
|
||||||
|
Fn<void()> repaint) {
|
||||||
|
auto result = context;
|
||||||
|
result.repaint = std::move(repaint);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] QString SerializeColorData(const QColor &color) {
|
||||||
|
return u"color:%1,%2,%3,%4"_q
|
||||||
|
.arg(color.red())
|
||||||
|
.arg(color.green())
|
||||||
|
.arg(color.blue())
|
||||||
|
.arg(color.alpha());
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool IsColorData(QStringView data) {
|
||||||
|
return data.startsWith(u"color:"_q);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] QColor ParseColorData(QStringView data) {
|
||||||
|
Expects(data.size() > 12);
|
||||||
|
|
||||||
|
const auto parts = data.mid(6).split(',');
|
||||||
|
Assert(parts.size() == 4);
|
||||||
|
return QColor(
|
||||||
|
parts[0].toInt(),
|
||||||
|
parts[1].toInt(),
|
||||||
|
parts[2].toInt(),
|
||||||
|
parts[3].toInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
GiftResaleFilterAction::GiftResaleFilterAction(
|
||||||
|
not_null<RpWidget*> parent,
|
||||||
|
const style::Menu &st,
|
||||||
|
const TextWithEntities &text,
|
||||||
|
const Text::MarkedContext &context,
|
||||||
|
QString iconEmojiData,
|
||||||
|
const style::icon *icon)
|
||||||
|
: Action(parent, st, new QAction(parent), icon, icon)
|
||||||
|
, _iconEmoji(iconEmojiData.isEmpty()
|
||||||
|
? nullptr
|
||||||
|
: context.customEmojiFactory(
|
||||||
|
iconEmojiData,
|
||||||
|
WithRepaint(context, [=] { update(); }))) {
|
||||||
|
setMarkedText(text, QString(), context);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GiftResaleFilterAction::paintEvent(QPaintEvent *e) {
|
||||||
|
Action::paintEvent(e);
|
||||||
|
|
||||||
|
Painter p(this);
|
||||||
|
|
||||||
|
const auto enabled = isEnabled();
|
||||||
|
const auto selected = isSelected();
|
||||||
|
const auto fg = selected
|
||||||
|
? st().itemFgOver
|
||||||
|
: enabled
|
||||||
|
? st().itemFg
|
||||||
|
: st().itemFgDisabled;
|
||||||
|
if (const auto emoji = _iconEmoji.get()) {
|
||||||
|
const auto x = st().itemIconPosition.x();
|
||||||
|
const auto y = (height() - st::emojiSize) / 2;
|
||||||
|
emoji->paint(p, {
|
||||||
|
.textColor = fg->c,
|
||||||
|
.position = { x, y },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (_checked) {
|
||||||
|
const auto &icon = st::mediaPlayerMenuCheck;
|
||||||
|
const auto skip = st().itemRightSkip;
|
||||||
|
const auto left = width() - skip - icon.width();
|
||||||
|
const auto top = (height() - icon.height()) / 2;
|
||||||
|
icon.paint(p, left, top, width());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GiftResaleFilterAction::setChecked(bool checked) {
|
||||||
|
if (_checked != checked) {
|
||||||
|
_checked = checked;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GiftResaleColorEmoji::GiftResaleColorEmoji(QStringView data)
|
||||||
|
: _color(ParseColorData(data)) {
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GiftResaleColorEmoji::Owns(QStringView data) {
|
||||||
|
return IsColorData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString GiftResaleColorEmoji::DataFor(QColor color) {
|
||||||
|
return SerializeColorData(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
int GiftResaleColorEmoji::width() {
|
||||||
|
return st::giftBoxResaleColorSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString GiftResaleColorEmoji::entityData() {
|
||||||
|
return DataFor(_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GiftResaleColorEmoji::paint(QPainter &p, const Context &context) {
|
||||||
|
auto hq = PainterHighQualityEnabler(p);
|
||||||
|
p.setBrush(_color);
|
||||||
|
p.setPen(Qt::NoPen);
|
||||||
|
p.drawEllipse(
|
||||||
|
context.position.x(),
|
||||||
|
context.position.y() + st::giftBoxResaleColorTop,
|
||||||
|
width(),
|
||||||
|
width());
|
||||||
|
}
|
||||||
|
|
||||||
|
void GiftResaleColorEmoji::unload() {
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GiftResaleColorEmoji::ready() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GiftResaleColorEmoji::readyInDefaultState() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Ui
|
55
Telegram/SourceFiles/menu/gift_resale_filter.h
Normal file
55
Telegram/SourceFiles/menu/gift_resale_filter.h
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
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ui/widgets/menu/menu_action.h"
|
||||||
|
#include "ui/text/text_custom_emoji.h"
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
|
||||||
|
class GiftResaleFilterAction final : public Menu::Action {
|
||||||
|
public:
|
||||||
|
GiftResaleFilterAction(
|
||||||
|
not_null<RpWidget*> parent,
|
||||||
|
const style::Menu &st,
|
||||||
|
const TextWithEntities &text,
|
||||||
|
const Text::MarkedContext &context,
|
||||||
|
QString iconEmojiData,
|
||||||
|
const style::icon *icon);
|
||||||
|
|
||||||
|
void setChecked(bool checked);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void paintEvent(QPaintEvent *e) override;
|
||||||
|
|
||||||
|
const std::unique_ptr<Text::CustomEmoji> _iconEmoji;
|
||||||
|
bool _checked = false;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
class GiftResaleColorEmoji final : public Text::CustomEmoji {
|
||||||
|
public:
|
||||||
|
GiftResaleColorEmoji(QStringView data);
|
||||||
|
|
||||||
|
[[nodiscard]] static bool Owns(QStringView data);
|
||||||
|
[[nodiscard]] static QString DataFor(QColor color);
|
||||||
|
|
||||||
|
int width() override;
|
||||||
|
QString entityData() override;
|
||||||
|
|
||||||
|
void paint(QPainter &p, const Context &context) override;
|
||||||
|
void unload() override;
|
||||||
|
bool ready() override;
|
||||||
|
bool readyInDefaultState() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QColor _color;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Ui
|
|
@ -122,6 +122,21 @@ giftBoxTabStyle: semiboldTextStyle;
|
||||||
giftBoxTabFg: windowSubTextFg;
|
giftBoxTabFg: windowSubTextFg;
|
||||||
giftBoxTabFgActive: windowBoldFg;
|
giftBoxTabFgActive: windowBoldFg;
|
||||||
giftBoxTabBgActive: windowBgRipple;
|
giftBoxTabBgActive: windowBgRipple;
|
||||||
|
giftBoxResaleTabsMargin: margins(11px, 10px, 11px, 8px);
|
||||||
|
giftBoxResaleTabSkip: 8px;
|
||||||
|
giftBoxResaleTabsDropdown: IconEmoji {
|
||||||
|
icon: icon{{ "intro_country_dropdown", lightButtonFg }};
|
||||||
|
padding: margins(4px, 6px, 0px, 0px);
|
||||||
|
}
|
||||||
|
giftBoxResaleFilter: PopupMenu(popupMenuWithIcons) {
|
||||||
|
menu: Menu(menuWithIcons) {
|
||||||
|
itemPadding: margins(54px, 8px, 48px, 8px);
|
||||||
|
itemRightSkip: 12px;
|
||||||
|
}
|
||||||
|
maxHeight: 320px;
|
||||||
|
}
|
||||||
|
giftBoxResaleColorSize: 18px;
|
||||||
|
giftBoxResaleColorTop: 1px;
|
||||||
giftBoxPadding: margins(11px, 4px, 11px, 24px);
|
giftBoxPadding: margins(11px, 4px, 11px, 24px);
|
||||||
giftBoxGiftSkip: point(10px, 8px);
|
giftBoxGiftSkip: point(10px, 8px);
|
||||||
giftBoxGiftHeight: 164px;
|
giftBoxGiftHeight: 164px;
|
||||||
|
|
|
@ -187,6 +187,8 @@ PRIVATE
|
||||||
|
|
||||||
media/media_common.h
|
media/media_common.h
|
||||||
|
|
||||||
|
menu/gift_resale_filter.cpp
|
||||||
|
menu/gift_resale_filter.h
|
||||||
menu/menu_check_item.cpp
|
menu/menu_check_item.cpp
|
||||||
menu/menu_check_item.h
|
menu/menu_check_item.h
|
||||||
menu/menu_ttl.cpp
|
menu/menu_ttl.cpp
|
||||||
|
|
Loading…
Add table
Reference in a new issue