diff --git a/Telegram/Resources/icons/menu/craft_chance.svg b/Telegram/Resources/icons/menu/craft_chance.svg new file mode 100644 index 0000000000..c3ea4c40db --- /dev/null +++ b/Telegram/Resources/icons/menu/craft_chance.svg @@ -0,0 +1,7 @@ + + + Icon / Menu / craft_chance + + + + \ No newline at end of file diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index eab0525678..dfdb5f8e48 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -4100,17 +4100,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_collection_remove_from" = "Remove from Collection"; "lng_gift_locked_title" = "Gift Locked"; "lng_gift_craft_info_title" = "Gift Crafting"; -"lng_gift_craft_info_about" = "Use your existing gifts to craft new ones."; -"lng_gift_craft_combine_title" = "New Models"; -"lng_gift_craft_combine_about" = "Add up to 4 NFT Gifts to attempt crafting a new upgraded model."; -"lng_gift_craft_input_title" = "Input Matters"; -"lng_gift_craft_input_about" = "Combine more gifts to improve your success chance."; -"lng_gift_craft_exclusive_title" = "Exclusive Look"; -"lng_gift_craft_exclusive_about" = "Reforge gifts into a rarer collectibles with a new look."; +"lng_gift_craft_info_about" = "Turn your gifts into rare, epic\nand legendary versions."; +"lng_gift_craft_rare_title" = "Get Rare Models"; +"lng_gift_craft_rare_about" = "Select up to 4 gifts to craft a new exclusive model."; +"lng_gift_craft_chance_title" = "Maximize Chances"; +"lng_gift_craft_chance_about" = "Combine more gifts to increase your odds of success."; +"lng_gift_craft_affect_title" = "Affect the Result"; +"lng_gift_craft_affect_about" = "Use gifts with the same attribute to boost its chance."; "lng_gift_craft_start_button" = "Start Crafting"; "lng_gift_craft_title" = "Craft Gift"; -"lng_gift_craft_about1" = "Add up to **4** gifts to craft new {gift}."; -"lng_gift_craft_about2" = "If crafting fails, all selected gifts will be **consumed**."; +"lng_gift_craft_about1" = "Add up to **4** gifts to craft new\n{gift}."; +"lng_gift_craft_about2" = "If crafting fails, all used gifts\nwill be lost."; "lng_gift_craft_view_all" = "{emoji} View all new variants {arrow}"; "lng_gift_craft_button" = "Craft {gift}"; "lng_gift_craft_button_chance" = "{percent} Success Chance"; @@ -4121,9 +4121,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_gift_craft_select_market#one" = "{count} suitable gift on sale"; "lng_gift_craft_select_market#other" = "{count} suitable gifts on sale"; "lng_gift_craft_progress" = "Crafting"; +"lng_gift_craft_progress_about" = "Crafting **{gift}**"; "lng_gift_craft_progress_chance" = "{percent} success chance"; -"lng_gift_craft_progress_fails" = "If crafting fails, all selected gifts will be consumed."; +"lng_gift_craft_progress_fails" = "If crafting fails, all used\ngifts will be lost."; "lng_gift_craft_failed" = "Crafting failed. Gifts consumed :("; +"lng_gift_craft_failed_title" = "Crafting Failed"; +"lng_gift_craft_failed_about#one" = "This crafting attempt was unsuccessful.\n**{count}** gift was lost."; +"lng_gift_craft_failed_about#other" = "This crafting attempt was unsuccessful.\n**{count}** gifts were lost."; +"lng_gift_craft_new_button" = "Craft New Gift"; "lng_auction_about_title" = "Auction"; "lng_auction_about_subtitle" = "Join the battle for exclusive gifts."; diff --git a/Telegram/SourceFiles/api/api_who_reacted.cpp b/Telegram/SourceFiles/api/api_who_reacted.cpp index a10fffcd15..f3af5312fb 100644 --- a/Telegram/SourceFiles/api/api_who_reacted.cpp +++ b/Telegram/SourceFiles/api/api_who_reacted.cpp @@ -666,12 +666,7 @@ QString FormatReadDate(TimeId date, const QDateTime &now) { return tr::lng_mediaview_date_time( tr::now, lt_date, - tr::lng_month_day( - tr::now, - lt_month, - Lang::MonthDay(readDate.month())(tr::now), - lt_day, - QString::number(readDate.day())), + langDayOfMonthShort(readDate), lt_time, QLocale().toString(parsed.time(), QLocale::ShortFormat)); } diff --git a/Telegram/SourceFiles/boxes/star_gift_craft_animation.cpp b/Telegram/SourceFiles/boxes/star_gift_craft_animation.cpp index 1811dbbe29..53318cad65 100644 --- a/Telegram/SourceFiles/boxes/star_gift_craft_animation.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_craft_animation.cpp @@ -18,12 +18,15 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_session.h" #include "history/view/media/history_view_sticker_player.h" #include "info/peer_gifts/info_peer_gifts_common.h" +#include "lang/lang_keys.h" #include "lottie/lottie_common.h" #include "main/main_session.h" #include "settings/settings_credits_graphics.h" #include "ui/boxes/boost_box.h" #include "ui/image/image_prepare.h" #include "ui/layers/generic_box.h" +#include "ui/widgets/gradient_round_button.h" +#include "ui/widgets/labels.h" #include "ui/wrap/vertical_layout.h" #include "ui/top_background_gradient.h" #include "ui/painter.h" @@ -46,6 +49,13 @@ constexpr auto kFailureFadeDuration = crl::time(400); constexpr auto kSuccessFadeInDuration = crl::time(300); constexpr auto kSuccessExpandDuration = crl::time(400); constexpr auto kSuccessExpandStart = crl::time(100); +constexpr auto kProgressFadeInDuration = crl::time(300); +constexpr auto kFailureFadeInDuration = crl::time(300); + +[[nodiscard]] QString FormatPercent(int permille) { + const auto rounded = (permille + 5) / 10; + return QString::number(rounded) + '%'; +} struct Rotation { float64 x = 0.; @@ -424,8 +434,7 @@ void PaintCubeFirstFlight( sideBg.setAlphaF(progress); auto hq = PainterHighQualityEnabler(p); - const auto offset = shared->craftingOffsetY; - const auto forge = shared->forgeRect.translated(0, offset); + const auto forge = shared->forgeRect; const auto radius = (1. - progress) * st::boxRadius; p.setPen(Qt::NoPen); p.setBrush(overlayBg); @@ -450,35 +459,37 @@ void FailureAnimationCheck( not_null canvas, crl::time now) { const auto animation = state->failureAnimation.get(); - if (!animation) { + if (!animation || animation->started) { return; } const auto elapsed = (now - state->animationStartTime); const auto left = (state->animationDuration * Slowing()) - elapsed; - if (!animation->started - && left > 0 - && left <= kFailureFadeDuration * Slowing()) { - animation->started = true; - auto &covers = state->shared->covers; - for (auto i = 0; i != 5; ++i) { - auto &cover = covers[i]; - if (!cover.shown) { - animation->finalCoverIndex = i; - if (i != 4) { - auto &last = covers.back(); - cover.backdrop = last.backdrop; - cover.pattern = std::move(last.pattern); - cover.button1 = last.button1; - cover.button2 = last.button2; - } - cover.shown = true; - cover.shownAnimation.start([=] { - canvas->update(); - }, 0., 1., left); - break; + if (left <= 0 || left > kFailureFadeDuration * Slowing()) { + return; + } + animation->started = true; + auto &covers = state->shared->covers; + for (auto i = 0; i != 5; ++i) { + auto &cover = covers[i]; + if (!cover.shown) { + animation->finalCoverIndex = i; + if (i != 4) { + auto &last = covers.back(); + cover.backdrop = last.backdrop; + cover.pattern = std::move(last.pattern); + cover.button1 = last.button1; + cover.button2 = last.button2; } + cover.shown = true; + cover.shownAnimation.start([=] { + canvas->update(); + }, 0., 1., left); + break; } } + + state->failureShown = true; + state->progressShown = false; } void FailureAnimationPrepareFrame( @@ -521,6 +532,11 @@ void FailureAnimationPrepareFrame( p.fillRect(rect, bg); } st::craftFail.paintInCenter(p, rect, st::white->c); + p.setOpacity(progress); + p.drawImage( + st::craftForgePadding, + st::craftForgePadding, + state->shared->lostRadial); } [[nodiscard]] QRect PrepareCraftFrame( @@ -559,11 +575,11 @@ void FailureAnimationPrepareFrame( } void PaintCube( - QPainter &p, - not_null state, - not_null canvas, - QPointF center, - float64 size) { + QPainter &p, + not_null state, + not_null canvas, + QPointF center, + float64 size) { const auto shared = state->shared.get(); const auto now = crl::now(); @@ -824,15 +840,13 @@ void PaintFlyingGift( void PaintSlideOutCorner( QPainter &p, const CraftState::CornerSnapshot &corner, - float64 offsetY, float64 progress) { if (corner.originalRect.isEmpty()) { return; } const auto ratio = style::DevicePixelRatio(); - const auto originalTopLeft = QPointF(corner.originalRect.topLeft()); - const auto currentTopLeft = originalTopLeft + QPointF(0, offsetY); + const auto currentTopLeft = QPointF(corner.originalRect.topLeft()); if (corner.giftButton) { const auto frame = corner.gift(0.); const auto giftSize = QSizeF(frame.size()) / ratio; @@ -887,19 +901,6 @@ void PaintSlideOutPhase( float64 progress) { const auto ratio = style::DevicePixelRatio(); - if (!shared->topPart.isNull()) { - const auto topSize = QSizeF(shared->topPart.size()) / ratio; - const auto slideDistance = topSize.height(); - const auto offsetY = slideDistance * progress; - const auto opacity = 1. - progress; - - p.setOpacity(opacity); - p.drawImage( - shared->topPartRect.topLeft() - QPointF(0, offsetY), - shared->topPart); - p.setOpacity(1.); - } - if (!shared->bottomPart.isNull()) { const auto slideDistance = QSizeF(shared->bottomPart.size()).height() / ratio; @@ -913,13 +914,12 @@ void PaintSlideOutPhase( p.setOpacity(1.); } - const auto offset = shared->craftingOffsetY; for (const auto &corner : shared->corners) { - PaintSlideOutCorner(p, corner, offset, progress); + PaintSlideOutCorner(p, corner, progress); } auto hq = PainterHighQualityEnabler(p); - const auto forge = shared->forgeRect.translated(0, offset); + const auto forge = shared->forgeRect; const auto radius = st::boxRadius; p.setPen(Qt::NoPen); p.setBrush(shared->forgeBgOverlay); @@ -932,6 +932,46 @@ void PaintSlideOutPhase( shared->forgePercent); } +void PaintFailureThumbnails( + QPainter &p, + not_null shared, + QSize canvasSize, + float64 fadeProgress) { + if (fadeProgress <= 0.) { + return; + } + + p.setOpacity(fadeProgress); + + const auto width = canvasSize.width(); + const auto giftsCount = int(shared->lostGifts.size()); + const auto thumbSize = shared->lostGifts.front().thumbnail.size() + / style::DevicePixelRatio(); + const auto thumbSpacing = st::boxRowPadding.left() / 2; + const auto totalThumbWidth = giftsCount * thumbSize.width() + + (giftsCount - 1) * thumbSpacing; + const auto available = width + - st::boxRowPadding.left() + - st::boxRowPadding.right(); + const auto skip = (totalThumbWidth > available) + ? (available - giftsCount * thumbSize.width()) / (giftsCount - 1) + : thumbSpacing; + const auto full = giftsCount * thumbSize.width() + + (giftsCount - 1) * skip; + auto x = (width - full) / 2; + const auto y = shared->forgeRect.bottom() + st::craftFailureThumbsTop; + + for (const auto &gift : shared->lostGifts) { + if (!gift.thumbnail.isNull()) { + const auto targetRect = QRect(QPoint(x, y), thumbSize); + p.drawImage(targetRect, gift.thumbnail); + } + x += thumbSize.width() + skip; + } + + p.setOpacity(1.); +} + [[nodiscard]] std::array RotateCornersForFace( std::array corners, int rotation) { @@ -1018,8 +1058,7 @@ void StartGiftFlightToFace( const auto cubeSize = float64(shared->forgeRect.width()); const auto cubeCenter = QPointF(shared->forgeRect.topLeft()) - + QPointF(cubeSize, cubeSize) / 2. - + QPointF(0, shared->craftingOffsetY); + + QPointF(cubeSize, cubeSize) / 2.; const auto targetCorners = ComputeCubeFaceCorners( cubeCenter, @@ -1090,8 +1129,7 @@ void PaintLoadingAnimation( } const auto shared = state->shared.get(); - const auto forge = shared->forgeRect.translated( - { 0, shared->craftingOffsetY }); + const auto forge = shared->forgeRect; const auto inner = forge.marginsRemoved({ st::craftForgePadding, st::craftForgePadding, @@ -1150,12 +1188,6 @@ void CraftState::paint( return gradient; }; auto patternOffsetY = 0.; - if (slideProgress > 0. - && containerHeight > 0 - && craftingAreaCenterY > 0) { - patternOffsetY = (containerHeight / 2. - craftingAreaCenterY) - * slideProgress; - } const auto paintPattern = [&]( QPainter &q, PatternView &pattern, @@ -1279,28 +1311,202 @@ CraftState::EmptySide CraftState::prepareEmptySide( return { .bg = bg, .frame = result }; } +void SetupProgressControls( + not_null state, + not_null canvas) { + const auto add = [&](not_null label) { + state->progressOpacity.value() | rpl::on_next([=](float64 opacity) { + label->setOpacity(opacity); + }, label->lifetime()); + }; + + const auto controls = CreateChild(canvas); + controls->resizeToWidth(canvas->width()); + controls->move(0, state->shared->craftingBottom); + + const auto title = controls->add( + object_ptr( + controls, + tr::lng_gift_craft_progress(), + st::uniqueGiftTitle), + st::boxRowPadding + st::craftProgressTitleMargin, + style::al_top); + add(title); + title->setTextColorOverride(st::white->c); + + const auto subColor = QColor(255, 255, 255, 178); + const auto about = controls->add( + object_ptr( + controls, + tr::lng_gift_craft_progress_about( + lt_gift, + rpl::single(tr::marked(state->shared->giftName)), + tr::rich), + st::uniqueGiftSubtitle), + st::boxRowPadding + st::craftProgressAboutMargin, + style::al_top); + add(about); + about->setTextColorOverride(subColor); + + const auto warning = controls->add( + object_ptr( + controls, + tr::lng_gift_craft_progress_fails(), + st::craftAbout), + st::boxRowPadding + st::craftProgressWarningMargin, + style::al_top); + add(warning); + warning->setTextColorOverride(subColor); + + const auto chance = controls->add( + object_ptr( + controls, + tr::lng_gift_craft_progress_chance( + tr::now, + lt_percent, + FormatPercent(state->shared->successPermille)), + st::craftChance), + st::boxRowPadding + st::craftProgressChanceMargin, + style::al_top); + add(chance); + chance->setTextColorOverride(st::white->c); + chance->paintOn([=](QPainter &p) { + if (const auto opacity = state->progressOpacity.current()) { + p.setPen(Qt::NoPen); + p.setOpacity(opacity); + p.setBrush(state->shared->forgeBgOverlay); + const auto radius = chance->height() / 2.; + p.drawRoundedRect(chance->rect(), radius, radius); + } + }); + + controls->showOn(state->progressOpacity.value( + ) | rpl::map( + rpl::mappers::_1 > 0. + ) | rpl::distinct_until_changed()); +} + +void SetupFailureControls( + not_null state, + not_null canvas) { + const auto add = [&](not_null label) { + state->failureOpacity.value() | rpl::on_next([=](float64 opacity) { + label->setOpacity(opacity); + }, label->lifetime()); + }; + + const auto controls = CreateChild(canvas); + controls->resizeToWidth(canvas->width()); + controls->move(0, state->shared->craftingBottom); + + const auto title = controls->add( + object_ptr( + controls, + tr::lng_gift_craft_failed_title(), + st::uniqueGiftTitle), + st::boxRowPadding + st::craftFailureTitleMargin, + style::al_top); + add(title); + title->setTextColorOverride(QColor(0xF8, 0x4A, 0x4A)); + + const auto about = controls->add( + object_ptr( + controls, + tr::lng_gift_craft_failed_about( + lt_count, + rpl::single(state->shared->lostGifts.size() * 1.), + tr::rich), + st::uniqueGiftSubtitle), + st::boxRowPadding + st::craftFailureAboutMargin, + style::al_top); + add(about); + about->setTextColorOverride(QColor(0xFF, 0xBC, 0x9B)); + + controls->showOn(state->failureOpacity.value( + ) | rpl::map( + rpl::mappers::_1 > 0. + ) | rpl::distinct_until_changed()); +} + +void SetupCraftNewButton( + not_null state, + not_null canvas) { + const auto button = CreateChild( + canvas, + QGradientStops{ + { 0., QColor(0xE2, 0x75, 0x19) }, + { 1., QColor(0xDD, 0x48, 0x19) }, + }); + button->setFullRadius(true); + button->setClickedCallback([=] { + state->retryWithNewGift(); + }); + + const auto buttonLabel = CreateChild( + button, + tr::lng_gift_craft_new_button(), + st::creditsBoxButtonLabel); + buttonLabel->setTextColorOverride(st::white->c); + buttonLabel->setAttribute( + Qt::WA_TransparentForMouseEvents); + button->sizeValue() | rpl::on_next([=](QSize size) { + buttonLabel->moveToLeft( + (size.width() - buttonLabel->width()) / 2, + st::giftBox.button.textTop); + }, buttonLabel->lifetime()); + + const auto buttonWidth = canvas->width() + - st::craftBoxButtonPadding.left() + - st::craftBoxButtonPadding.right(); + const auto buttonHeight = st::giftBox.button.height; + button->setNaturalWidth(buttonWidth); + button->setGeometry( + st::craftBoxButtonPadding.left(), + state->shared->originalButtonY, + buttonWidth, + buttonHeight); + button->show(); + + button->shownValue() | rpl::filter( + rpl::mappers::_1 + ) | rpl::take(1) | rpl::on_next([=] { + button->startGlareAnimation(); + }, button->lifetime()); +} + void StartCraftAnimation( not_null box, std::shared_ptr show, std::shared_ptr shared, Fn startRequest, - Fn closeParent) { + Fn closeParent, + Fn closeCurrent)> retryWithNewGift) { const auto container = box->verticalLayout(); - const auto closeOnFail = crl::guard(box, [=] { - if (const auto onstack = closeParent) { - onstack(); - } - box->closeBox(); - show->showToast("failed");// tr::lng_gift_craft_failed(tr::now)); - }); - while (container->count() > 0) { delete container->widgetAt(0); } + auto canvas = object_ptr(container); const auto raw = canvas.data(); const auto state = raw->lifetime().make_state(); + state->retryWithNewGift = [=] { + retryWithNewGift(crl::guard(box, [=] { box->closeBox(); })); + }; + + const auto title = CreateChild( + raw, + tr::lng_gift_craft_title(), + st::uniqueGiftTitle); + title->naturalWidthValue() | rpl::on_next([=](int titleWidth) { + const auto width = st::boxWideWidth; + title->moveToLeft( + (width - titleWidth) / 2, + st::craftTitleMargin.top(), + width); + }, title->lifetime()); + title->setTextColorOverride(st::white->c); + const auto height = shared->containerHeight; const auto craftingHeight = shared->craftingBottom - shared->craftingTop; state->shared = std::move(shared); @@ -1319,6 +1525,38 @@ void StartCraftAnimation( [=] { raw->update(); }, st::craftForgeLoading); + SetupProgressControls(state, raw); + SetupFailureControls(state, raw); + + state->progressShown.value( + ) | rpl::combine_previous( + ) | rpl::on_next([=](bool wasShown, bool nowShown) { + if (wasShown == nowShown) { + return; + } + const auto from = wasShown ? 1. : 0.; + const auto to = nowShown ? 1. : 0.; + state->progressFadeIn.start([=] { + raw->update(); + }, from, to, kProgressFadeInDuration, anim::easeOutCubic); + }, raw->lifetime()); + + state->failureShown.value( + ) | rpl::combine_previous( + ) | rpl::on_next([=](bool wasShown, bool nowShown) { + if (wasShown == nowShown) { + return; + } + const auto from = wasShown ? 1. : 0.; + const auto to = nowShown ? 1. : 0.; + state->failureFadeIn.start([=] { + raw->update(); + if (!state->failureFadeIn.animating()) { + SetupCraftNewButton(state, raw); + } + }, from, to, kFailureFadeDuration, anim::easeOutCubic); + }, raw->lifetime()); + raw->paintOn([=](QPainter &p) { const auto shared = state->shared.get(); const auto success = state->successAnimation.get(); @@ -1347,27 +1585,31 @@ void StartCraftAnimation( } } - shared->craftingOffsetY = (shared->containerHeight / 2.) - - shared->craftingAreaCenterY; if (slideProgress < 1.) { - shared->craftingOffsetY *= slideProgress; PaintSlideOutPhase(p, shared, craftingSize, slideProgress); } else { - if (success) { - const auto target = success->coverShift; - shared->craftingOffsetY -= target * success->expanded; + const auto craftingOffsetY = success + ? (-success->coverShift * success->expanded) + : 0.; + if (success && success->expanded > 0.) { + const auto width = st::boxWideWidth; + const auto top = st::craftTitleMargin.top(); + title->moveToLeft( + (width - title->naturalWidth()) / 2, + top - (success->expanded * (top + title->height())), + width); } const auto cubeSize = float64(shared->forgeRect.width()); const auto cubeCenter = QPointF(shared->forgeRect.topLeft()) + QPointF(cubeSize, cubeSize) / 2. - + QPointF(0, shared->craftingOffsetY); + + QPointF(0, craftingOffsetY); for (auto i = 0; i < 4; ++i) { const auto &corner = shared->corners[i]; if (corner.giftButton && state->giftToSide[i].face < 0) { const auto giftTopLeft = QPointF() + QPointF(corner.originalRect.topLeft()) - + QPointF(0, shared->craftingOffsetY); + + QPointF(0, craftingOffsetY); p.drawImage(giftTopLeft, corner.gift(0)); } } @@ -1399,7 +1641,7 @@ void StartCraftAnimation( if (state->flightTargetCorners) { const auto sourceRect = QRectF(corner.originalRect) - .translated(0, shared->craftingOffsetY); + .translated(0, craftingOffsetY); const auto sourceCorners = RectToCorners(sourceRect); const auto interpolatedCorners = InterpolateQuadCorners( sourceCorners, @@ -1416,13 +1658,26 @@ void StartCraftAnimation( cubeCenter, cubeSize, progress, - shared->craftingOffsetY); + craftingOffsetY); PaintFlyingGift(p, corner, position, progress); } } PaintLoadingAnimation(p, state); + + if (const auto opacity = state->failureOpacity.current()) { + PaintFailureThumbnails( + p, + shared, + craftingSize, + opacity); + } } + + state->progressOpacity = state->progressFadeIn.value( + state->progressShown.current() ? 1. : 0.); + state->failureOpacity = state->failureFadeIn.value( + state->failureShown.current() ? 1. : 0.); }); const auto startFlying = [=] { @@ -1472,6 +1727,8 @@ void StartCraftAnimation( } }; + state->progressShown = true; + state->slideOutAnimation.start([=] { raw->update(); @@ -1597,9 +1854,10 @@ void StartCraftAnimation( raw->resizeToWidth(st::boxWideWidth); SendPendingMoveResizeEvents(raw); - const auto fullHeight = state->shared->containerHeight; success->coverHeight = raw->height(); - success->coverShift = (fullHeight - raw->height()) / 2; + success->coverShift = state->shared->forgeRect.y() + + (state->shared->forgeRect.height() / 2) + - (raw->height() / 2); } tryStartFlying(); }); diff --git a/Telegram/SourceFiles/boxes/star_gift_craft_animation.h b/Telegram/SourceFiles/boxes/star_gift_craft_animation.h index 82a675e0cc..dc68b56e11 100644 --- a/Telegram/SourceFiles/boxes/star_gift_craft_animation.h +++ b/Telegram/SourceFiles/boxes/star_gift_craft_animation.h @@ -32,7 +32,9 @@ class GiftButton; namespace Ui { +class FlatLabel; class GenericBox; +class RpWidget; class VerticalLayout; class UniqueGiftCoverWidget; @@ -104,7 +106,16 @@ struct CraftState { int craftingTop = 0; int craftingBottom = 0; int craftingAreaCenterY = 0; - int craftingOffsetY = 0; + int originalButtonY = 0; + + QString giftName; + int successPermille = 0; + struct LostGift { + QImage thumbnail; + QString number; + }; + std::vector lostGifts; + QImage lostRadial; void paint( QPainter &p, @@ -178,6 +189,16 @@ struct CraftAnimationState { std::unique_ptr successAnimation; std::unique_ptr failureAnimation; + + rpl::variable progressOpacity; + Animations::Simple progressFadeIn; + rpl::variable progressShown; + + rpl::variable failureOpacity; + Animations::Simple failureFadeIn; + rpl::variable failureShown; + + Fn retryWithNewGift; }; void StartCraftAnimation( @@ -185,6 +206,7 @@ void StartCraftAnimation( std::shared_ptr show, std::shared_ptr state, Fn startRequest, - Fn closeParent); + Fn closeParent, + Fn closeCurrent)> retryWithNewGift); } // namespace Ui diff --git a/Telegram/SourceFiles/boxes/star_gift_craft_box.cpp b/Telegram/SourceFiles/boxes/star_gift_craft_box.cpp index dba07751a8..16ee851c2d 100644 --- a/Telegram/SourceFiles/boxes/star_gift_craft_box.cpp +++ b/Telegram/SourceFiles/boxes/star_gift_craft_box.cpp @@ -9,11 +9,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/call_delayed.h" #include "base/random.h" +#include "boxes/star_gift_auction_box.h" #include "boxes/star_gift_craft_animation.h" +#include "boxes/star_gift_preview_box.h" #include "apiwrap.h" #include "api/api_credits.h" #include "api/api_premium.h" #include "boxes/star_gift_box.h" +#include "data/components/gift_auctions.h" #include "core/ui_integration.h" #include "data/stickers/data_custom_emoji.h" #include "data/data_document.h" @@ -36,6 +39,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/ui_utility.h" #include "ui/vertical_list.h" #include "window/window_session_controller.h" +#include "styles/style_chat.h" #include "styles/style_chat_helpers.h" // stickerPanDeleteIconFg #include "styles/style_credits.h" #include "styles/style_layers.h" @@ -104,6 +108,12 @@ struct GiftForCraft { const GiftForCraft &) = default; }; +void ShowGiftCraftBoxInternal( + not_null controller, + std::vector gifts, + Fn closeParent, + bool autoStartCraft); + struct CraftingView { object_ptr widget; rpl::producer editRequests; @@ -119,7 +129,8 @@ struct CraftingView { [[nodiscard]] not_null MakeRadialPercent( not_null parent, const style::CraftRadialPercent &st, - rpl::producer permille) { + rpl::producer permille, + Fn format = FormatPercent) { auto raw = CreateChild(parent); struct State { @@ -130,7 +141,7 @@ struct CraftingView { Animations::Simple animation; NumbersAnimation numbers; - int permille = 0; + int permille = -1; }; const auto state = raw->lifetime().make_state( st, @@ -144,7 +155,7 @@ struct CraftingView { raw->update(); }, state->permille, value, st::slideWrapDuration); state->permille = value; - state->numbers.setText(FormatPercent(value), value); + state->numbers.setText(format(value), value); }, raw->lifetime()); state->animation.stop(); state->numbers.finishAnimating(); @@ -476,6 +487,7 @@ AbstractButton *MakeRemoveButton( craftState->forgePercent = GrabWidgetToImage(state->forgeRadial); auto giftsTotal = 0; + auto lostIndex = 0; for (auto i = 0; i != 4; ++i) { auto &entry = state->entries[i]; auto &corner = craftState->corners[i]; @@ -488,6 +500,11 @@ AbstractButton *MakeRemoveButton( if (entry.remove) { corner.removeButton = GrabWidgetToImage(entry.remove); } + if (lostIndex < craftState->lostGifts.size()) { + craftState->lostGifts[lostIndex].thumbnail + = GrabWidgetToImage(entry.button); + ++lostIndex; + } corner.giftButton.reset(entry.button); entry.button->setParent(parent); base::take(entry.button)->hide(); @@ -499,6 +516,15 @@ AbstractButton *MakeRemoveButton( } } + const auto failedCount = MakeRadialPercent( + raw, + st::craftForgePercent, + rpl::single(0), + [=](int) { return QString::number(giftsTotal); }); + failedCount->setGeometry(state->forgeRadial->geometry()); + craftState->lostRadial = GrabWidgetToImage(failedCount); + delete failedCount; + const auto overlayBg = craftState->forgeBgOverlay = ForgeBgOverlay(); const auto backdrop = CraftBackdrops()[giftsTotal - 1].backdrop; craftState->forgeBg1 = anim::color( @@ -815,6 +841,7 @@ void ShowSelectGiftBox( void Craft( not_null box, + not_null controller, std::shared_ptr show, std::shared_ptr state, const std::vector &gifts, @@ -827,7 +854,8 @@ void Craft( const auto giftsCopy = gifts; base::call_delayed(delay, box, [=] { - const auto shouldSucceed = false;// (base::RandomIndex(5) != 0); + //static auto testing = 0; + const auto shouldSucceed = false;// (((++testing) / 4) % 2 == 0); const auto count = int(giftsCopy.size()); if (shouldSucceed && count > 0) { const auto &chosen = giftsCopy[base::RandomIndex(count)]; @@ -847,18 +875,135 @@ void Craft( } }); }; + const auto session = &controller->session(); + const auto initialGiftId = gifts.front().unique->initialGiftId; + auto retryWithNewGift = [=](Fn closeCurrent) { + session->api().request(MTPpayments_GetCraftStarGifts( + MTP_long(initialGiftId), + MTP_string(), + MTP_int(30) + )).done(crl::guard(box, [=](const MTPpayments_SavedStarGifts &result) { + const auto user = session->user(); + const auto owner = &session->data(); + const auto &data = result.data(); + owner->processUsers(data.vusers()); + owner->processChats(data.vchats()); + + auto list = std::vector(); + list.reserve(data.vgifts().v.size()); + for (const auto &gift : data.vgifts().v) { + if (auto parsed = Api::FromTL(user, gift)) { + list.push_back(std::move(*parsed)); + } + } + + ShowSelectGiftBox(controller, list, [=](GiftForCraft chosen) { + ShowGiftCraftBoxInternal( + controller, + { chosen }, + closeParent, + false); + closeCurrent(); + }, {}); + })).send(); + }; StartCraftAnimation( box, std::move(show), std::move(state), std::move(startRequest), - closeParent); + closeParent, + std::move(retryWithNewGift)); +} + +void AddPreviewNewModels( + not_null container, + std::shared_ptr show, + const QString &giftName, + Data::UniqueGiftAttributes attributes) { + auto exceptional = std::vector(); + for (const auto &model : attributes.models) { + if (Data::UniqueGiftAttributeHasSpecialRarity(model)) { + exceptional.push_back(model); + } + } + attributes.models = std::move(exceptional); + + auto emoji = TextWithEntities(); + const auto indices = RandomIndicesSubset( + int(attributes.models.size()), + std::min(3, int(attributes.models.size()))); + for (const auto index : indices) { + emoji.append(Data::SingleCustomEmoji( + attributes.models[index].document)); + } + + auto badge = object_ptr(container); + const auto badgeRaw = badge.data(); + + const auto label = CreateChild( + badgeRaw, + tr::lng_gift_craft_view_all( + lt_emoji, + rpl::single(emoji), + lt_arrow, + rpl::single(Ui::Text::IconEmoji(&st::textMoreIconEmoji)), + Text::WithEntities), + st::uniqueGiftResalePrice, + st::defaultPopupMenu, + Core::TextContext({ .session = &show->session() })); + label->setTextColorOverride(st::white->c); + + label->sizeValue() | rpl::on_next([=](QSize size) { + const auto padding = st::craftPreviewAllPadding; + badgeRaw->setNaturalWidth( + padding.left() + size.width() + padding.right()); + badgeRaw->resize( + badgeRaw->naturalWidth(), + padding.top() + size.height() + padding.bottom()); + }, label->lifetime()); + + badgeRaw->widthValue() | rpl::on_next([=](int width) { + const auto padding = st::craftPreviewAllPadding; + label->move(padding.left(), padding.top()); + }, badgeRaw->lifetime()); + + badgeRaw->paintOn([=](QPainter &p) { + auto hq = PainterHighQualityEnabler(p); + const auto rect = badgeRaw->rect(); + const auto radius = rect.height() / 2.; + p.setPen(Qt::NoPen); + p.setBrush(ForgeBgOverlay()); + p.drawRoundedRect(rect, radius, radius); + }); + + label->setAttribute(Qt::WA_TransparentForMouseEvents); + badgeRaw->setClickedCallback([=] { + auto previewAttrs = attributes; + previewAttrs.models.erase( + ranges::remove_if(previewAttrs.models, [](const auto &m) { + return !Data::UniqueGiftAttributeHasSpecialRarity(m); + }), + end(previewAttrs.models)); + show->show(Box( + StarGiftPreviewBox, + giftName, + previewAttrs, + Data::GiftAttributeIdType::Model, + nullptr)); + }); + + container->add( + std::move(badge), + st::craftPreviewAllMargin, + style::al_top); } void MakeCraftContent( not_null box, not_null controller, std::vector gifts, + Data::UniqueGiftAttributes attributes, Fn closeParent, bool autoStartCraft) { Expects(!gifts.empty()); @@ -902,6 +1047,7 @@ void MakeCraftContent( } } + const auto giftName = gifts.front().unique->title; state->chosen = std::move(gifts); for (auto i = 0; i != int(state->chosen.current().size()); ++i) { state->craftState->covers[i].shown = true; @@ -1030,8 +1176,15 @@ void MakeCraftContent( state->attributes = raw->add( MakeRarityExpectancyPreview(raw, session, state->chosen.value()), + st::craftAttributesMargin, style::al_top); + AddPreviewNewModels( + raw, + controller->uiShow(), + giftName, + std::move(attributes)); + raw->paintOn([=](QPainter &p) { const auto &cs = state->craftState; if (cs->craftingStarted) { @@ -1050,7 +1203,7 @@ void MakeCraftContent( const auto button = state->button = raw->add( object_ptr(raw, QGradientStops()), - st::giftBox.buttonPadding); + st::craftBoxButtonPadding); button->setFullRadius(true); button->startGlareAnimation(); @@ -1061,9 +1214,17 @@ void MakeCraftContent( state->crafting = true; const auto &cs = state->craftState; + const auto &gifts = state->chosen.current(); + cs->giftName = state->name.current(); + cs->successPermille = state->successPercentPermille.current(); - cs->topPart = GrabWidgetToImage(state->title); - cs->topPartRect = state->title->geometry(); + for (const auto &gift : gifts) { + if (gift.unique) { + cs->lostGifts.push_back({ + .number = u"#%1"_q.arg(gift.unique->number), + }); + } + } if (state->grabCraftingView) { state->grabCraftingView(cs); @@ -1080,8 +1241,9 @@ void MakeCraftContent( cs->craftingAreaCenterY = cs->forgeRect.center().y(); const auto aboutPos = state->about->pos(); - const auto buttonBottom = state->button->pos().y() - + state->button->height(); + const auto buttonY = state->button->pos().y(); + const auto buttonBottom = buttonY + state->button->height(); + cs->originalButtonY = buttonY; const auto bottomRect = QRect( 0, aboutPos.y(), @@ -1109,6 +1271,7 @@ void MakeCraftContent( Craft( box, + controller, controller->uiShow(), state->craftState, state->chosen.current(), @@ -1131,19 +1294,20 @@ void MakeCraftContent( st::resaleButtonSubtitle); raw->widthValue() | rpl::on_next([=](int width) { - const auto padding = st::giftBox.buttonPadding; + const auto padding = st::craftBoxButtonPadding; button->setNaturalWidth(width - padding.left() - padding.right()); button->resize(button->naturalWidth(), st::giftBox.button.height); }, raw->lifetime()); if (autoStartCraft) { - base::call_delayed(crl::time(1000), raw, startCrafting); +// base::call_delayed(crl::time(1000), raw, startCrafting); } } -void ShowGiftCraftBoxInternal( +void ShowGiftCraftBoxWithAttributes( not_null controller, std::vector gifts, + Data::UniqueGiftAttributes attributes, Fn closeParent, bool autoStartCraft) { controller->show(Box([=](not_null box) { @@ -1154,6 +1318,7 @@ void ShowGiftCraftBoxInternal( box, controller, gifts, + attributes, closeParent, autoStartCraft); AddUniqueCloseButton(box); @@ -1177,6 +1342,33 @@ void ShowGiftCraftBoxInternal( })); } +void ShowGiftCraftBoxInternal( + not_null controller, + std::vector gifts, + Fn closeParent, + bool autoStartCraft) { + const auto giftId = gifts.front().unique->initialGiftId; + const auto auctions = &controller->session().giftAuctions(); + if (auto attrs = auctions->attributes(giftId)) { + ShowGiftCraftBoxWithAttributes( + controller, + std::move(gifts), + std::move(*attrs), + std::move(closeParent), + autoStartCraft); + } else { + auctions->requestAttributes(giftId, crl::guard(controller, [=] { + auto attrs = auctions->attributes(giftId); + ShowGiftCraftBoxWithAttributes( + controller, + gifts, + attrs ? std::move(*attrs) : Data::UniqueGiftAttributes{}, + closeParent, + autoStartCraft); + })); + } +} + } // namespace void ShowGiftCraftInfoBox( @@ -1201,19 +1393,19 @@ void ShowGiftCraftInfoBox( const auto features = std::vector{ { - st::menuIconTools, - tr::lng_gift_craft_combine_title(tr::now), - tr::lng_gift_craft_combine_about(tr::now, tr::rich), + st::menuIconUnique, + tr::lng_gift_craft_rare_title(tr::now), + tr::lng_gift_craft_rare_about(tr::now, tr::rich), }, { st::menuIconCraftTraits, - tr::lng_gift_craft_input_title(tr::now), - tr::lng_gift_craft_input_about(tr::now, tr::marked), + tr::lng_gift_craft_chance_title(tr::now), + tr::lng_gift_craft_chance_about(tr::now, tr::marked), }, { - st::menuIconUnique, - tr::lng_gift_craft_exclusive_title(tr::now), - tr::lng_gift_craft_exclusive_about(tr::now, tr::marked), + st::menuIconCraftChance, + tr::lng_gift_craft_affect_title(tr::now), + tr::lng_gift_craft_affect_about(tr::now, tr::marked), }, }; for (const auto &feature : features) { diff --git a/Telegram/SourceFiles/lang/lang_keys.cpp b/Telegram/SourceFiles/lang/lang_keys.cpp index aa65ed81d7..8df01ea7e9 100644 --- a/Telegram/SourceFiles/lang/lang_keys.cpp +++ b/Telegram/SourceFiles/lang/lang_keys.cpp @@ -110,6 +110,20 @@ QString langDayOfMonthFull(const QDate &date) { }); } +QString langDayOfMonthShort(const QDate &date) { + auto day = date.day(); + return langDateMaybeWithYear(date, [&](int month, int year) { + return QLocale().toString(date, QLocale::ShortFormat); + }, [day](int month, int year) { + return tr::lng_month_day( + tr::now, + lt_month, + MonthSmall(month)(tr::now), + lt_day, + QString::number(day)); + }); +} + QString langMonthOfYear(int month, int year) { return (month > 0 && month <= 12) ? tr::lng_month_year( diff --git a/Telegram/SourceFiles/lang/lang_keys.h b/Telegram/SourceFiles/lang/lang_keys.h index f49f4fd32c..0a2a9b9f1a 100644 --- a/Telegram/SourceFiles/lang/lang_keys.h +++ b/Telegram/SourceFiles/lang/lang_keys.h @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL [[nodiscard]] QString langDayOfMonth(const QDate &date); [[nodiscard]] QString langDayOfMonthFull(const QDate &date); +[[nodiscard]] QString langDayOfMonthShort(const QDate &date); [[nodiscard]] QString langMonthOfYear(int month, int year); [[nodiscard]] QString langMonth(const QDate &date); [[nodiscard]] QString langMonthOfYearFull(int month, int year); diff --git a/Telegram/SourceFiles/ui/effects/credits.style b/Telegram/SourceFiles/ui/effects/credits.style index 4706535385..b74c4f8931 100644 --- a/Telegram/SourceFiles/ui/effects/credits.style +++ b/Telegram/SourceFiles/ui/effects/credits.style @@ -598,15 +598,32 @@ craftAbout: FlatLabel(defaultFlatLabel) { align: align(top); minWidth: 128px; } -craftAboutMargin: margins(24px, 16px, 24px, 4px); +craftAboutMargin: margins(24px, 12px, 24px, 4px); +craftChance: FlatLabel(defaultFlatLabel) { + style: semiboldTextStyle; + textFg: attentionButtonFg; + margin: margins(8px, 4px, 8px, 4px); +} craftPattern: IconEmoji { icon: icon{{ "chat/filled_forge-36x36", windowFg }}; padding: margins(0px, 0px, 0px, 0px); } craftAddIcon: icon {{ "settings/plus", windowFgActive }}; -craftPreviewPadding: margins(24px, 12px, 40px, 8px); +craftPreviewPadding: margins(24px, 8px, 40px, 8px); +craftBoxButtonPadding: margins(22px, 12px, 22px, 22px); +craftAttributesMargin: margins(22px, 12px, 22px, 16px); +craftPreviewAllPadding: margins(6px, 4px, 6px, 4px); +craftPreviewAllMargin: margins(22px, 0px, 22px, 8px); +craftProgressTitleMargin: margins(0px, 12px, 0px, 16px); +craftProgressAboutMargin: margins(0px, 4px, 0px, 16px); +craftProgressWarningMargin: margins(0px, 4px, 0px, 16px); +craftProgressChanceMargin: margins(0px, 40px, 0px, 16px); +craftFailureThumbsTop: 36px; +craftFailureTitleMargin: margins(0px, 88px, 0px, 12px); +craftFailureAboutMargin: margins(0px, 0px, 0px, 16px); + craftForge: icon{{ "chat/filled_forge-48x48", windowFg }}; -craftFail: icon{{ "chat/filled_forge_fail-48x48", windowFg }}; +craftFail: icon{{ "chat/filled_forge_fail-36x36", windowFg }}; craftForgePadding: 12px; craftForgePercent: CraftRadialPercent { stroke: 4px; diff --git a/Telegram/SourceFiles/ui/menu_icons.style b/Telegram/SourceFiles/ui/menu_icons.style index 6c365d3343..3c48f7d4fd 100644 --- a/Telegram/SourceFiles/ui/menu_icons.style +++ b/Telegram/SourceFiles/ui/menu_icons.style @@ -200,6 +200,7 @@ menuIconTon: icon{{ "payments/ton_emoji-18x18", menuIconColor, point(3px, 2px) } menuIconReorder: icon{{ "menu/reorder-24x24", menuIconColor }}; menuIconTools: icon{{ "menu/craft_tools-24x24", menuIconColor }}; menuIconCraftTraits: icon{{ "menu/craft_random-24x24", menuIconColor }}; +menuIconCraftChance: icon{{ "menu/craft_chance-24x24", menuIconColor }}; menuIconTTLAny: icon {{ "menu/auto_delete_plain", menuIconColor }}; menuIconTTLAnyTextPosition: point(11px, 22px); diff --git a/Telegram/SourceFiles/window/window_session_controller.cpp b/Telegram/SourceFiles/window/window_session_controller.cpp index ab65f6dccd..fe070ff6ec 100644 --- a/Telegram/SourceFiles/window/window_session_controller.cpp +++ b/Telegram/SourceFiles/window/window_session_controller.cpp @@ -1685,7 +1685,7 @@ SessionController::SessionController( #if _DEBUG // TEST: Auto-open craft box on startup constexpr auto kGiftsCount = 4; crl::on_main(this, [=] { - if (rand() % 2 < 0) { + if (rand() % 2 >= 0) { return; } const auto user = session->user();