diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt
index 4ef120b1af..7bfcc3227d 100644
--- a/Telegram/CMakeLists.txt
+++ b/Telegram/CMakeLists.txt
@@ -127,8 +127,20 @@ set(ayugram_files
ayu/ui/ayu_logo.h
ayu/ui/utils/ayu_profile_values.cpp
ayu/ui/utils/ayu_profile_values.h
+ ayu/ui/settings/settings_appearance.cpp
+ ayu/ui/settings/settings_appearance.h
+ ayu/ui/settings/settings_ayu_utils.cpp
+ ayu/ui/settings/settings_ayu_utils.h
+ ayu/ui/settings/settings_chats.cpp
+ ayu/ui/settings/settings_chats.h
+ ayu/ui/settings/settings_general.cpp
+ ayu/ui/settings/settings_general.h
ayu/ui/settings/settings_ayu.cpp
ayu/ui/settings/settings_ayu.h
+ ayu/ui/settings/settings_main.cpp
+ ayu/ui/settings/settings_main.h
+ ayu/ui/settings/settings_other.cpp
+ ayu/ui/settings/settings_other.h
ayu/ui/context_menu/context_menu.cpp
ayu/ui/context_menu/context_menu.h
ayu/ui/context_menu/menu_item_subtext.cpp
@@ -147,6 +159,10 @@ set(ayugram_files
ayu/ui/boxes/theme_selector_box.h
ayu/ui/boxes/message_shot_box.cpp
ayu/ui/boxes/message_shot_box.h
+ ayu/ui/boxes/donate_qr_box.cpp
+ ayu/ui/boxes/donate_qr_box.h
+ ayu/ui/boxes/donate_info_box.cpp
+ ayu/ui/boxes/donate_info_box.h
ayu/ui/components/image_view.cpp
ayu/ui/components/image_view.h
ayu/ui/components/icon_picker.cpp
@@ -170,6 +186,18 @@ set(ayugram_files
ayu/features/forward/ayu_forward.h
ayu/features/forward/ayu_sync.cpp
ayu/features/forward/ayu_sync.h
+ ayu/features/translator/ayu_translator.cpp
+ ayu/features/translator/ayu_translator.h
+ ayu/features/translator/html_parser.cpp
+ ayu/features/translator/html_parser.h
+ ayu/features/translator/implementations/google.cpp
+ ayu/features/translator/implementations/google.h
+ ayu/features/translator/implementations/yandex.cpp
+ ayu/features/translator/implementations/yandex.h
+ ayu/features/translator/implementations/telegram.cpp
+ ayu/features/translator/implementations/telegram.h
+ ayu/features/translator/implementations/base.cpp
+ ayu/features/translator/implementations/base.h
ayu/data/messages_storage.cpp
ayu/data/messages_storage.h
ayu/data/entities.h
diff --git a/Telegram/Resources/icons/ayu/donates/bitcoin.svg b/Telegram/Resources/icons/ayu/donates/bitcoin.svg
new file mode 100644
index 0000000000..71020dcef1
--- /dev/null
+++ b/Telegram/Resources/icons/ayu/donates/bitcoin.svg
@@ -0,0 +1,4 @@
+
diff --git a/Telegram/Resources/icons/ayu/donates/boosty.svg b/Telegram/Resources/icons/ayu/donates/boosty.svg
new file mode 100644
index 0000000000..5e798673c6
--- /dev/null
+++ b/Telegram/Resources/icons/ayu/donates/boosty.svg
@@ -0,0 +1,12 @@
+
diff --git a/Telegram/Resources/icons/ayu/donates/ethereum.svg b/Telegram/Resources/icons/ayu/donates/ethereum.svg
new file mode 100644
index 0000000000..91fce6be32
--- /dev/null
+++ b/Telegram/Resources/icons/ayu/donates/ethereum.svg
@@ -0,0 +1,7 @@
+
diff --git a/Telegram/Resources/icons/ayu/donates/solana.svg b/Telegram/Resources/icons/ayu/donates/solana.svg
new file mode 100644
index 0000000000..8f7ba85589
--- /dev/null
+++ b/Telegram/Resources/icons/ayu/donates/solana.svg
@@ -0,0 +1,19 @@
+
diff --git a/Telegram/Resources/icons/ayu/donates/support_logo.svg b/Telegram/Resources/icons/ayu/donates/support_logo.svg
new file mode 100644
index 0000000000..c1fbb73d0f
--- /dev/null
+++ b/Telegram/Resources/icons/ayu/donates/support_logo.svg
@@ -0,0 +1,4 @@
+
diff --git a/Telegram/Resources/icons/ayu/donates/ton.svg b/Telegram/Resources/icons/ayu/donates/ton.svg
new file mode 100644
index 0000000000..751a602d5f
--- /dev/null
+++ b/Telegram/Resources/icons/ayu/donates/ton.svg
@@ -0,0 +1,11 @@
+
diff --git a/Telegram/Resources/icons/ayu/donates/tron.svg b/Telegram/Resources/icons/ayu/donates/tron.svg
new file mode 100644
index 0000000000..e4610c1c05
--- /dev/null
+++ b/Telegram/Resources/icons/ayu/donates/tron.svg
@@ -0,0 +1,3 @@
+
diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index ac5ef1dec2..c907aad5d9 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -6831,6 +6831,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"ayu_AyuPreferences" = "AyuGram Preferences";
"ayu_DocsText" = "Documentation";
+"ayu_CategoryGeneral" = "General";
+"ayu_CategoryAppearance" = "Appearance";
+"ayu_CategoryChats" = "Chats";
+"ayu_CategoryOther" = "Other";
"ayu_CategoryGhostMode" = "Ghost Mode";
"ayu_CategorySpy" = "Spy";
"ayu_CategoryFilters" = "Filters";
@@ -6886,12 +6890,39 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"ayu_DisableAds" = "Disable Ads";
"ayu_DisableStories" = "Disable Stories";
"ayu_DisableCustomBackgrounds" = "Disable Custom Backgrounds";
+"ayu_SmoothScroll" = "Smooth Scroll";
"ayu_DisableNotificationsDelay" = "Disable Notify Delay";
"ayu_LocalPremium" = "Local Telegram Premium";
"ayu_DisplayGhostStatus" = "Display Ghost Mode Status";
"ayu_CopyUsernameAsLink" = "Copy Username as Link";
"ayu_HideChannelReactions" = "Hide Reactions in Channels";
"ayu_HideGroupReactions" = "Hide Reactions in Groups";
+"ayu_HideReactions" = "Hide Reactions";
+"ayu_HideReactionsInChannels" = "In Channels";
+"ayu_HideReactionsInGroups" = "In Groups";
+"ayu_QuickAdminShortcuts" = "Quick Admin Shortcuts";
+"ayu_TranslationProvider" = "Translation Provider";
+"ayu_LinksHeader" = "Links";
+"ayu_LinksChannel" = "Channel";
+"ayu_LinksChats" = "Chats";
+"ayu_LinksTranslate" = "Translate";
+"ayu_LinksDocumentation" = "Documentation";
+"ayu_CategoriesHeader" = "Categories";
+"ayu_SupportHeader" = "Support";
+"ayu_SupportDescription1" = "Support Development";
+"ayu_SupportDescription2" = "{item} and get an exclusive badge!";
+"ayu_SupportBoxHeader" = "Support Development";
+"ayu_SupportBoxInfo" = "By supporting the project, you not only contribute to its development but also get a unique badge.";
+"ayu_SupportBoxMakeDonationHeader" = "Make a Donation";
+"ayu_SupportBoxMakeDonationInfo" = "Transfer an amount of {amount1} ({amount2}) to any of the project's payment details. These can be found in the **Other** section of the app settings.";
+"ayu_SupportBoxSendProofHeader" = "Send Proof of Payment";
+"ayu_SupportBoxSendProofInfo" = "Send a photo of the payment confirmation to **{item}**. Make sure the photo clearly shows the amount, date, and time of the transfer.";
+"ayu_SupportBoxReceiveBadgeHeader" = "Receive Your Badge";
+"ayu_SupportBoxReceiveBadgeInfo" = "After payment verification, you will receive a unique badge that will be displayed on your profile and visible to other users.";
+"ayu_CrashReporting" = "Crash Reporting";
+"ayu_CrashReportingDescription" = "When this option is enabled, you'll be prompted to send a report after the app crashes. You can decide whether to send it or not.";
+"ayu_ResetSettings" = "Reset Settings";
+"ayu_ResetSettingsConfirmation" = "Are you sure you want to reset **all** AyuGram preferences to their defaults?";
"ayu_CustomizationHeader" = "Customization";
"ayu_DeletedMarkText" = "Deleted Mark";
"ayu_DeletedMarkNothing" = "Nothing";
@@ -6903,6 +6934,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"ayu_SemiTransparentDeletedMessages" = "Translucent Deleted Messages";
"ayu_HideNotificationCounters" = "Hide Notification Counters";
"ayu_HideNotificationBadge" = "Hide Notification Badge";
+"ayu_HideNotificationBadgeDescription" = "Hides the notification counter on the app icon in the taskbar and tray.";
"ayu_HideAllChats" = "Hide \"All Chats\" Tab";
"ayu_ChannelBottomButton" = "Channel Bottom Button";
"ayu_ChannelBottomButtonHide" = "Hide";
@@ -6911,8 +6943,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"ayu_SettingsShowID" = "Show Peer ID";
"ayu_SettingsShowID_Hide" = "Hide";
"ayu_SettingsShowMessageShot" = "Show Message Shot";
+"ayu_SettingsShowMessageShotDescription" = "Allows taking a screenshot of the selected messages.";
"ayu_SettingsRecentStickersCount" = "Recent Stickers Count";
"ayu_SettingsCustomizationHint" = "You must restart the application after making changes in the \"Customization\" section.";
+"ayu_MaterialSwitches" = "MD3 Switch Style";
+"ayu_RemoveMessageTail" = "Remove Message Tail";
+"ayu_HideShareButton" = "Hide Side \"Share\" Button";
+"ayu_ChatFoldersHeader" = "Chat Folders";
"ayu_SettingsContextMenuTitle" = "Choose when to show the item";
"ayu_SettingsContextMenuDescription" = "Extended menu items will be displayed if you hold CTRL or SHIFT while right-clicking on the message.";
"ayu_SettingsContextMenuItemHidden" = "Hidden";
@@ -6931,6 +6968,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"ayu_DrawerElementsHeader" = "Drawer Elements";
"ayu_TrayElementsHeader" = "Tray Elements";
"ayu_SettingsWideMultiplier" = "Wide Messages Multiplier";
+"ayu_SettingsWideMultiplierDescription" = "You can change message width for better display on wide monitors.";
"ayu_SettingsSpoofWebviewAsAndroid" = "Spoof Client as Android";
"ayu_SettingsBiggerWindow" = "Bigger Window";
"ayu_SettingsIncreaseWebviewHeight" = "Increase Content Height";
@@ -7007,6 +7045,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"ayu_FiltersClearPopupText" = "Are you sure you want to clear all filters?";
"ayu_FiltersClearPopupActionText" = "Clear";
"ayu_FiltersClearPopupAltActionText" = "Clear unknown";
+"ayu_AppIconHeader" = "App Icon";
"ayu_IconDefault" = "Default";
"ayu_IconAlternative" = "AyuGram Alt";
"ayu_IconDiscord" = "Discord";
@@ -7148,7 +7187,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"ayu_LocalPremiumNotice" = "You're using **local** Telegram Premium.\nIt **won't** give you any benefits.\n**Enjoy the star near your nickname!**";
"ayu_DeveloperPopup" = "**{item}** is a member of the **exteraGram** development team.";
"ayu_SupporterPopup" = "**{item}** supported the development of **exteraGram** or **AyuGram** and received an exclusive badge.";
+"ayu_OfficialResourcePopup" = "**{item}** is the official resource of **exteraGram** or **AyuGram**.";
"ayu_SettingsWatermark" = "AyuGram is developed and maintained by Radolyn Labs.";
+"ayu_SettingsDescription" = "Telegram Desktop fork focused on customization and ToS-breaking features.";
"ayu_ConfirmationSticker" = "Do you want to send this sticker?";
"ayu_ConfirmationGIF" = "Do you want to send this GIF?";
"ayu_ConfirmationVoice" = "Do you want to send this voice message?";
diff --git a/Telegram/Resources/qrc/ayu/ayu.qrc b/Telegram/Resources/qrc/ayu/ayu.qrc
index 150c549d8e..57b4c4ad2e 100644
--- a/Telegram/Resources/qrc/ayu/ayu.qrc
+++ b/Telegram/Resources/qrc/ayu/ayu.qrc
@@ -52,5 +52,12 @@
../../art/ayu/yaplus/app_preview.png
../../art/ayu/yaplus/app_macos.png
../../art/ayu/yaplus/app_icon.ico
+ ../../icons/ayu/donates/boosty.svg
+ ../../icons/ayu/donates/ton.svg
+ ../../icons/ayu/donates/bitcoin.svg
+ ../../icons/ayu/donates/ethereum.svg
+ ../../icons/ayu/donates/solana.svg
+ ../../icons/ayu/donates/tron.svg
+ ../../icons/ayu/donates/support_logo.svg
diff --git a/Telegram/SourceFiles/api/api_chat_invite.h b/Telegram/SourceFiles/api/api_chat_invite.h
index 123ccb1f8d..7ae7114243 100644
--- a/Telegram/SourceFiles/api/api_chat_invite.h
+++ b/Telegram/SourceFiles/api/api_chat_invite.h
@@ -14,7 +14,7 @@ class ChannelData;
namespace Info::Profile {
class Badge;
-enum class BadgeType : uchar;
+enum class BadgeType : ushort;
} // namespace Info::Profile
namespace Main {
diff --git a/Telegram/SourceFiles/ayu/ayu_infra.cpp b/Telegram/SourceFiles/ayu/ayu_infra.cpp
index 71bb7804a4..129d1e1fd0 100644
--- a/Telegram/SourceFiles/ayu/ayu_infra.cpp
+++ b/Telegram/SourceFiles/ayu/ayu_infra.cpp
@@ -11,6 +11,7 @@
#include "ayu/ayu_ui_settings.h"
#include "ayu/ayu_worker.h"
#include "ayu/data/ayu_database.h"
+#include "features/translator/ayu_translator.h"
#include "lang/lang_instance.h"
#include "utils/rc_manager.h"
@@ -32,6 +33,7 @@ void initUiSettings() {
AyuUiSettings::setMonoFont(settings.monoFont);
AyuUiSettings::setWideMultiplier(settings.wideMultiplier);
+ AyuUiSettings::setMaterialSwitches(settings.materialSwitches);
}
void initDatabase() {
@@ -46,12 +48,17 @@ void initRCManager() {
RCManager::getInstance().start();
}
+void initTranslator() {
+ Ayu::Translator::TranslateManager::init();
+}
+
void init() {
initLang();
initDatabase();
initUiSettings();
initWorker();
initRCManager();
+ initTranslator();
}
}
diff --git a/Telegram/SourceFiles/ayu/ayu_settings.cpp b/Telegram/SourceFiles/ayu/ayu_settings.cpp
index 3d55d637a8..332b9489bf 100644
--- a/Telegram/SourceFiles/ayu/ayu_settings.cpp
+++ b/Telegram/SourceFiles/ayu/ayu_settings.cpp
@@ -18,6 +18,7 @@
#include
#include "ayu_worker.h"
+#include "features/translator/ayu_translator.h"
#include "window/window_controller.h"
using json = nlohmann::json;
@@ -41,6 +42,8 @@ rpl::variable editedMarkReactive;
rpl::variable showPeerIdReactive;
+rpl::variable translationProviderReactive;
+
rpl::variable hideFromBlockedReactive;
rpl::event_stream<> historyUpdateReactive;
@@ -137,6 +140,7 @@ void postinitialize() {
deletedMarkReactive = settings->deletedMark;
editedMarkReactive = settings->editedMark;
showPeerIdReactive = settings->showPeerId;
+ translationProviderReactive = settings->translationProvider;
hideFromBlockedReactive = settings->hideFromBlocked;
@@ -194,6 +198,15 @@ void save() {
postinitialize();
}
+void reset() {
+ lifetime.destroy();
+ lifetime = rpl::lifetime();
+ settings = std::nullopt;
+ initialize();
+ postinitialize();
+ save();
+}
+
AyuGramSettings::AyuGramSettings() {
// ~ Ghost essentials
sendReadMessages = true;
@@ -229,6 +242,9 @@ AyuGramSettings::AyuGramSettings() {
increaseWebviewHeight = false;
increaseWebviewWidth = false;
+ materialSwitches = true;
+ removeMessageTail = false;
+
disableNotificationsDelay = false;
localPremium = false;
showChannelReactions = true;
@@ -243,6 +259,7 @@ AyuGramSettings::AyuGramSettings() {
#endif
;
simpleQuotesAndReplies = true;
+ hideFastShare = false;
replaceBottomInfoWithIcons = true;
deletedMark = "🧹";
editedMark = Core::IsAppLaunched() ? tr::lng_edited(tr::now) : QString("edited");
@@ -267,8 +284,17 @@ AyuGramSettings::AyuGramSettings() {
showAttachPopup = true;
showEmojiPopup = true;
+ // ~ Drawer Elements
+ showMyProfileInDrawer = true;
+ showBotsInDrawer = true;
+ showNewGroupInDrawer = true;
+ showNewChannelInDrawer = true;
+ showContactsInDrawer = true;
+ showCallsInDrawer = true;
+ showSavedMessagesInDrawer = true;
showLReadToggleInDrawer = false;
showSReadToggleInDrawer = true;
+ showNightModeToggleInDrawer = true;
showGhostToggleInDrawer = true;
showStreamerToggleInDrawer = false;
@@ -287,6 +313,7 @@ AyuGramSettings::AyuGramSettings() {
* channelBottomButton = 2 means "Discuss" + fallback to "Mute"/"Unmute"
*/
channelBottomButton = 2;
+ quickAdminShortcuts = true;
/*
* showPeerId = 0 means no ID shown
@@ -301,6 +328,10 @@ AyuGramSettings::AyuGramSettings() {
stickerConfirmation = false;
gifConfirmation = false;
voiceConfirmation = false;
+
+ translationProvider = "telegram"; // telegram, google, yandex
+
+ crashReporting = true;
}
void set_sendReadMessages(bool val) {
@@ -411,6 +442,14 @@ void set_increaseWebviewWidth(bool val) {
settings->increaseWebviewWidth = val;
}
+void set_materialSwitches(bool val) {
+ settings->materialSwitches = val;
+}
+
+void set_removeMessageTail(bool val) {
+ settings->removeMessageTail = val;
+}
+
void set_disableNotificationsDelay(bool val) {
settings->disableNotificationsDelay = val;
}
@@ -435,6 +474,10 @@ void set_simpleQuotesAndReplies(bool val) {
settings->simpleQuotesAndReplies = val;
}
+void set_hideFastShare(bool val) {
+ settings->hideFastShare = val;
+}
+
void set_replaceBottomInfoWithIcons(bool val) {
settings->replaceBottomInfoWithIcons = val;
}
@@ -508,6 +551,34 @@ void set_showEmojiPopup(bool val) {
triggerHistoryUpdate();
}
+void set_showMyProfileInDrawer(bool val) {
+ settings->showMyProfileInDrawer = val;
+}
+
+void set_showBotsInDrawer(bool val) {
+ settings->showBotsInDrawer = val;
+}
+
+void set_showNewGroupInDrawer(bool val) {
+ settings->showNewGroupInDrawer = val;
+}
+
+void set_showNewChannelInDrawer(bool val) {
+ settings->showNewChannelInDrawer = val;
+}
+
+void set_showContactsInDrawer(bool val) {
+ settings->showContactsInDrawer = val;
+}
+
+void set_showCallsInDrawer(bool val) {
+ settings->showCallsInDrawer = val;
+}
+
+void set_showSavedMessagesInDrawer(bool val) {
+ settings->showSavedMessagesInDrawer = val;
+}
+
void set_showLReadToggleInDrawer(bool val) {
settings->showLReadToggleInDrawer = val;
}
@@ -516,6 +587,10 @@ void set_showSReadToggleInDrawer(bool val) {
settings->showSReadToggleInDrawer = val;
}
+void set_showNightModeToggleInDrawer(bool val) {
+ settings->showNightModeToggleInDrawer = val;
+}
+
void set_showGhostToggleInDrawer(bool val) {
settings->showGhostToggleInDrawer = val;
}
@@ -557,6 +632,10 @@ void set_channelBottomButton(int val) {
settings->channelBottomButton = val;
}
+void set_quickAdminShortcuts(bool val) {
+ settings->quickAdminShortcuts = val;
+}
+
void set_showMessageSeconds(bool val) {
settings->showMessageSeconds = val;
}
@@ -577,6 +656,16 @@ void set_voiceConfirmation(bool val) {
settings->voiceConfirmation = val;
}
+void set_translationProvider(const QString &val) {
+ settings->translationProvider = val;
+ translationProviderReactive = val;
+ Ayu::Translator::TranslateManager::currentInstance()->resetCache();
+}
+
+void set_crashReporting(bool val) {
+ settings->crashReporting = val;
+}
+
bool isUseScheduledMessages() {
return isGhostModeActive() && settings->useScheduledMessages;
}
@@ -597,6 +686,10 @@ rpl::producer get_showPeerIdReactive() {
return showPeerIdReactive.value();
}
+rpl::producer get_translationProviderReactive() {
+ return translationProviderReactive.value();
+}
+
rpl::producer get_ghostModeEnabledReactive() {
return ghostModeEnabled.value();
}
diff --git a/Telegram/SourceFiles/ayu/ayu_settings.h b/Telegram/SourceFiles/ayu/ayu_settings.h
index 86f2d8c205..cd04ad5149 100644
--- a/Telegram/SourceFiles/ayu/ayu_settings.h
+++ b/Telegram/SourceFiles/ayu/ayu_settings.h
@@ -74,6 +74,9 @@ public:
bool increaseWebviewHeight;
bool increaseWebviewWidth;
+ bool materialSwitches;
+ bool removeMessageTail;
+
bool disableNotificationsDelay;
bool localPremium;
bool showChannelReactions;
@@ -81,6 +84,7 @@ public:
QString appIcon;
bool simpleQuotesAndReplies;
+ bool hideFastShare;
bool replaceBottomInfoWithIcons;
QString deletedMark;
QString editedMark;
@@ -101,8 +105,16 @@ public:
bool showAttachPopup;
bool showEmojiPopup;
+ bool showMyProfileInDrawer;
+ bool showBotsInDrawer;
+ bool showNewGroupInDrawer;
+ bool showNewChannelInDrawer;
+ bool showContactsInDrawer;
+ bool showCallsInDrawer;
+ bool showSavedMessagesInDrawer;
bool showLReadToggleInDrawer;
bool showSReadToggleInDrawer;
+ bool showNightModeToggleInDrawer;
bool showGhostToggleInDrawer;
bool showStreamerToggleInDrawer;
@@ -116,6 +128,7 @@ public:
bool hideAllChatsFolder;
int channelBottomButton;
+ bool quickAdminShortcuts;
int showPeerId;
bool showMessageSeconds;
@@ -124,6 +137,10 @@ public:
bool stickerConfirmation;
bool gifConfirmation;
bool voiceConfirmation;
+
+ QString translationProvider;
+
+ bool crashReporting;
};
void set_sendReadMessages(bool val);
@@ -158,6 +175,9 @@ void set_spoofWebviewAsAndroid(bool val);
void set_increaseWebviewHeight(bool val);
void set_increaseWebviewWidth(bool val);
+void set_materialSwitches(bool val);
+void set_removeMessageTail(bool val);
+
void set_disableNotificationsDelay(bool val);
void set_localPremium(bool val);
void set_hideChannelReactions(bool val);
@@ -165,6 +185,7 @@ void set_hideGroupReactions(bool val);
void set_appIcon(const QString &val);
void set_simpleQuotesAndReplies(bool val);
+void set_hideFastShare(bool val);
void set_replaceBottomInfoWithIcons(bool val);
void set_deletedMark(const QString &val);
void set_editedMark(const QString &val);
@@ -185,8 +206,16 @@ void set_showAutoDeleteButtonInMessageField(bool val);
void set_showAttachPopup(bool val);
void set_showEmojiPopup(bool val);
+void set_showMyProfileInDrawer(bool val);
+void set_showBotsInDrawer(bool val);
+void set_showNewGroupInDrawer(bool val);
+void set_showNewChannelInDrawer(bool val);
+void set_showContactsInDrawer(bool val);
+void set_showCallsInDrawer(bool val);
+void set_showSavedMessagesInDrawer(bool val);
void set_showLReadToggleInDrawer(bool val);
void set_showSReadToggleInDrawer(bool val);
+void set_showNightModeToggleInDrawer(bool val);
void set_showGhostToggleInDrawer(bool val);
void set_showStreamerToggleInDrawer(bool val);
@@ -200,6 +229,7 @@ void set_hideNotificationBadge(bool val);
void set_hideAllChatsFolder(bool val);
void set_channelBottomButton(int val);
+void set_quickAdminShortcuts(bool val);
void set_showPeerId(int val);
void set_showMessageSeconds(bool val);
@@ -209,6 +239,10 @@ void set_stickerConfirmation(bool val);
void set_gifConfirmation(bool val);
void set_voiceConfirmation(bool val);
+void set_translationProvider(const QString &val);
+
+void set_crashReporting(bool val);
+
inline void to_json(nlohmann::json &nlohmann_json_j, const AyuGramSettings &nlohmann_json_t) {
NLOHMANN_JSON_TO(sendReadMessages)
NLOHMANN_JSON_TO(sendReadStories)
@@ -232,10 +266,13 @@ inline void to_json(nlohmann::json &nlohmann_json_j, const AyuGramSettings &nloh
NLOHMANN_JSON_TO(spoofWebviewAsAndroid)
NLOHMANN_JSON_TO(increaseWebviewHeight)
NLOHMANN_JSON_TO(increaseWebviewWidth)
+ NLOHMANN_JSON_TO(materialSwitches)
+ NLOHMANN_JSON_TO(removeMessageTail)
NLOHMANN_JSON_TO(disableNotificationsDelay)
NLOHMANN_JSON_TO(localPremium)
NLOHMANN_JSON_TO(appIcon)
NLOHMANN_JSON_TO(simpleQuotesAndReplies)
+ NLOHMANN_JSON_TO(hideFastShare)
NLOHMANN_JSON_TO(replaceBottomInfoWithIcons)
NLOHMANN_JSON_TO(deletedMark)
NLOHMANN_JSON_TO(editedMark)
@@ -252,8 +289,16 @@ inline void to_json(nlohmann::json &nlohmann_json_j, const AyuGramSettings &nloh
NLOHMANN_JSON_TO(showAutoDeleteButtonInMessageField)
NLOHMANN_JSON_TO(showAttachPopup)
NLOHMANN_JSON_TO(showEmojiPopup)
+ NLOHMANN_JSON_TO(showMyProfileInDrawer)
+ NLOHMANN_JSON_TO(showBotsInDrawer)
+ NLOHMANN_JSON_TO(showNewGroupInDrawer)
+ NLOHMANN_JSON_TO(showNewChannelInDrawer)
+ NLOHMANN_JSON_TO(showContactsInDrawer)
+ NLOHMANN_JSON_TO(showCallsInDrawer)
+ NLOHMANN_JSON_TO(showSavedMessagesInDrawer)
NLOHMANN_JSON_TO(showLReadToggleInDrawer)
NLOHMANN_JSON_TO(showSReadToggleInDrawer)
+ NLOHMANN_JSON_TO(showNightModeToggleInDrawer)
NLOHMANN_JSON_TO(showGhostToggleInDrawer)
NLOHMANN_JSON_TO(showStreamerToggleInDrawer)
NLOHMANN_JSON_TO(showGhostToggleInTray)
@@ -265,12 +310,15 @@ inline void to_json(nlohmann::json &nlohmann_json_j, const AyuGramSettings &nloh
NLOHMANN_JSON_TO(hideNotificationBadge)
NLOHMANN_JSON_TO(hideAllChatsFolder)
NLOHMANN_JSON_TO(channelBottomButton)
+ NLOHMANN_JSON_TO(quickAdminShortcuts)
NLOHMANN_JSON_TO(showPeerId)
NLOHMANN_JSON_TO(showMessageSeconds)
NLOHMANN_JSON_TO(showMessageShot)
NLOHMANN_JSON_TO(stickerConfirmation)
NLOHMANN_JSON_TO(gifConfirmation)
NLOHMANN_JSON_TO(voiceConfirmation)
+ NLOHMANN_JSON_TO(translationProvider)
+ NLOHMANN_JSON_TO(crashReporting)
}
inline void from_json(const nlohmann::json &nlohmann_json_j, AyuGramSettings &nlohmann_json_t) {
@@ -297,10 +345,13 @@ inline void from_json(const nlohmann::json &nlohmann_json_j, AyuGramSettings &nl
NLOHMANN_JSON_FROM_WITH_DEFAULT(spoofWebviewAsAndroid)
NLOHMANN_JSON_FROM_WITH_DEFAULT(increaseWebviewHeight)
NLOHMANN_JSON_FROM_WITH_DEFAULT(increaseWebviewWidth)
+ NLOHMANN_JSON_FROM_WITH_DEFAULT(materialSwitches)
+ NLOHMANN_JSON_FROM_WITH_DEFAULT(removeMessageTail)
NLOHMANN_JSON_FROM_WITH_DEFAULT(disableNotificationsDelay)
NLOHMANN_JSON_FROM_WITH_DEFAULT(localPremium)
NLOHMANN_JSON_FROM_WITH_DEFAULT(appIcon)
NLOHMANN_JSON_FROM_WITH_DEFAULT(simpleQuotesAndReplies)
+ NLOHMANN_JSON_FROM_WITH_DEFAULT(hideFastShare)
NLOHMANN_JSON_FROM_WITH_DEFAULT(replaceBottomInfoWithIcons)
NLOHMANN_JSON_FROM_WITH_DEFAULT(deletedMark)
NLOHMANN_JSON_FROM_WITH_DEFAULT(editedMark)
@@ -317,8 +368,16 @@ inline void from_json(const nlohmann::json &nlohmann_json_j, AyuGramSettings &nl
NLOHMANN_JSON_FROM_WITH_DEFAULT(showAutoDeleteButtonInMessageField)
NLOHMANN_JSON_FROM_WITH_DEFAULT(showAttachPopup)
NLOHMANN_JSON_FROM_WITH_DEFAULT(showEmojiPopup)
+ NLOHMANN_JSON_FROM_WITH_DEFAULT(showMyProfileInDrawer)
+ NLOHMANN_JSON_FROM_WITH_DEFAULT(showBotsInDrawer)
+ NLOHMANN_JSON_FROM_WITH_DEFAULT(showNewGroupInDrawer)
+ NLOHMANN_JSON_FROM_WITH_DEFAULT(showNewChannelInDrawer)
+ NLOHMANN_JSON_FROM_WITH_DEFAULT(showContactsInDrawer)
+ NLOHMANN_JSON_FROM_WITH_DEFAULT(showCallsInDrawer)
+ NLOHMANN_JSON_FROM_WITH_DEFAULT(showSavedMessagesInDrawer)
NLOHMANN_JSON_FROM_WITH_DEFAULT(showLReadToggleInDrawer)
NLOHMANN_JSON_FROM_WITH_DEFAULT(showSReadToggleInDrawer)
+ NLOHMANN_JSON_FROM_WITH_DEFAULT(showNightModeToggleInDrawer)
NLOHMANN_JSON_FROM_WITH_DEFAULT(showGhostToggleInDrawer)
NLOHMANN_JSON_FROM_WITH_DEFAULT(showStreamerToggleInDrawer)
NLOHMANN_JSON_FROM_WITH_DEFAULT(showGhostToggleInTray)
@@ -330,24 +389,30 @@ inline void from_json(const nlohmann::json &nlohmann_json_j, AyuGramSettings &nl
NLOHMANN_JSON_FROM_WITH_DEFAULT(hideNotificationBadge)
NLOHMANN_JSON_FROM_WITH_DEFAULT(hideAllChatsFolder)
NLOHMANN_JSON_FROM_WITH_DEFAULT(channelBottomButton)
+ NLOHMANN_JSON_FROM_WITH_DEFAULT(quickAdminShortcuts)
NLOHMANN_JSON_FROM_WITH_DEFAULT(showPeerId)
NLOHMANN_JSON_FROM_WITH_DEFAULT(showMessageSeconds)
NLOHMANN_JSON_FROM_WITH_DEFAULT(showMessageShot)
NLOHMANN_JSON_FROM_WITH_DEFAULT(stickerConfirmation)
NLOHMANN_JSON_FROM_WITH_DEFAULT(gifConfirmation)
NLOHMANN_JSON_FROM_WITH_DEFAULT(voiceConfirmation)
+ NLOHMANN_JSON_FROM_WITH_DEFAULT(translationProvider)
+ NLOHMANN_JSON_FROM_WITH_DEFAULT(crashReporting)
}
AyuGramSettings &getInstance();
void load();
void save();
+void reset();
rpl::producer get_deletedMarkReactive();
rpl::producer get_editedMarkReactive();
rpl::producer get_showPeerIdReactive();
+rpl::producer get_translationProviderReactive();
+
bool isGhostModeActive();
bool isUseScheduledMessages();
diff --git a/Telegram/SourceFiles/ayu/ayu_url_handlers.cpp b/Telegram/SourceFiles/ayu/ayu_url_handlers.cpp
index ea1eed9942..82c1ed9b11 100644
--- a/Telegram/SourceFiles/ayu/ayu_url_handlers.cpp
+++ b/Telegram/SourceFiles/ayu/ayu_url_handlers.cpp
@@ -20,6 +20,9 @@
#include
#include "main/main_session.h"
+#include "ui/boxes/donate_info_box.h"
+#include "ui/settings/settings_main.h"
+#include "window/window_controller.h"
namespace AyuUrlHandlers {
@@ -68,7 +71,40 @@ bool HandleAyu(
if (!controller) {
return false;
}
- controller->showToast(QString(":3"), 500);
+
+ try {
+ const auto section = match->captured(1).mid(1).toLower();
+ const auto type = [&]() -> std::optional<::Settings::Type>
+ {
+ if (section == u"settings"_q || section == u"preferences"_q || section == u"prefs"_q) {
+ return ::Settings::AyuMain::Id();
+ }
+ return std::nullopt;
+ }();
+
+ if (type.has_value()) {
+ controller->showSettings(*type);
+ controller->window().activate();
+ } else {
+ controller->showToast(QString(":3"), 500);
+ }
+ } catch (...) {
+ }
+
+ return true;
+}
+
+bool HandleSupport(
+ Window::SessionController *controller,
+ const Match &match,
+ const QVariant &context) {
+ if (!controller) {
+ return false;
+ }
+ auto box = Box(
+ Ui::FillDonateInfoBox,
+ controller);
+ Ui::show(std::move(box));
return true;
}
diff --git a/Telegram/SourceFiles/ayu/ayu_url_handlers.h b/Telegram/SourceFiles/ayu/ayu_url_handlers.h
index e8b3476633..c92759d4ed 100644
--- a/Telegram/SourceFiles/ayu/ayu_url_handlers.h
+++ b/Telegram/SourceFiles/ayu/ayu_url_handlers.h
@@ -23,6 +23,11 @@ bool HandleAyu(
const Match &match,
const QVariant &context);
+bool HandleSupport(
+ Window::SessionController *controller,
+ const Match &match,
+ const QVariant &context);
+
bool TryHandleSpotify(const QString &url);
}
diff --git a/Telegram/SourceFiles/ayu/features/messageshot/message_shot.cpp b/Telegram/SourceFiles/ayu/features/messageshot/message_shot.cpp
index 581103ff68..c169d37be3 100644
--- a/Telegram/SourceFiles/ayu/features/messageshot/message_shot.cpp
+++ b/Telegram/SourceFiles/ayu/features/messageshot/message_shot.cpp
@@ -6,8 +6,8 @@
// Copyright @Radolyn, 2025
#include "message_shot.h"
-#include "styles/style_layers.h"
#include "styles/style_ayu_styles.h"
+#include "styles/style_layers.h"
#include "qguiapplication.h"
#include "ayu/ui/boxes/message_shot_box.h"
@@ -20,8 +20,8 @@
#include "history/history.h"
#include "history/history_inner_widget.h"
#include "history/history_item.h"
-#include "history/view/history_view_element.h"
#include "history/history_item_components.h"
+#include "history/view/history_view_element.h"
#include "history/view/media/history_view_media.h"
#include "main/main_session.h"
#include "styles/style_chat.h"
diff --git a/Telegram/SourceFiles/ayu/features/translator/ayu_translator.cpp b/Telegram/SourceFiles/ayu/features/translator/ayu_translator.cpp
new file mode 100644
index 0000000000..5f845ad2ce
--- /dev/null
+++ b/Telegram/SourceFiles/ayu/features/translator/ayu_translator.cpp
@@ -0,0 +1,332 @@
+// This is the source code of AyuGram for Desktop.
+//
+// We do not and cannot prevent the use of our code,
+// but be respectful and credit the original author.
+//
+// Copyright @Radolyn, 2025
+#include "ayu_translator.h"
+
+#include
+#include
+#include
+#include
+
+#include "api/api_text_entities.h"
+#include "ayu/ayu_settings.h"
+#include "data/data_peer.h"
+#include "data/data_session.h"
+#include "history/history_item.h"
+#include "implementations/google.h"
+#include "implementations/telegram.h"
+#include "implementations/yandex.h"
+#include "main/main_session.h"
+
+// todo: expose available languages from current translator and use in `ChooseTranslateToBox`
+
+namespace Ayu::Translator {
+
+TranslateManager::Builder::Builder(
+ TranslateManager &manager,
+ Main::Session &session,
+ const MTPflags &flags,
+ const MTPInputPeer &peer,
+ const MTPVector &id,
+ const MTPVector &text,
+ const MTPstring &to_lang
+)
+ : _manager(&manager)
+ , _session(&session)
+ , _flags(flags)
+ , _peer(peer)
+ , _idList(id)
+ , _text(text)
+ , _toLang(to_lang) {
+}
+
+TranslateManager::Builder &TranslateManager::Builder::done(std::function < void(const Result &) > cb) {
+ _done = std::move(cb);
+ return *this;
+}
+
+TranslateManager::Builder &TranslateManager::Builder::fail(std::function < void(const MTP::Error &) > cb) {
+ _fail = std::move(cb);
+ return *this;
+}
+
+TranslateManager::Builder &TranslateManager::Builder::fail(std::function cb) {
+ _fail = [cb = std::move(cb)](const MTP::Error &)
+ {
+ cb();
+ };
+ return *this;
+}
+
+mtpRequestId TranslateManager::Builder::send() {
+ return _manager->performTranslation(*this);
+}
+
+void TranslateManager::Builder::cancel() {
+ if (_id) {
+ _manager->cancel(_id);
+ _id = 0;
+ }
+}
+
+TranslateManager::Builder TranslateManager::request(
+ Main::Session &session,
+ const MTPflags &flags,
+ const MTPInputPeer &peer,
+ const MTPVector &id,
+ const MTPVector &text,
+ const MTPstring &to_lang
+) {
+ return Builder(*this, session, flags, peer, id, text, to_lang);
+}
+
+mtpRequestId TranslateManager::performTranslation(Builder &req) {
+ const auto id = _nextId++;
+ _pending.emplace(id,
+ Pending{
+ .done = std::move(req._done),
+ .fail = std::move(req._fail),
+ .cancel = nullptr,
+ });
+ req._id = id;
+
+ std::vector texts;
+ std::vector cacheKeys;
+ std::vector resultTexts;
+ std::vector uncachedIndices;
+ std::vector uncachedTexts;
+
+ const auto toLang = qs(req._toLang);
+ const auto fromLang = QStringLiteral("auto");
+
+ if (!req.texts().v.isEmpty()) {
+ for (int i = 0; i < req.texts().v.size(); ++i) {
+ const auto text = qs(req.texts().v[i].data().vtext());
+ const auto entities = Api::EntitiesFromMTP(req.session(), req.texts().v[i].data().ventities().v);
+ const auto textWithEntities = TextWithEntities{
+ .text = text,
+ .entities = entities
+ };
+ texts.push_back(textWithEntities);
+
+ // todo: entities are not considered in cache key
+ const auto key = generateCacheKey(text, fromLang, toLang);
+ cacheKeys.push_back(key);
+
+ if (const auto cached = getFromCache(key)) {
+ resultTexts.push_back(cached->translatedText);
+ } else {
+ resultTexts.push_back({});
+ uncachedIndices.push_back(i);
+ uncachedTexts.push_back(textWithEntities);
+ }
+ }
+ } else if (!req.ids().v.isEmpty()) {
+ if (const auto peerData = Data::PeerFromInputMTP(&req.session()->data(), req.peer())) {
+ for (int i = 0; i < req.ids().v.size(); ++i) {
+ const auto msgId = req.ids().v[i].v;
+ if (const auto message = req.session()->data().message(peerData->id, msgId)) {
+ const auto textWithEntities = message->originalText();
+ texts.push_back(textWithEntities);
+
+ const auto key = generateMessageCacheKey(peerData->id, msgId, fromLang, toLang);
+ cacheKeys.push_back(key);
+
+ if (const auto cached = getFromCache(key)) {
+ resultTexts.push_back(cached->translatedText);
+ } else {
+ resultTexts.push_back({});
+ uncachedIndices.push_back(i);
+ uncachedTexts.push_back(textWithEntities);
+ }
+ } else {
+ // todo: ??
+ texts.push_back({});
+ cacheKeys.push_back(QString());
+ resultTexts.push_back({});
+ }
+ }
+ }
+ }
+
+ if (texts.empty() || toLang.isEmpty()) {
+ triggerFail(id);
+ return id;
+ }
+
+ if (uncachedTexts.empty()) {
+ auto vec = QVector();
+ for (const auto &translatedText : resultTexts) {
+ vec.push_back(MTP_textWithEntities(
+ MTP_string(translatedText.text),
+ Api::EntitiesToMTP(req.session(), translatedText.entities)));
+ }
+ const auto result = MTP_messages_translateResult(MTP_vector(vec));
+ triggerDone(id, result);
+ return id;
+ }
+
+ CallbackSuccess onSuccess = [this, id, resultTexts = std::move(resultTexts), cacheKeys = std::move(cacheKeys),
+ uncachedIndices = std::move(uncachedIndices), texts = std::move(texts),
+ fromLang, toLang, &req](const std::vector &translated) mutable
+ {
+ for (size_t i = 0; i < translated.size() && i < uncachedIndices.size(); ++i) {
+ const auto index = uncachedIndices[i];
+ resultTexts[index] = translated[i];
+
+ const auto &key = cacheKeys[index];
+ if (!key.isEmpty()) {
+ insertToCache(key,
+ CacheEntry{
+ .originalText = texts[index],
+ .translatedText = translated[i],
+ .fromLang = fromLang,
+ .toLang = toLang
+ });
+ }
+ }
+
+ auto vec = QVector();
+ for (const auto &translatedText : resultTexts) {
+ vec.push_back(MTP_textWithEntities(
+ MTP_string(translatedText.text),
+ Api::EntitiesToMTP(req.session(), translatedText.entities)));
+ }
+ const auto result = MTP_messages_translateResult(MTP_vector(vec));
+ triggerDone(id, result);
+ };
+
+ CallbackFail onFail = [this, id]
+ {
+ triggerFail(id);
+ };
+
+ const auto args = StartTranslationArgs{
+ .session = req.session(),
+ .requestData = {
+ .flags = req.flags(),
+ .peer = req.peer(),
+ .idList = req.ids(),
+ .text = req.texts(),
+ .toLang = req.toLang(),
+ },
+ .parsedData = {
+ .texts = uncachedTexts,
+ .fromLang = fromLang,
+ .toLang = toLang,
+ },
+ .onSuccess = std::move(onSuccess),
+ .onFail = std::move(onFail),
+ };
+
+ if (const auto it = _pending.find(id); it != _pending.end()) {
+ const auto &settings = AyuSettings::getInstance();
+ if (settings.translationProvider == "telegram") {
+ it->second.cancel = TelegramTranslator::instance().startTranslation(args);
+ } else if (settings.translationProvider == "yandex") {
+ it->second.cancel = YandexTranslator::instance().startTranslation(args);
+ } else {
+ it->second.cancel = GoogleTranslator::instance().startTranslation(args);
+ }
+ }
+
+ return id;
+}
+
+bool TranslateManager::cancel(mtpRequestId requestId) {
+ const auto it = _pending.find(requestId);
+ if (it == _pending.end()) return false;
+ if (it->second.cancel) {
+ it->second.cancel();
+ }
+ _pending.erase(it);
+ return true;
+}
+
+bool TranslateManager::triggerDone(mtpRequestId id, const Result &result) {
+ const auto it = _pending.find(id);
+ if (it == _pending.end()) return false;
+ auto cb = std::move(it->second.done);
+ _pending.erase(it);
+ if (cb) cb(result);
+ return true;
+}
+
+bool TranslateManager::triggerFail(mtpRequestId id) {
+ const auto it = _pending.find(id);
+ if (it == _pending.end()) return false;
+ auto cb = std::move(it->second.fail);
+ _pending.erase(it);
+ if (cb) cb(MTP::Error(MTP::Error::MTPLocal("RESPONSE_PARSE_FAILED", "Error parse failed.")));
+ return true;
+}
+
+void TranslateManager::resetCache() {
+ _cacheList.clear();
+ _cacheMap.clear();
+
+ // todo: remove all running requests
+}
+
+TranslateManager *TranslateManager::currentInstance() {
+ return instance;
+}
+
+TranslateManager *TranslateManager::instance = nullptr;
+
+void TranslateManager::init() {
+ if (!instance) instance = new TranslateManager;
+}
+
+QString TranslateManager::generateCacheKey(const QString &text, const QString &fromLang, const QString &toLang) const {
+ const auto textHash = QCryptographicHash::hash(text.toUtf8(), QCryptographicHash::Sha1).toHex();
+ return QStringLiteral("%1_%2_%3").arg(QString::fromLatin1(textHash), fromLang, toLang);
+}
+
+QString TranslateManager::generateMessageCacheKey(PeerId peerId,
+ MsgId msgId,
+ const QString &fromLang,
+ const QString &toLang) const {
+ return QStringLiteral("%1_%2_%3_%4").arg(peerId.value).arg(msgId.bare).arg(fromLang, toLang);
+}
+
+void TranslateManager::insertToCache(const QString &key, const CacheEntry &entry) {
+ if (const auto it = _cacheMap.find(key); it != _cacheMap.end()) {
+ _cacheList.erase(it->second);
+ _cacheMap.erase(it);
+ }
+
+ _cacheList.emplace_front(key, entry);
+ _cacheMap[key] = _cacheList.begin();
+
+ if (_cacheList.size() > MAX_CACHE_SIZE) {
+ removeLeastRecentlyUsed();
+ }
+}
+
+std::optional TranslateManager::getFromCache(const QString &key) {
+ const auto it = _cacheMap.find(key);
+ if (it == _cacheMap.end()) {
+ return std::nullopt;
+ }
+
+ auto entry = it->second->second;
+ _cacheList.erase(it->second);
+ _cacheList.emplace_front(key, entry);
+ _cacheMap[key] = _cacheList.begin();
+
+ return entry;
+}
+
+void TranslateManager::removeLeastRecentlyUsed() {
+ if (_cacheList.empty()) return;
+
+ const auto &lru = _cacheList.back();
+ _cacheMap.erase(lru.first);
+ _cacheList.pop_back();
+}
+
+}
\ No newline at end of file
diff --git a/Telegram/SourceFiles/ayu/features/translator/ayu_translator.h b/Telegram/SourceFiles/ayu/features/translator/ayu_translator.h
new file mode 100644
index 0000000000..937b8bfd8c
--- /dev/null
+++ b/Telegram/SourceFiles/ayu/features/translator/ayu_translator.h
@@ -0,0 +1,137 @@
+// This is the source code of AyuGram for Desktop.
+//
+// We do not and cannot prevent the use of our code,
+// but be respectful and credit the original author.
+//
+// Copyright @Radolyn, 2025
+#pragma once
+
+#include "mtproto/sender.h"
+
+#include
+#include
+#include
+#include
+
+#include "implementations/base.h"
+
+class QNetworkReply;
+
+namespace Main {
+class Session;
+}
+
+namespace Ayu::Translator {
+
+class TranslateManager
+{
+public:
+ using Request = MTPmessages_TranslateText;
+ using Result = Request::ResponseType;
+
+ class Builder
+ {
+ public:
+ Builder(
+ TranslateManager &manager,
+ Main::Session &session,
+ const MTPflags &flags,
+ const MTPInputPeer &peer,
+ const MTPVector &id,
+ const MTPVector &text,
+ const MTPstring &to_lang
+ );
+
+ Builder(Builder &&) noexcept = default;
+ Builder &operator=(Builder &&) noexcept = default;
+
+ Builder &done(std::function cb);
+ Builder &fail(std::function cb);
+ Builder &fail(std::function cb);
+
+ mtpRequestId send();
+ void cancel();
+
+ [[nodiscard]] const auto &session() const { return _session; }
+ [[nodiscard]] const auto &flags() const { return _flags; }
+ [[nodiscard]] const auto &peer() const { return _peer; }
+ [[nodiscard]] const auto &ids() const { return _idList; }
+ [[nodiscard]] const auto &texts() const { return _text; }
+ [[nodiscard]] const auto &toLang() const { return _toLang; }
+
+ private:
+ TranslateManager *_manager = nullptr;
+ not_null _session;
+ mtpRequestId _id = 0;
+
+ MTPflags _flags;
+ MTPInputPeer _peer;
+ MTPVector _idList;
+ MTPVector _text;
+ MTPstring _toLang;
+
+ std::function _done;
+ std::function _fail;
+
+ friend class TranslateManager;
+ };
+
+ TranslateManager() = default;
+ ~TranslateManager() = default;
+
+ Builder request(
+ Main::Session &session,
+ const MTPflags &flags,
+ const MTPInputPeer &peer,
+ const MTPVector &id,
+ const MTPVector &text,
+ const MTPstring &to_lang
+ );
+
+ [[nodiscard]] mtpRequestId performTranslation(Builder &req);
+
+ bool cancel(mtpRequestId requestId);
+
+ bool triggerDone(mtpRequestId id, const Result &result);
+ bool triggerFail(mtpRequestId id);
+
+ void resetCache();
+
+ static TranslateManager *currentInstance();
+ static void init();
+ static TranslateManager *instance;
+
+private:
+ struct CacheEntry
+ {
+ TextWithEntities originalText;
+ TextWithEntities translatedText;
+ QString fromLang;
+ QString toLang;
+ };
+
+ using CacheKey = QString;
+ using CacheIterator = std::list>::iterator;
+
+ std::list> _cacheList;
+ std::unordered_map _cacheMap;
+ static constexpr size_t MAX_CACHE_SIZE = 500;
+
+ QString generateCacheKey(const QString &text, const QString &fromLang, const QString &toLang) const;
+ QString generateMessageCacheKey(PeerId peerId, MsgId msgId, const QString &fromLang, const QString &toLang) const;
+ void insertToCache(const QString &key, const CacheEntry &entry);
+ std::optional getFromCache(const QString &key);
+ void removeLeastRecentlyUsed();
+
+ struct Pending
+ {
+ std::function done;
+ std::function fail;
+ CallbackCancel cancel;
+ };
+
+ mtpRequestId _nextId = 1;
+ std::unordered_map _pending;
+};
+
+} // namespace Ayu::Translator
diff --git a/Telegram/SourceFiles/ayu/features/translator/html_parser.cpp b/Telegram/SourceFiles/ayu/features/translator/html_parser.cpp
new file mode 100644
index 0000000000..423895dc1c
--- /dev/null
+++ b/Telegram/SourceFiles/ayu/features/translator/html_parser.cpp
@@ -0,0 +1,29 @@
+// This is the source code of AyuGram for Desktop.
+//
+// We do not and cannot prevent the use of our code,
+// but be respectful and credit the original author.
+//
+// Copyright @Radolyn, 2025
+#include "html_parser.h"
+
+namespace Ayu::Translator::Html {
+
+// yandex messes up HTML badly, so formatting removed for now
+
+QString entitiesToHtml(const TextWithEntities &text) {
+ return text.text;
+}
+
+TextWithEntities htmlToEntities(const QString &text) {
+ TextWithEntities result = {.text = text};
+
+ // links parsing doesn't work actually as it's not even accounted in ParseEntities
+ // todo: find a way to parse links
+ TextUtilities::ApplyServerCleaning(result);
+ TextUtilities::ParseEntities(result, TextParseLinks | TextParseMentions | TextParseHashtags | TextParseBotCommands);
+ TextUtilities::Trim(result);
+
+ return result;
+}
+
+}
\ No newline at end of file
diff --git a/Telegram/SourceFiles/ayu/features/translator/html_parser.h b/Telegram/SourceFiles/ayu/features/translator/html_parser.h
new file mode 100644
index 0000000000..60bb4f20f4
--- /dev/null
+++ b/Telegram/SourceFiles/ayu/features/translator/html_parser.h
@@ -0,0 +1,17 @@
+// This is the source code of AyuGram for Desktop.
+//
+// We do not and cannot prevent the use of our code,
+// but be respectful and credit the original author.
+//
+// Copyright @Radolyn, 2025
+#pragma once
+
+#include
+#include "ui/text/text_entity.h"
+
+namespace Ayu::Translator::Html {
+
+[[nodiscard]] QString entitiesToHtml(const TextWithEntities &text);
+[[nodiscard]] TextWithEntities htmlToEntities(const QString &text);
+
+}
\ No newline at end of file
diff --git a/Telegram/SourceFiles/ayu/features/translator/implementations/base.cpp b/Telegram/SourceFiles/ayu/features/translator/implementations/base.cpp
new file mode 100644
index 0000000000..00527dbb29
--- /dev/null
+++ b/Telegram/SourceFiles/ayu/features/translator/implementations/base.cpp
@@ -0,0 +1,305 @@
+// This is the source code of AyuGram for Desktop.
+//
+// We do not and cannot prevent the use of our code,
+// but be respectful and credit the original author.
+//
+// Copyright @Radolyn, 2025
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "base/random.h"
+
+#include
+#include
+#include
+
+#include "./base.h"
+
+namespace Ayu::Translator {
+
+std::vector desktopUserAgents = {
+ // zen
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0",
+ // cent
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36",
+ // orion mac
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Safari/605.1.15",
+ // chrome mac
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36"
+};
+
+QString randomDesktopUserAgent() {
+ return desktopUserAgents[base::RandomIndex(static_cast(desktopUserAgents.size()))];
+}
+
+bool shouldWrapInHtml() {
+ // todo: make an option
+ return true;
+}
+
+QString parseJsonPath(const QByteArray &body, const QString &jsonPath, bool *ok) {
+ if (ok) *ok = false;
+ if (body.isEmpty()) {
+ return {};
+ }
+
+ QJsonParseError err{};
+ const auto doc = QJsonDocument::fromJson(body, &err);
+ if (err.error != QJsonParseError::NoError || doc.isNull()) {
+ return {};
+ }
+
+ QJsonValue current;
+ if (doc.isObject()) {
+ current = doc.object();
+ } else if (doc.isArray()) {
+ current = doc.array();
+ } else {
+ return {};
+ }
+
+ auto indexInto = [](const QJsonValue &val, int idx, bool *okPtr) -> QJsonValue
+ {
+ if (!val.isArray()) {
+ if (okPtr) *okPtr = false;
+ return QJsonValue{};
+ }
+ const auto arr = val.toArray();
+ if (idx < 0 || idx >= arr.size()) {
+ if (okPtr) *okPtr = false;
+ return QJsonValue{};
+ }
+ if (okPtr) *okPtr = true;
+ return arr.at(idx);
+ };
+
+ const auto parts = jsonPath.split('.', Qt::SkipEmptyParts);
+ for (const auto &partRaw : parts) {
+ QString part = partRaw;
+
+ int pos = 0;
+ if (!part.isEmpty() && part[0] != '[') {
+ const int bracket = part.indexOf('[');
+ QString key = (bracket >= 0) ? part.left(bracket) : part;
+ pos = key.size();
+
+ if (!key.isEmpty()) {
+ if (!current.isObject()) {
+ return {};
+ }
+ current = current.toObject().value(key);
+ if (current.isUndefined() || current.isNull()) {
+ return {};
+ }
+ }
+ }
+
+ while (pos < part.size() && part[pos] == '[') {
+ const int end = part.indexOf(']', pos + 1);
+ if (end < 0) return {};
+ const auto idxStr = part.mid(pos + 1, end - pos - 1);
+ bool okIndex = false;
+ int idx = idxStr.toInt(&okIndex);
+ if (!okIndex) return {};
+
+ bool okStep = false;
+ current = indexInto(current, idx, &okStep);
+ if (!okStep) return {};
+ pos = end + 1;
+ }
+ }
+
+ QString result;
+ if (current.isString()) {
+ result = current.toString();
+ } else if (current.isArray()) {
+ const auto arr = current.toArray();
+ result.reserve(256);
+ for (const auto &v : arr) {
+ if (v.isObject()) {
+ const auto o = v.toObject();
+ if (o.contains("trans")) {
+ result += o.value("trans").toString();
+ } else if (o.contains("text")) {
+ result += o.value("text").toString();
+ }
+ } else if (v.isString()) {
+ result += v.toString();
+ }
+ }
+ } else if (current.isObject()) {
+ const auto o = current.toObject();
+ if (o.contains("trans")) {
+ result = o.value("trans").toString();
+ } else if (o.contains("text")) {
+ result = o.value("text").toString();
+ }
+ }
+
+ if (ok) *ok = !result.isNull();
+ return result;
+}
+
+CallbackCancel MultiThreadTranslator::startTranslation(const StartTranslationArgs &args) {
+ const auto &texts = args.parsedData.texts;
+ const auto &fromLang = args.parsedData.fromLang;
+ const auto &toLang = args.parsedData.toLang;
+ if (texts.empty() || toLang.trimmed().isEmpty()) {
+ if (args.onFail) args.onFail();
+ return []
+ {
+ };
+ }
+
+ struct BatchState
+ {
+ MultiThreadTranslator *self = nullptr;
+ std::vector inputs;
+ QString from;
+ QString to;
+ CallbackSuccess onSuccess;
+ CallbackFail onFail;
+
+ std::vector results;
+ std::vector> replies;
+ std::vector retryCount;
+ std::vector> retryTimers;
+ int total = 0;
+ int nextIndex = 0;
+ int inProgress = 0;
+ bool finished = false;
+
+ std::function pump;
+ std::function tryTranslateIndex;
+
+ void cancelAll() const {
+ for (auto &r : replies) {
+ if (r && r->isRunning()) {
+ r->abort();
+ }
+ }
+ for (auto &timer : retryTimers) {
+ if (timer) {
+ timer->stop();
+ }
+ }
+ }
+ };
+
+ auto state = std::make_shared();
+ state->self = this;
+ state->inputs = texts;
+ state->from = fromLang;
+ state->to = toLang;
+ state->onSuccess = args.onSuccess;
+ state->onFail = args.onFail;
+ state->total = static_cast(texts.size());
+ state->results.resize(state->total);
+ state->replies.resize(state->total);
+ state->retryCount.resize(state->total, 0);
+ state->retryTimers.resize(state->total);
+
+ const auto maxConcurrent = getConcurrencyLimit();
+ const auto maxRetries = getMaxRetries();
+ const auto baseWaitTime = getBaseWaitTimeMs();
+
+ auto finishFail = [state]()
+ {
+ if (state->finished) return;
+ state->finished = true;
+ state->cancelAll();
+ if (state->onFail) state->onFail();
+ };
+
+ auto finishSuccess = [state]()
+ {
+ if (state->finished) return;
+ state->finished = true;
+ if (state->onSuccess) state->onSuccess(state->results);
+ };
+
+ state->tryTranslateIndex = [state, finishFail, finishSuccess, maxRetries, baseWaitTime](int i) mutable
+ {
+ if (state->finished) return;
+
+ MultiThreadArgs singleArgs;
+ singleArgs.parsedData.text = state->inputs[i];
+ singleArgs.parsedData.fromLang = state->from;
+ singleArgs.parsedData.toLang = state->to;
+ singleArgs.onSuccess = [state, i, finishSuccess](const TextWithEntities &translated) mutable
+ {
+ if (state->finished) return;
+ state->results[i] = translated;
+ state->replies[i] = nullptr;
+ state->inProgress--;
+ if (state->nextIndex >= state->total && state->inProgress == 0) {
+ finishSuccess();
+ return;
+ }
+ if (!state->finished && state->pump) {
+ state->pump();
+ }
+ };
+ singleArgs.onFail = [state, i, finishFail, maxRetries, baseWaitTime]() mutable
+ {
+ if (state->finished) return;
+
+ state->replies[i] = nullptr;
+ state->retryCount[i]++;
+
+ if (state->retryCount[i] >= maxRetries) {
+ finishFail();
+ return;
+ }
+
+ const int delayMs = static_cast(baseWaitTime * std::pow(2.0, state->retryCount[i] - 1));
+
+ auto timer = new QTimer(state->self);
+ state->retryTimers[i] = timer;
+ timer->setSingleShot(true);
+
+ QObject::connect(timer,
+ &QTimer::timeout,
+ [state, i, timer]() mutable
+ {
+ if (state->finished) return;
+ timer->deleteLater();
+ state->retryTimers[i] = nullptr;
+ state->tryTranslateIndex(i);
+ });
+
+ timer->start(delayMs);
+ };
+
+ const auto r = state->self->startSingleTranslation(singleArgs);
+ state->replies[i] = r;
+ if (!r && !state->finished) {
+ singleArgs.onFail();
+ }
+ };
+
+ state->pump = [state, maxConcurrent]() mutable
+ {
+ if (state->finished) return;
+ while (!state->finished && state->inProgress < maxConcurrent && state->nextIndex < state->total) {
+ const int i = state->nextIndex++;
+ state->inProgress++;
+ state->tryTranslateIndex(i);
+ }
+ };
+
+ state->pump();
+
+ return [state, finishFail]() mutable
+ {
+ finishFail();
+ };
+}
+
+}
diff --git a/Telegram/SourceFiles/ayu/features/translator/implementations/base.h b/Telegram/SourceFiles/ayu/features/translator/implementations/base.h
new file mode 100644
index 0000000000..198582a904
--- /dev/null
+++ b/Telegram/SourceFiles/ayu/features/translator/implementations/base.h
@@ -0,0 +1,121 @@
+// This is the source code of AyuGram for Desktop.
+//
+// We do not and cannot prevent the use of our code,
+// but be respectful and credit the original author.
+//
+// Copyright @Radolyn, 2025
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include "apiwrap.h"
+
+namespace Main {
+class Session;
+}
+
+namespace Ayu::Translator {
+
+using CallbackSuccess = std::function &)>;
+using CallbackFail = std::function;
+using CallbackCancel = std::function;
+
+using MultiThreadCallbackSuccess = std::function;
+
+QString randomDesktopUserAgent();
+
+bool shouldWrapInHtml();
+
+QString parseJsonPath(const QByteArray &body, const QString &jsonPath, bool *ok);
+
+struct PassedData
+{
+ MTPflags flags;
+ MTPInputPeer peer;
+ MTPVector idList;
+ MTPVector text;
+ MTPstring toLang;
+};
+
+struct ParsedData
+{
+ std::vector texts;
+ QString fromLang;
+ QString toLang;
+};
+
+struct StartTranslationArgs
+{
+ Main::Session *session;
+
+ PassedData requestData;
+ ParsedData parsedData;
+
+ CallbackSuccess onSuccess;
+ CallbackFail onFail;
+};
+
+struct ParsedDataSingle
+{
+ TextWithEntities text;
+ QString fromLang;
+ QString toLang;
+};
+
+struct MultiThreadArgs
+{
+ ParsedDataSingle parsedData;
+
+ MultiThreadCallbackSuccess onSuccess;
+ CallbackFail onFail;
+};
+
+class BaseTranslator : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit BaseTranslator(QObject *parent = nullptr)
+ : QObject(parent) {
+ }
+
+ ~BaseTranslator() override = default;
+
+ [[nodiscard]] virtual QSet supportedLanguages() const { return {}; }
+
+ [[nodiscard]] virtual CallbackCancel startTranslation(
+ const StartTranslationArgs &args
+ ) = 0;
+};
+
+class MultiThreadTranslator : public BaseTranslator
+{
+ Q_OBJECT
+
+public:
+ explicit MultiThreadTranslator(QObject *parent = nullptr)
+ : BaseTranslator(parent) {
+ }
+
+ ~MultiThreadTranslator() override = default;
+
+ [[nodiscard]] virtual int getConcurrencyLimit() const { return 1; }
+ [[nodiscard]] virtual int getMaxRetries() const { return 3; }
+ [[nodiscard]] virtual int getBaseWaitTimeMs() const { return 1000; }
+
+ [[nodiscard]] CallbackCancel startTranslation(
+ const StartTranslationArgs &args
+ ) override;
+
+ [[nodiscard]] virtual QPointer startSingleTranslation(
+ const MultiThreadArgs &args
+ ) = 0;
+};
+
+}
\ No newline at end of file
diff --git a/Telegram/SourceFiles/ayu/features/translator/implementations/google.cpp b/Telegram/SourceFiles/ayu/features/translator/implementations/google.cpp
new file mode 100644
index 0000000000..71dbcf34d2
--- /dev/null
+++ b/Telegram/SourceFiles/ayu/features/translator/implementations/google.cpp
@@ -0,0 +1,109 @@
+// This is the source code of AyuGram for Desktop.
+//
+// We do not and cannot prevent the use of our code,
+// but be respectful and credit the original author.
+//
+// Copyright @Radolyn, 2025
+#include "google.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "ayu/features/translator/html_parser.h"
+
+namespace Ayu::Translator {
+
+GoogleTranslator &GoogleTranslator::instance() {
+ static GoogleTranslator inst;
+ return inst;
+}
+
+GoogleTranslator::GoogleTranslator(QObject *parent)
+ : MultiThreadTranslator(parent) {
+}
+
+QPointer GoogleTranslator::startSingleTranslation(
+ const MultiThreadArgs &args
+) {
+ const auto &text = args.parsedData.text;
+ const auto &fromLang = args.parsedData.fromLang;
+ const auto &toLang = args.parsedData.toLang;
+ const auto onSuccess = args.onSuccess;
+ const auto onFail = args.onFail;
+
+ if (text.empty() || toLang.isEmpty()) {
+ if (onFail) onFail();
+ return nullptr;
+ }
+
+ const auto from = fromLang.trimmed().isEmpty() ? QStringLiteral("auto") : fromLang.trimmed();
+ const auto to = toLang.trimmed();
+
+ QUrl url(QStringLiteral("https://translate.googleapis.com/translate_a/single"));
+ QUrlQuery query;
+ query.addQueryItem(QStringLiteral("dj"), QStringLiteral("1"));
+ query.addQueryItem(QStringLiteral("q"), shouldWrapInHtml() ? Html::entitiesToHtml(text) : text.text);
+ query.addQueryItem(QStringLiteral("sl"), from);
+ query.addQueryItem(QStringLiteral("tl"), to);
+ query.addQueryItem(QStringLiteral("ie"), QStringLiteral("UTF-8"));
+ query.addQueryItem(QStringLiteral("oe"), QStringLiteral("UTF-8"));
+ query.addQueryItem(QStringLiteral("client"), QStringLiteral("at"));
+ query.addQueryItem(QStringLiteral("dt"), QStringLiteral("t"));
+ query.addQueryItem(QStringLiteral("otf"), QStringLiteral("2"));
+ url.setQuery(query);
+
+ QNetworkRequest req(url);
+ const auto userAgent = randomDesktopUserAgent();
+ req.setHeader(QNetworkRequest::UserAgentHeader, userAgent);
+
+ QPointer reply = _nam.get(req);
+
+ auto timer = new QTimer(reply);
+ timer->setSingleShot(true);
+ timer->setInterval(15000);
+ QObject::connect(timer,
+ &QTimer::timeout,
+ reply,
+ [reply]
+ {
+ if (!reply) return;
+ if (reply->isRunning()) reply->abort();
+ });
+ timer->start();
+
+ QObject::connect(reply,
+ &QNetworkReply::finished,
+ reply,
+ [reply, onSuccess = onSuccess, onFail = onFail, timer]
+ {
+ if (!reply) return;
+ timer->stop();
+ const auto guard = std::unique_ptr(
+ reply,
+ [](QNetworkReply *r) { r->deleteLater(); });
+ if (reply->error() != QNetworkReply::NoError) {
+ if (onFail) onFail();
+ return;
+ }
+ const auto body = reply->readAll();
+ bool ok = false;
+ const auto textOut = parseJsonPath(body, QStringLiteral("sentences"), &ok);
+ if (!ok) {
+ if (onFail) onFail();
+ return;
+ }
+ if (onSuccess) onSuccess(shouldWrapInHtml()
+ ? Html::htmlToEntities(textOut)
+ : TextWithEntities{textOut});
+ });
+
+ return reply;
+}
+
+}
\ No newline at end of file
diff --git a/Telegram/SourceFiles/ayu/features/translator/implementations/google.h b/Telegram/SourceFiles/ayu/features/translator/implementations/google.h
new file mode 100644
index 0000000000..18dda57bc2
--- /dev/null
+++ b/Telegram/SourceFiles/ayu/features/translator/implementations/google.h
@@ -0,0 +1,38 @@
+// This is the source code of AyuGram for Desktop.
+//
+// We do not and cannot prevent the use of our code,
+// but be respectful and credit the original author.
+//
+// Copyright @Radolyn, 2025
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#include "./base.h"
+
+namespace Ayu::Translator {
+
+class GoogleTranslator final : public MultiThreadTranslator
+{
+ Q_OBJECT
+
+public:
+ static GoogleTranslator &instance();
+
+ // all languages
+ [[nodiscard]] QSet supportedLanguages() const override { return {}; }
+
+ [[nodiscard]] QPointer startSingleTranslation(
+ const MultiThreadArgs &args
+ ) override;
+
+private:
+ explicit GoogleTranslator(QObject *parent = nullptr);
+
+ QNetworkAccessManager _nam;
+};
+
+}
\ No newline at end of file
diff --git a/Telegram/SourceFiles/ayu/features/translator/implementations/telegram.cpp b/Telegram/SourceFiles/ayu/features/translator/implementations/telegram.cpp
new file mode 100644
index 0000000000..85475fd343
--- /dev/null
+++ b/Telegram/SourceFiles/ayu/features/translator/implementations/telegram.cpp
@@ -0,0 +1,65 @@
+// This is the source code of AyuGram for Desktop.
+//
+// We do not and cannot prevent the use of our code,
+// but be respectful and credit the original author.
+//
+// Copyright @Radolyn, 2025
+#include "telegram.h"
+
+#include
+
+#include "api/api_text_entities.h"
+#include "main/main_session.h"
+
+namespace Ayu::Translator {
+
+TelegramTranslator &TelegramTranslator::instance() {
+ static TelegramTranslator inst;
+ return inst;
+}
+
+TelegramTranslator::TelegramTranslator(QObject *parent)
+ : BaseTranslator(parent) {
+}
+
+CallbackCancel TelegramTranslator::startTranslation(
+ const StartTranslationArgs &args
+) {
+ const auto requestId = args.session->api().request(MTPmessages_TranslateText(
+ args.requestData.flags,
+ args.requestData.peer,
+ args.requestData.idList,
+ args.requestData.text,
+ args.requestData.toLang
+ )).done([=](const MTPmessages_TranslatedText &result)
+ {
+ const auto &data = result.data();
+ const auto &list = data.vresult().v;
+ if (list.isEmpty()) {
+ args.onFail();
+ } else {
+ auto vec = std::vector();
+ vec.reserve(list.size());
+ for (const auto &item : list) {
+ const auto &d = item.data();
+ vec.push_back(TextWithEntities{
+ .text = qs(d.vtext()),
+ .entities = Api::EntitiesFromMTP(
+ args.session,
+ d.ventities().v)
+ });
+ }
+ args.onSuccess(vec);
+ }
+ }).fail([=](const MTP::Error &)
+ {
+ args.onFail();
+ }).send();
+
+ return [=]
+ {
+ args.session->api().request(requestId).cancel();
+ };
+}
+
+}
diff --git a/Telegram/SourceFiles/ayu/features/translator/implementations/telegram.h b/Telegram/SourceFiles/ayu/features/translator/implementations/telegram.h
new file mode 100644
index 0000000000..2adae73120
--- /dev/null
+++ b/Telegram/SourceFiles/ayu/features/translator/implementations/telegram.h
@@ -0,0 +1,34 @@
+// This is the source code of AyuGram for Desktop.
+//
+// We do not and cannot prevent the use of our code,
+// but be respectful and credit the original author.
+//
+// Copyright @Radolyn, 2025
+#pragma once
+
+#include
+#include
+#include
+
+#include "./base.h"
+
+namespace Ayu::Translator {
+
+class TelegramTranslator final : public BaseTranslator
+{
+ Q_OBJECT
+
+public:
+ static TelegramTranslator &instance();
+
+ [[nodiscard]] QSet supportedLanguages() const override { return {}; }
+
+ [[nodiscard]] CallbackCancel startTranslation(
+ const StartTranslationArgs &args
+ ) override;
+
+private:
+ explicit TelegramTranslator(QObject *parent = nullptr);
+};
+
+}
\ No newline at end of file
diff --git a/Telegram/SourceFiles/ayu/features/translator/implementations/yandex.cpp b/Telegram/SourceFiles/ayu/features/translator/implementations/yandex.cpp
new file mode 100644
index 0000000000..a43efa5279
--- /dev/null
+++ b/Telegram/SourceFiles/ayu/features/translator/implementations/yandex.cpp
@@ -0,0 +1,140 @@
+// This is the source code of AyuGram for Desktop.
+//
+// We do not and cannot prevent the use of our code,
+// but be respectful and credit the original author.
+//
+// Copyright @Radolyn, 2025
+#include "yandex.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "ayu/features/translator/html_parser.h"
+
+namespace Ayu::Translator {
+
+YandexTranslator &YandexTranslator::instance() {
+ static YandexTranslator inst;
+ return inst;
+}
+
+YandexTranslator::YandexTranslator(QObject *parent)
+ : MultiThreadTranslator(parent)
+ , _uuid(QUuid::createUuid().toString(QUuid::WithoutBraces).replace("-", "")) {
+}
+
+QSet YandexTranslator::supportedLanguages() const {
+ static const QSet languages = {
+ QStringLiteral("az"), QStringLiteral("sq"), QStringLiteral("am"), QStringLiteral("en"),
+ QStringLiteral("ar"), QStringLiteral("hy"), QStringLiteral("af"), QStringLiteral("eu"),
+ QStringLiteral("ba"), QStringLiteral("be"), QStringLiteral("bn"), QStringLiteral("my"),
+ QStringLiteral("bg"), QStringLiteral("bs"), QStringLiteral("cv"), QStringLiteral("cy"),
+ QStringLiteral("hu"), QStringLiteral("vi"), QStringLiteral("ht"), QStringLiteral("gl"),
+ QStringLiteral("nl"), QStringLiteral("mrj"), QStringLiteral("el"), QStringLiteral("ka"),
+ QStringLiteral("gu"), QStringLiteral("da"), QStringLiteral("he"), QStringLiteral("yi"),
+ QStringLiteral("id"), QStringLiteral("ga"), QStringLiteral("it"), QStringLiteral("is"),
+ QStringLiteral("es"), QStringLiteral("kk"), QStringLiteral("kn"), QStringLiteral("ca"),
+ QStringLiteral("ky"), QStringLiteral("zh"), QStringLiteral("ko"), QStringLiteral("xh"),
+ QStringLiteral("km"), QStringLiteral("lo"), QStringLiteral("la"), QStringLiteral("lv"),
+ QStringLiteral("lt"), QStringLiteral("lb"), QStringLiteral("mg"), QStringLiteral("ms"),
+ QStringLiteral("ml"), QStringLiteral("mt"), QStringLiteral("mk"), QStringLiteral("mi"),
+ QStringLiteral("mr"), QStringLiteral("mhr"), QStringLiteral("mn"), QStringLiteral("de"),
+ QStringLiteral("ne"), QStringLiteral("no"), QStringLiteral("pa"), QStringLiteral("pap"),
+ QStringLiteral("fa"), QStringLiteral("pl"), QStringLiteral("pt"), QStringLiteral("ro"),
+ QStringLiteral("ru"), QStringLiteral("ceb"), QStringLiteral("sr"), QStringLiteral("si"),
+ QStringLiteral("sk"), QStringLiteral("sl"), QStringLiteral("sw"), QStringLiteral("su"),
+ QStringLiteral("tg"), QStringLiteral("th"), QStringLiteral("tl"), QStringLiteral("ta"),
+ QStringLiteral("tt"), QStringLiteral("te"), QStringLiteral("tr"), QStringLiteral("udm"),
+ QStringLiteral("uz"), QStringLiteral("uk"), QStringLiteral("ur"), QStringLiteral("fi"),
+ QStringLiteral("fr"), QStringLiteral("hi"), QStringLiteral("hr"), QStringLiteral("cs"),
+ QStringLiteral("sv"), QStringLiteral("gd"), QStringLiteral("et"), QStringLiteral("eo"),
+ QStringLiteral("jv"), QStringLiteral("ja")
+ };
+ return languages;
+}
+
+QPointer YandexTranslator::startSingleTranslation(
+ const MultiThreadArgs &args
+) {
+ const auto &text = args.parsedData.text;
+ // const auto &fromLang = args.parsedData.fromLang;
+ const auto &toLang = args.parsedData.toLang;
+ const auto onSuccess = args.onSuccess;
+ const auto onFail = args.onFail;
+
+ if (text.empty() || toLang.isEmpty()) {
+ if (onFail) onFail();
+ return nullptr;
+ }
+
+ const auto to = toLang.trimmed();
+
+ QUrl url(QStringLiteral("https://translate.yandex.net/api/v1/tr.json/translate"));
+ QUrlQuery query;
+ query.addQueryItem(QStringLiteral("srv"), QStringLiteral("android"));
+ query.addQueryItem(QStringLiteral("id"), _uuid + QStringLiteral("-0-0"));
+ url.setQuery(query);
+
+ QNetworkRequest req(url);
+ req.setHeader(QNetworkRequest::UserAgentHeader,
+ QStringLiteral("ru.yandex.translate/21.15.4.21402814 (Xiaomi Redmi K20 Pro; Android 11)"));
+ req.setHeader(QNetworkRequest::ContentTypeHeader,
+ QStringLiteral("application/x-www-form-urlencoded"));
+
+ QUrlQuery postData;
+ postData.addQueryItem(QStringLiteral("lang"), to);
+ postData.addQueryItem(QStringLiteral("text"), shouldWrapInHtml() ? Html::entitiesToHtml(text) : text.text);
+ const auto postDataEncoded = postData.toString(QUrl::FullyEncoded).toUtf8();
+
+ QPointer reply = _nam.post(req, postDataEncoded);
+
+ auto timer = new QTimer(reply);
+ timer->setSingleShot(true);
+ timer->setInterval(15000);
+ QObject::connect(timer,
+ &QTimer::timeout,
+ reply,
+ [reply]
+ {
+ if (!reply) return;
+ if (reply->isRunning()) reply->abort();
+ });
+ timer->start();
+
+ QObject::connect(reply,
+ &QNetworkReply::finished,
+ [reply, onSuccess = onSuccess, onFail = onFail, timer]
+ {
+ if (!reply) return;
+ timer->stop();
+ const auto guard = std::unique_ptr(
+ reply,
+ [](QNetworkReply *r) { r->deleteLater(); });
+
+ if (reply->error() != QNetworkReply::NoError) {
+ if (onFail) onFail();
+ return;
+ }
+
+ const auto body = reply->readAll();
+ bool ok = false;
+ const auto translatedText = parseJsonPath(body, QStringLiteral("text"), &ok);
+ if (!ok) {
+ if (onFail) onFail();
+ return;
+ }
+ if (onSuccess) onSuccess(shouldWrapInHtml()
+ ? Html::htmlToEntities(translatedText)
+ : TextWithEntities{translatedText});
+ });
+
+ return reply;
+}
+
+}
\ No newline at end of file
diff --git a/Telegram/SourceFiles/ayu/features/translator/implementations/yandex.h b/Telegram/SourceFiles/ayu/features/translator/implementations/yandex.h
new file mode 100644
index 0000000000..1a3eeecc7f
--- /dev/null
+++ b/Telegram/SourceFiles/ayu/features/translator/implementations/yandex.h
@@ -0,0 +1,38 @@
+// This is the source code of AyuGram for Desktop.
+//
+// We do not and cannot prevent the use of our code,
+// but be respectful and credit the original author.
+//
+// Copyright @Radolyn, 2025
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#include "./base.h"
+
+namespace Ayu::Translator {
+
+class YandexTranslator final : public MultiThreadTranslator
+{
+ Q_OBJECT
+
+public:
+ static YandexTranslator &instance();
+
+ [[nodiscard]] QSet supportedLanguages() const override;
+
+ [[nodiscard]] QPointer startSingleTranslation(
+ const MultiThreadArgs &args
+ ) override;
+
+private:
+ explicit YandexTranslator(QObject *parent = nullptr);
+
+ QNetworkAccessManager _nam;
+ QString _uuid;
+};
+
+}
\ No newline at end of file
diff --git a/Telegram/SourceFiles/ayu/ui/ayu_styles.style b/Telegram/SourceFiles/ayu/ui/ayu_styles.style
index cf06340642..73970e0bfd 100644
--- a/Telegram/SourceFiles/ayu/ui/ayu_styles.style
+++ b/Telegram/SourceFiles/ayu/ui/ayu_styles.style
@@ -12,7 +12,7 @@ using "ui/colors.palette";
using "ui/widgets/widgets.style";
using "info/info.style";
-/* Color Picker */
+/* Icon Picker */
cpPadding: 14px;
cpSelectedPadding: 2px;
cpSelectedRounding: 12px;
@@ -69,3 +69,5 @@ exteraBadgeToast: Toast(defaultToast) {
icon: icon {{ "ayu/extera_badge", toastFg }};
iconPosition: point(13px, 13px);
}
+
+supportLogoSize: 96px;
diff --git a/Telegram/SourceFiles/ayu/ui/boxes/donate_info_box.cpp b/Telegram/SourceFiles/ayu/ui/boxes/donate_info_box.cpp
new file mode 100644
index 0000000000..fab0d56039
--- /dev/null
+++ b/Telegram/SourceFiles/ayu/ui/boxes/donate_info_box.cpp
@@ -0,0 +1,244 @@
+// This is the source code of AyuGram for Desktop.
+//
+// We do not and cannot prevent the use of our code,
+// but be respectful and credit the original author.
+//
+// Copyright @Radolyn, 2025
+#include "ayu/ui/boxes/donate_info_box.h"
+
+#include
+
+#include "lang_auto.h"
+#include "ayu/utils/rc_manager.h"
+#include "core/ui_integration.h"
+#include "data/data_session.h"
+#include "data/stickers/data_custom_emoji.h"
+#include "info/channel_statistics/earn/earn_icons.h"
+#include "lang/lang_text_entity.h"
+
+#include "info/profile/info_profile_icon.h"
+#include "main/main_session.h"
+#include "styles/style_ayu_styles.h"
+#include "styles/style_boxes.h"
+#include "styles/style_channel_earn.h"
+#include "styles/style_giveaway.h"
+#include "styles/style_layers.h"
+#include "styles/style_menu_icons.h"
+#include "styles/style_premium.h"
+#include "styles/style_settings.h"
+#include "styles/style_widgets.h"
+#include "ui/painter.h"
+#include "ui/rect.h"
+#include "ui/rp_widget.h"
+#include "ui/vertical_list.h"
+#include "ui/layers/generic_box.h"
+#include "ui/text/text_utilities.h"
+#include "ui/widgets/buttons.h"
+#include "ui/widgets/labels.h"
+#include "window/window_session_controller.h"
+
+namespace Ui {
+namespace {
+
+QImage MakeSupportLogo() {
+ const auto s = Size(st::supportLogoSize);
+ auto svg = QSvgRenderer(QString(":/gui/icons/ayu/donates/support_logo.svg"));
+ auto image = QImage(
+ s * style::DevicePixelRatio(),
+ QImage::Format_ARGB32_Premultiplied);
+ image.setDevicePixelRatio(style::DevicePixelRatio());
+ image.fill(Qt::transparent);
+ {
+ auto p = QPainter(&image);
+ svg.render(&p, Rect(s));
+ }
+ return image;
+}
+
+object_ptr CreateTopLogoWidget(
+ not_null parent) {
+ auto w = object_ptr(parent);
+ const auto raw = w.data();
+
+ const auto logo = MakeSupportLogo();
+
+ raw->paintRequest(
+ ) | rpl::start_with_next(
+ [=](QRect)
+ {
+ QPainter p(raw);
+ PainterHighQualityEnabler hq(p);
+
+ const auto original = logo.size() / style::DevicePixelRatio();
+ const auto maxWidth = raw->width() - rect::m::sum::h(st::boxRowPadding);
+ const auto maxHeight = raw->height();
+ if (original.isEmpty() || maxWidth <= 0 || maxHeight <= 0) {
+ return;
+ }
+ const auto scale = std::min(
+ double(maxWidth) / double(original.width()),
+ double(maxHeight) / double(original.height()));
+ const auto target = QSize(
+ int(original.width() * scale),
+ int(original.height() * scale));
+ const auto x = (raw->width() - target.width()) / 2;
+ const auto y = (raw->height() - target.height()) / 2;
+ const auto rect = QRect(QPoint(x, y), target);
+ p.drawImage(rect, logo);
+ },
+ raw->lifetime());
+
+ return w;
+}
+
+object_ptr InfoRow(
+ not_null parent,
+ not_null session,
+ const QString &title,
+ const TextWithEntities &text,
+ not_null icon) {
+ auto row = object_ptr(parent);
+ const auto raw = row.data();
+
+ raw->add(
+ object_ptr(
+ raw,
+ rpl::single(title) | Ui::Text::ToBold(),
+ st::defaultFlatLabel),
+ st::settingsPremiumRowTitlePadding);
+
+ const auto label = raw->add(
+ object_ptr(
+ raw,
+ st::boxDividerLabel),
+ st::settingsPremiumRowAboutPadding);
+
+ label->setMarkedText(
+ text,
+ Core::TextContext({
+ .session = std::move(session),
+ })
+ );
+
+ object_ptr(
+ raw,
+ *icon,
+ st::starrefInfoIconPosition);
+
+ return row;
+}
+
+} // namespace
+
+void FillDonateInfoBox(not_null box, not_null controller) {
+ // box->setStyle(st::starrefFooterBox);
+ box->setStyle(st::giveawayGiftCodeBox);
+ box->setNoContentMargin(true);
+ box->setWidth(int(st::aboutWidth * 1.1));
+ box->verticalLayout()->resizeToWidth(box->width());
+
+ box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); });
+
+ Ui::AddSkip(box->verticalLayout());
+ Ui::AddSkip(box->verticalLayout());
+
+ const auto logoWidget = box->verticalLayout()->add(
+ CreateTopLogoWidget(box->verticalLayout()));
+
+ logoWidget->resize(st::supportLogoSize, st::supportLogoSize);
+
+ Ui::AddSkip(box->verticalLayout());
+
+ box->verticalLayout()->add(
+ object_ptr>(
+ box,
+ object_ptr(
+ box->verticalLayout(),
+ tr::ayu_SupportBoxHeader()
+ | Ui::Text::ToBold(),
+ st::boxTitle)),
+ st::boxRowPadding);
+
+ box->verticalLayout()->add(
+ object_ptr(
+ box->verticalLayout(),
+ tr::ayu_SupportBoxInfo(),
+ st::starrefCenteredText),
+ st::boxRowPadding);
+
+ Ui::AddSkip(box->verticalLayout());
+ Ui::AddSkip(box->verticalLayout());
+ Ui::AddSkip(box->verticalLayout());
+
+ const auto tonSymbol = Ui::Text::SingleCustomEmoji(
+ controller->session().data().customEmojiManager().registerInternalEmoji(
+ Ui::Earn::IconCurrencyColored(
+ st::boxDividerLabel.style.font,
+ st::boxDividerLabel.textFg->c),
+ st::channelEarnCurrencyLearnMargins));
+
+ const auto dollarAmount = RCManager::getInstance().donateAmountUsd().prepend("$");
+ const auto tonAmount = RCManager::getInstance().donateAmountTon();
+ const auto rubleAmount = RCManager::getInstance().donateAmountRub().append("₽");
+
+ const auto innerText = TextWithEntities{}.append(tonSymbol).append(tonAmount).append(", ").append(rubleAmount);
+ const auto str = tr::ayu_SupportBoxMakeDonationInfo(
+ tr::now,
+ lt_amount1,
+ TextWithEntities{dollarAmount},
+ lt_amount2,
+ innerText,
+ Ui::Text::RichLangValue
+ );
+
+ box->verticalLayout()->add(InfoRow(
+ box->verticalLayout(),
+ &controller->session(),
+ tr::ayu_SupportBoxMakeDonationHeader(tr::now),
+ str,
+ &st::menuIconEarn));
+
+ Ui::AddSkip(box->verticalLayout());
+
+ const auto username = RCManager::getInstance().donateUsername();
+ auto usernameTrimmed = username;
+ if (usernameTrimmed.startsWith('@')) {
+ usernameTrimmed.remove(0, 1);
+ }
+ const TextWithEntities proofText = tr::ayu_SupportBoxSendProofInfo(
+ tr::now,
+ lt_item,
+ Ui::Text::Link(username, controller->session().createInternalLinkFull(usernameTrimmed)),
+ Ui::Text::RichLangValue);
+ box->verticalLayout()->add(InfoRow(
+ box->verticalLayout(),
+ &controller->session(),
+ tr::ayu_SupportBoxSendProofHeader(tr::now),
+ proofText,
+ &st::menuIconPhoto));
+
+ Ui::AddSkip(box->verticalLayout());
+
+ box->verticalLayout()->add(InfoRow(
+ box->verticalLayout(),
+ &controller->session(),
+ tr::ayu_SupportBoxReceiveBadgeHeader(tr::now),
+ TextWithEntities{
+ tr::ayu_SupportBoxReceiveBadgeInfo(tr::now)
+ },
+ &st::menuIconStarRefShare));
+
+ const auto closeButton = box->addButton(tr::lng_close(), [=] { box->closeBox(); });
+ const auto buttonWidth = box->width()
+ - rect::m::sum::h(st::starrefFooterBox.buttonPadding);
+ closeButton->widthValue() | rpl::filter([=]
+ {
+ return (closeButton->widthNoMargins() != buttonWidth);
+ }) | rpl::start_with_next([=]
+ {
+ closeButton->resizeToWidth(buttonWidth);
+ },
+ closeButton->lifetime());
+}
+
+} // namespace Ui
diff --git a/Telegram/SourceFiles/ayu/ui/boxes/donate_info_box.h b/Telegram/SourceFiles/ayu/ui/boxes/donate_info_box.h
new file mode 100644
index 0000000000..076a59464e
--- /dev/null
+++ b/Telegram/SourceFiles/ayu/ui/boxes/donate_info_box.h
@@ -0,0 +1,19 @@
+// This is the source code of AyuGram for Desktop.
+//
+// We do not and cannot prevent the use of our code,
+// but be respectful and credit the original author.
+//
+// Copyright @Radolyn, 2025
+#pragma once
+
+namespace Window {
+class SessionController;
+}
+
+namespace Ui {
+
+class GenericBox;
+
+void FillDonateInfoBox(not_null box, not_null controller);
+
+} // namespace Ui
diff --git a/Telegram/SourceFiles/ayu/ui/boxes/donate_qr_box.cpp b/Telegram/SourceFiles/ayu/ui/boxes/donate_qr_box.cpp
new file mode 100644
index 0000000000..d7823ffa47
--- /dev/null
+++ b/Telegram/SourceFiles/ayu/ui/boxes/donate_qr_box.cpp
@@ -0,0 +1,164 @@
+// This is the source code of AyuGram for Desktop.
+//
+// We do not and cannot prevent the use of our code,
+// but be respectful and credit the original author.
+//
+// Copyright @Radolyn, 2025
+#include "ayu/ui/boxes/donate_qr_box.h"
+
+#include "qr/qr_generate.h"
+#include "styles/style_boxes.h"
+#include "styles/style_intro.h"
+#include "styles/style_layers.h"
+#include "ui/painter.h"
+#include "ui/rect.h"
+#include "ui/rp_widget.h"
+#include "ui/vertical_list.h"
+#include "ui/controls/invite_link_label.h"
+#include "ui/layers/generic_box.h"
+#include "ui/widgets/buttons.h"
+
+#include
+#include
+#include
+
+#include "lang_auto.h"
+#include "styles/style_giveaway.h"
+#include "ui/toast/toast.h"
+
+namespace Ui {
+namespace {
+
+[[nodiscard]] QImage MakeQrWithIcon(
+ const QByteArray &payload,
+ const QString &iconPath,
+ int pixel,
+ int max) {
+ Expects(!payload.isEmpty());
+
+ const auto data = Qr::Encode(payload, Qr::Redundancy::Default);
+ Expects(data.size > 0);
+ if (max > 0 && data.size * pixel > max) {
+ pixel = std::max(max / data.size, 1);
+ }
+
+ auto qr = Qr::Generate(
+ data,
+ pixel * style::DevicePixelRatio(),
+ Qt::black,
+ Qt::white);
+
+ {
+ QPainter p(&qr);
+ PainterHighQualityEnabler hq(p);
+ const auto size = qr.rect().size();
+ constexpr auto kCenterRatio = 0.20;
+ const auto centerRect = Rect(size)
+ - Margins((size.width() - (size.width() * kCenterRatio)) / 2);
+ p.setPen(Qt::NoPen);
+ p.setBrush(Qt::white);
+ p.drawEllipse(centerRect);
+
+ QSvgRenderer svg(iconPath);
+ if (svg.isValid()) {
+ svg.render(&p, centerRect);
+ }
+ }
+ return qr;
+}
+
+} // namespace
+
+void FillDonateQrBox(
+ not_null box,
+ const QString &address,
+ const QString &iconResourcePath) {
+ box->setStyle(st::giveawayGiftCodeBox);
+ box->setNoContentMargin(true);
+ box->setWidth(int(st::aboutWidth * 1.25));
+ box->setTitle(tr::lng_group_invite_context_qr());
+ box->verticalLayout()->resizeToWidth(box->width());
+
+ Ui::AddSkip(box->verticalLayout());
+ Ui::AddSkip(box->verticalLayout());
+
+ const auto qrWidget = box->verticalLayout()->add(
+ object_ptr(box->verticalLayout()));
+
+ struct State {
+ QImage qrImage;
+ int qrMaxSize = 0;
+ };
+ const auto state = qrWidget->lifetime().make_state();
+
+ const auto recompute = [=] {
+ const auto qrMaxSize = int(st::aboutWidth * 1.25) - st::boxRowPadding.left() - st::boxRowPadding.right();
+ state->qrMaxSize = qrMaxSize;
+
+ const auto remainder = qrMaxSize % st::introQrPixel;
+ const auto downTo = remainder ? (qrMaxSize - remainder) : qrMaxSize;
+ state->qrImage = MakeQrWithIcon(
+ address.toUtf8(),
+ iconResourcePath,
+ st::introQrPixel,
+ downTo).scaled(
+ Size(qrMaxSize * style::DevicePixelRatio()),
+ Qt::IgnoreAspectRatio,
+ Qt::SmoothTransformation);
+
+ qrWidget->resize(
+ state->qrMaxSize,
+ state->qrMaxSize);
+ };
+
+ recompute();
+
+ qrWidget->paintRequest(
+ ) | rpl::start_with_next([=](QRect) {
+ QPainter p(qrWidget);
+ PainterHighQualityEnabler hq(p);
+
+ const auto size = state->qrImage.size() / style::DevicePixelRatio();
+ const auto rect = Rect(
+ (qrWidget->width() - size.width()) / 2,
+ (qrWidget->height() - size.height()) / 2,
+ size);
+
+ QPainterPath path;
+ path.addRoundedRect(rect, st::roundRadiusLarge, st::roundRadiusLarge);
+ p.setPen(Qt::NoPen);
+ p.setBrush(Qt::white);
+ p.drawPath(path);
+ p.setClipPath(path);
+
+ const auto padding = st::boxRowPadding.left();
+ const auto innerRect = rect - Margins(padding);
+
+ p.drawImage(innerRect, state->qrImage);
+ }, qrWidget->lifetime());
+
+ Ui::AddSkip(box->verticalLayout());
+ Ui::AddSkip(box->verticalLayout());
+
+ const auto label = box->lifetime().make_state(
+ box->verticalLayout(),
+ rpl::single(address),
+ Fn()>());
+ box->verticalLayout()->add(label->take(), st::boxRowPadding);
+
+ const auto copyButton = box->addButton(tr::lng_chat_link_copy(), [=] {
+ QGuiApplication::clipboard()->setText(address);
+ Ui::Toast::Show(tr::lng_text_copied(tr::now));
+ });
+ const auto buttonWidth = box->width()
+ - rect::m::sum::h(st::giveawayGiftCodeBox.buttonPadding);
+ copyButton->widthValue() | rpl::filter([=] {
+ return (copyButton->widthNoMargins() != buttonWidth);
+ }) | rpl::start_with_next([=] {
+ copyButton->resizeToWidth(buttonWidth);
+ }, copyButton->lifetime());
+
+ box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); });
+}
+
+} // namespace Ui
diff --git a/Telegram/SourceFiles/ayu/ui/boxes/donate_qr_box.h b/Telegram/SourceFiles/ayu/ui/boxes/donate_qr_box.h
new file mode 100644
index 0000000000..75e4ece9ee
--- /dev/null
+++ b/Telegram/SourceFiles/ayu/ui/boxes/donate_qr_box.h
@@ -0,0 +1,18 @@
+// This is the source code of AyuGram for Desktop.
+//
+// We do not and cannot prevent the use of our code,
+// but be respectful and credit the original author.
+//
+// Copyright @Radolyn, 2025
+#pragma once
+
+namespace Ui {
+
+class GenericBox;
+
+void FillDonateQrBox(
+ not_null box,
+ const QString &address,
+ const QString &iconResourcePath);
+
+} // namespace Ui
diff --git a/Telegram/SourceFiles/ayu/ui/components/icon_picker.cpp b/Telegram/SourceFiles/ayu/ui/components/icon_picker.cpp
index ce89cd7b85..e3e5246c8b 100644
--- a/Telegram/SourceFiles/ayu/ui/components/icon_picker.cpp
+++ b/Telegram/SourceFiles/ayu/ui/components/icon_picker.cpp
@@ -58,7 +58,7 @@ void drawIcon(QPainter &p, const QImage &icon, int xOffset, int yOffset, float s
);
p.restore();
- auto rect = QRect(
+ const auto rect = QRect(
xOffset + st::cpImagePadding,
yOffset + st::cpImagePadding,
st::cpIconSize,
@@ -87,6 +87,7 @@ IconPicker::IconPicker(QWidget *parent)
IconPicker::~IconPicker() {
cachedIcons.clear();
}
+
void IconPicker::paintEvent(QPaintEvent *e) {
Painter p(this);
PainterHighQualityEnabler hq(p);
diff --git a/Telegram/SourceFiles/ayu/ui/settings/ayu_settings.style b/Telegram/SourceFiles/ayu/ui/settings/ayu_settings.style
new file mode 100644
index 0000000000..8d927a360f
--- /dev/null
+++ b/Telegram/SourceFiles/ayu/ui/settings/ayu_settings.style
@@ -0,0 +1,23 @@
+/*
+ * This is the source code of AyuGram for Desktop.
+ *
+ * We do not and cannot prevent the use of our code,
+ * but be respectful and credit the original author.
+ *
+ * Copyright @Radolyn, 2025
+ */
+
+using "ui/basic.style";
+using "ui/layers/layers.style";
+using "ui/widgets/widgets.style";
+
+centeredBoxLabelStyle: TextStyle(boxLabelStyle) {
+ font: font(13px);
+ lineHeight: 0px;
+}
+
+centeredBoxLabel: FlatLabel(defaultFlatLabel) {
+ minWidth: 256px;
+ align: align(center);
+ style: centeredBoxLabelStyle;
+}
diff --git a/Telegram/SourceFiles/ayu/ui/settings/settings_appearance.cpp b/Telegram/SourceFiles/ayu/ui/settings/settings_appearance.cpp
new file mode 100644
index 0000000000..9d98041608
--- /dev/null
+++ b/Telegram/SourceFiles/ayu/ui/settings/settings_appearance.cpp
@@ -0,0 +1,543 @@
+// This is the source code of AyuGram for Desktop.
+//
+// We do not and cannot prevent the use of our code,
+// but be respectful and credit the original author.
+//
+// Copyright @Radolyn, 2025
+#include "settings_appearance.h"
+
+#include "lang_auto.h"
+#include "ayu/ayu_settings.h"
+#include "ayu/ui/boxes/font_selector.h"
+#include "ayu/ui/components/icon_picker.h"
+#include "inline_bots/bot_attach_web_view.h"
+#include "main/main_session.h"
+#include "settings/settings_common.h"
+#include "styles/style_ayu_icons.h"
+#include "styles/style_menu_icons.h"
+#include "styles/style_settings.h"
+#include "ui/vertical_list.h"
+#include "ui/widgets/buttons.h"
+#include "ui/wrap/vertical_layout.h"
+#include "window/window_session_controller.h"
+
+namespace Settings {
+
+namespace {
+
+bool HasDrawerBots(not_null controller) {
+ // todo: maybe iterate through all accounts
+ const auto bots = &controller->session().attachWebView();
+ for (const auto &bot : bots->attachBots()) {
+ if (!bot.inMainMenu || !bot.media) {
+ continue;
+ }
+
+ return true;
+ }
+
+ return false;
+}
+
+}
+
+rpl::producer AyuAppearance::title() {
+ return tr::ayu_CategoryAppearance();
+}
+
+AyuAppearance::AyuAppearance(
+ QWidget *parent,
+ not_null controller)
+ : Section(parent) {
+ setupContent(controller);
+}
+
+void SetupAppIcon(not_null container) {
+ AddSubsectionTitle(container, tr::ayu_AppIconHeader());
+ container->add(
+ object_ptr(container),
+ st::settingsCheckboxPadding);
+
+#ifdef Q_OS_WIN
+ auto *settings = &AyuSettings::getInstance();
+
+ AddDivider(container);
+ AddSkip(container);
+ AddButtonWithIcon(
+ container,
+ tr::ayu_HideNotificationBadge(),
+ st::settingsButtonNoIcon
+ )->toggleOn(
+ rpl::single(settings->hideNotificationBadge)
+ )->toggledValue(
+ ) | rpl::filter(
+ [=](bool enabled)
+ {
+ return (enabled != settings->hideNotificationBadge);
+ }) | rpl::start_with_next(
+ [=](bool enabled)
+ {
+ AyuSettings::set_hideNotificationBadge(enabled);
+ AyuSettings::save();
+ },
+ container->lifetime());
+ AddSkip(container);
+ AddDividerText(container, tr::ayu_HideNotificationBadgeDescription());
+ AddSkip(container);
+#endif
+}
+
+void SetupAppearance(not_null container, not_null controller) {
+ auto *settings = &AyuSettings::getInstance();
+
+ AddSubsectionTitle(container, tr::ayu_CategoryAppearance());
+
+ AddButtonWithIcon(
+ container,
+ tr::ayu_MaterialSwitches(),
+ st::settingsButtonNoIcon
+ )->toggleOn(
+ rpl::single(settings->materialSwitches)
+ )->toggledValue(
+ ) | rpl::filter(
+ [=](bool enabled)
+ {
+ return (enabled != settings->materialSwitches);
+ }) | rpl::start_with_next(
+ [=](bool enabled)
+ {
+ AyuSettings::set_materialSwitches(enabled);
+ AyuSettings::save();
+ },
+ container->lifetime());
+
+ AddButtonWithIcon(
+ container,
+ tr::ayu_RemoveMessageTail(),
+ st::settingsButtonNoIcon
+ )->toggleOn(
+ rpl::single(settings->removeMessageTail)
+ )->toggledValue(
+ ) | rpl::filter(
+ [=](bool enabled)
+ {
+ return (enabled != settings->removeMessageTail);
+ }) | start_with_next(
+ [=](bool enabled)
+ {
+ AyuSettings::set_removeMessageTail(enabled);
+ AyuSettings::save();
+ },
+ container->lifetime());
+
+ AddButtonWithIcon(
+ container,
+ tr::ayu_DisableCustomBackgrounds(),
+ st::settingsButtonNoIcon
+ )->toggleOn(
+ rpl::single(settings->disableCustomBackgrounds)
+ )->toggledValue(
+ ) | rpl::filter(
+ [=](bool enabled)
+ {
+ return (enabled != settings->disableCustomBackgrounds);
+ }) | rpl::start_with_next(
+ [=](bool enabled)
+ {
+ AyuSettings::set_disableCustomBackgrounds(enabled);
+ AyuSettings::save();
+ },
+ container->lifetime());
+
+ const auto monoButton = AddButtonWithLabel(
+ container,
+ tr::ayu_MonospaceFont(),
+ rpl::single(
+ settings->monoFont.isEmpty() ? tr::ayu_FontDefault(tr::now) : settings->monoFont
+ ),
+ st::settingsButtonNoIcon);
+ const auto monoGuard = Ui::CreateChild(monoButton.get());
+
+ monoButton->addClickHandler(
+ [=]
+ {
+ *monoGuard = AyuUi::FontSelectorBox::Show(
+ controller,
+ [=](QString font)
+ {
+ AyuSettings::set_monoFont(std::move(font));
+ AyuSettings::save();
+ });
+ });
+
+ AddSkip(container);
+ AddDivider(container);
+ AddSkip(container);
+}
+
+void SetupChatFolders(not_null container) {
+ auto *settings = &AyuSettings::getInstance();
+
+ AddSubsectionTitle(container, tr::ayu_ChatFoldersHeader());
+
+ AddButtonWithIcon(
+ container,
+ tr::ayu_HideNotificationCounters(),
+ st::settingsButtonNoIcon
+ )->toggleOn(
+ rpl::single(settings->hideNotificationCounters)
+ )->toggledValue(
+ ) | rpl::filter(
+ [=](bool enabled)
+ {
+ return (enabled != settings->hideNotificationCounters);
+ }) | rpl::start_with_next(
+ [=](bool enabled)
+ {
+ AyuSettings::set_hideNotificationCounters(enabled);
+ AyuSettings::save();
+ },
+ container->lifetime());
+
+ AddButtonWithIcon(
+ container,
+ tr::ayu_HideAllChats(),
+ st::settingsButtonNoIcon
+ )->toggleOn(
+ rpl::single(settings->hideAllChatsFolder)
+ )->toggledValue(
+ ) | rpl::filter(
+ [=](bool enabled)
+ {
+ return (enabled != settings->hideAllChatsFolder);
+ }) | rpl::start_with_next(
+ [=](bool enabled)
+ {
+ AyuSettings::set_hideAllChatsFolder(enabled);
+ AyuSettings::save();
+ },
+ container->lifetime());
+
+ AddSkip(container);
+ AddDivider(container);
+ AddSkip(container);
+}
+
+void SetupDrawerElements(not_null container, not_null controller) {
+ auto *settings = &AyuSettings::getInstance();
+
+ AddSubsectionTitle(container, tr::ayu_DrawerElementsHeader());
+
+ AddButtonWithIcon(
+ container,
+ tr::lng_menu_my_profile(),
+ st::settingsButton,
+ {&st::menuIconProfile}
+ )->toggleOn(
+ rpl::single(settings->showMyProfileInDrawer)
+ )->toggledValue(
+ ) | rpl::filter(
+ [=](bool enabled)
+ {
+ return (enabled != settings->showMyProfileInDrawer);
+ }) | rpl::start_with_next(
+ [=](bool enabled)
+ {
+ AyuSettings::set_showMyProfileInDrawer(enabled);
+ AyuSettings::save();
+ },
+ container->lifetime());
+
+ if (HasDrawerBots(controller)) {
+ AddButtonWithIcon(
+ container,
+ tr::lng_filters_type_bots(),
+ st::settingsButton,
+ {&st::menuIconBot}
+ )->toggleOn(
+ rpl::single(settings->showBotsInDrawer)
+ )->toggledValue(
+ ) | rpl::filter(
+ [=](bool enabled)
+ {
+ return (enabled != settings->showBotsInDrawer);
+ }) | rpl::start_with_next(
+ [=](bool enabled)
+ {
+ AyuSettings::set_showBotsInDrawer(enabled);
+ AyuSettings::save();
+ },
+ container->lifetime());
+ }
+
+ AddButtonWithIcon(
+ container,
+ tr::lng_create_group_title(),
+ st::settingsButton,
+ {&st::menuIconGroups}
+ )->toggleOn(
+ rpl::single(settings->showNewGroupInDrawer)
+ )->toggledValue(
+ ) | rpl::filter(
+ [=](bool enabled)
+ {
+ return (enabled != settings->showNewGroupInDrawer);
+ }) | rpl::start_with_next(
+ [=](bool enabled)
+ {
+ AyuSettings::set_showNewGroupInDrawer(enabled);
+ AyuSettings::save();
+ },
+ container->lifetime());
+
+ AddButtonWithIcon(
+ container,
+ tr::lng_create_channel_title(),
+ st::settingsButton,
+ {&st::menuIconChannel}
+ )->toggleOn(
+ rpl::single(settings->showNewChannelInDrawer)
+ )->toggledValue(
+ ) | rpl::filter(
+ [=](bool enabled)
+ {
+ return (enabled != settings->showNewChannelInDrawer);
+ }) | rpl::start_with_next(
+ [=](bool enabled)
+ {
+ AyuSettings::set_showNewChannelInDrawer(enabled);
+ AyuSettings::save();
+ },
+ container->lifetime());
+
+ AddButtonWithIcon(
+ container,
+ tr::lng_menu_contacts(),
+ st::settingsButton,
+ {&st::menuIconUserShow}
+ )->toggleOn(
+ rpl::single(settings->showContactsInDrawer)
+ )->toggledValue(
+ ) | rpl::filter(
+ [=](bool enabled)
+ {
+ return (enabled != settings->showContactsInDrawer);
+ }) | rpl::start_with_next(
+ [=](bool enabled)
+ {
+ AyuSettings::set_showContactsInDrawer(enabled);
+ AyuSettings::save();
+ },
+ container->lifetime());
+
+ AddButtonWithIcon(
+ container,
+ tr::lng_menu_calls(),
+ st::settingsButton,
+ {&st::menuIconPhone}
+ )->toggleOn(
+ rpl::single(settings->showCallsInDrawer)
+ )->toggledValue(
+ ) | rpl::filter(
+ [=](bool enabled)
+ {
+ return (enabled != settings->showCallsInDrawer);
+ }) | rpl::start_with_next(
+ [=](bool enabled)
+ {
+ AyuSettings::set_showCallsInDrawer(enabled);
+ AyuSettings::save();
+ },
+ container->lifetime());
+
+ AddButtonWithIcon(
+ container,
+ tr::lng_saved_messages(),
+ st::settingsButton,
+ {&st::menuIconSavedMessages}
+ )->toggleOn(
+ rpl::single(settings->showSavedMessagesInDrawer)
+ )->toggledValue(
+ ) | rpl::filter(
+ [=](bool enabled)
+ {
+ return (enabled != settings->showSavedMessagesInDrawer);
+ }) | rpl::start_with_next(
+ [=](bool enabled)
+ {
+ AyuSettings::set_showSavedMessagesInDrawer(enabled);
+ AyuSettings::save();
+ },
+ container->lifetime());
+
+ AddButtonWithIcon(
+ container,
+ tr::ayu_LReadMessages(),
+ st::settingsButton,
+ {&st::ayuLReadMenuIcon}
+ )->toggleOn(
+ rpl::single(settings->showLReadToggleInDrawer)
+ )->toggledValue(
+ ) | rpl::filter(
+ [=](bool enabled)
+ {
+ return (enabled != settings->showLReadToggleInDrawer);
+ }) | rpl::start_with_next(
+ [=](bool enabled)
+ {
+ AyuSettings::set_showLReadToggleInDrawer(enabled);
+ AyuSettings::save();
+ },
+ container->lifetime());
+
+ AddButtonWithIcon(
+ container,
+ tr::ayu_SReadMessages(),
+ st::settingsButton,
+ {&st::ayuSReadMenuIcon}
+ )->toggleOn(
+ rpl::single(settings->showSReadToggleInDrawer)
+ )->toggledValue(
+ ) | rpl::filter(
+ [=](bool enabled)
+ {
+ return (enabled != settings->showSReadToggleInDrawer);
+ }) | rpl::start_with_next(
+ [=](bool enabled)
+ {
+ AyuSettings::set_showSReadToggleInDrawer(enabled);
+ AyuSettings::save();
+ },
+ container->lifetime());
+
+ AddButtonWithIcon(
+ container,
+ tr::lng_menu_night_mode(),
+ st::settingsButton,
+ {&st::menuIconNightMode}
+ )->toggleOn(
+ rpl::single(settings->showNightModeToggleInDrawer)
+ )->toggledValue(
+ ) | rpl::filter(
+ [=](bool enabled)
+ {
+ return (enabled != settings->showNightModeToggleInDrawer);
+ }) | rpl::start_with_next(
+ [=](bool enabled)
+ {
+ AyuSettings::set_showNightModeToggleInDrawer(enabled);
+ AyuSettings::save();
+ },
+ container->lifetime());
+
+ AddButtonWithIcon(
+ container,
+ tr::ayu_GhostModeToggle(),
+ st::settingsButton,
+ {&st::ayuGhostIcon}
+ )->toggleOn(
+ rpl::single(settings->showGhostToggleInDrawer)
+ )->toggledValue(
+ ) | rpl::filter(
+ [=](bool enabled)
+ {
+ return (enabled != settings->showGhostToggleInDrawer);
+ }) | rpl::start_with_next(
+ [=](bool enabled)
+ {
+ AyuSettings::set_showGhostToggleInDrawer(enabled);
+ AyuSettings::save();
+ },
+ container->lifetime());
+
+#ifdef WIN32
+ AddButtonWithIcon(
+ container,
+ tr::ayu_StreamerModeToggle(),
+ st::settingsButton,
+ {&st::ayuStreamerModeMenuIcon}
+ )->toggleOn(
+ rpl::single(settings->showStreamerToggleInDrawer)
+ )->toggledValue(
+ ) | rpl::filter(
+ [=](bool enabled)
+ {
+ return (enabled != settings->showStreamerToggleInDrawer);
+ }) | rpl::start_with_next(
+ [=](bool enabled)
+ {
+ AyuSettings::set_showStreamerToggleInDrawer(enabled);
+ AyuSettings::save();
+ },
+ container->lifetime());
+#endif
+
+ AddSkip(container);
+}
+
+void SetupTrayElements(not_null container) {
+ auto *settings = &AyuSettings::getInstance();
+
+ AddSubsectionTitle(container, tr::ayu_TrayElementsHeader());
+
+ AddButtonWithIcon(
+ container,
+ tr::ayu_EnableGhostModeTray(),
+ st::settingsButtonNoIcon
+ )->toggleOn(
+ rpl::single(settings->showGhostToggleInTray)
+ )->toggledValue(
+ ) | rpl::filter(
+ [=](bool enabled)
+ {
+ return (enabled != settings->showGhostToggleInTray);
+ }) | rpl::start_with_next(
+ [=](bool enabled)
+ {
+ AyuSettings::set_showGhostToggleInTray(enabled);
+ AyuSettings::save();
+ },
+ container->lifetime());
+
+#ifdef WIN32
+ AddButtonWithIcon(
+ container,
+ tr::ayu_EnableStreamerModeTray(),
+ st::settingsButtonNoIcon
+ )->toggleOn(
+ rpl::single(settings->showStreamerToggleInTray)
+ )->toggledValue(
+ ) | rpl::filter(
+ [=](bool enabled)
+ {
+ return (enabled != settings->showStreamerToggleInTray);
+ }) | rpl::start_with_next(
+ [=](bool enabled)
+ {
+ AyuSettings::set_showStreamerToggleInTray(enabled);
+ AyuSettings::save();
+ },
+ container->lifetime());
+#endif
+
+ AddSkip(container);
+ AddDivider(container);
+ AddSkip(container);
+}
+
+void AyuAppearance::setupContent(not_null controller) {
+ const auto content = Ui::CreateChild(this);
+
+ AddSkip(content);
+
+ SetupAppIcon(content);
+ SetupAppearance(content, controller);
+ SetupChatFolders(content);
+ SetupTrayElements(content);
+ SetupDrawerElements(content, controller);
+ AddSkip(content);
+
+ ResizeFitChild(this, content);
+}
+
+} // namespace Settings
\ No newline at end of file
diff --git a/Telegram/SourceFiles/ayu/ui/settings/settings_appearance.h b/Telegram/SourceFiles/ayu/ui/settings/settings_appearance.h
new file mode 100644
index 0000000000..c00ac57cfc
--- /dev/null
+++ b/Telegram/SourceFiles/ayu/ui/settings/settings_appearance.h
@@ -0,0 +1,29 @@
+// This is the source code of AyuGram for Desktop.
+//
+// We do not and cannot prevent the use of our code,
+// but be respectful and credit the original author.
+//
+// Copyright @Radolyn, 2025
+#pragma once
+
+#include "settings/settings_common.h"
+#include "settings/settings_common_session.h"
+
+namespace Window {
+class SessionController;
+} // namespace Window
+
+namespace Settings {
+
+class AyuAppearance : public Section
+{
+public:
+ AyuAppearance(QWidget *parent, not_null controller);
+
+ [[nodiscard]] rpl::producer title() override;
+
+private:
+ void setupContent(not_null controller);
+};
+
+} // namespace Settings
\ No newline at end of file
diff --git a/Telegram/SourceFiles/ayu/ui/settings/settings_ayu.cpp b/Telegram/SourceFiles/ayu/ui/settings/settings_ayu.cpp
index e6c05e1ffb..87f40e1c12 100644
--- a/Telegram/SourceFiles/ayu/ui/settings/settings_ayu.cpp
+++ b/Telegram/SourceFiles/ayu/ui/settings/settings_ayu.cpp
@@ -6,455 +6,24 @@
// Copyright @Radolyn, 2025
#include "settings_ayu.h"
-#include "ayu/ayu_settings.h"
-#include "ayu/ui/boxes/edit_mark_box.h"
-#include "ayu/ui/boxes/font_selector.h"
-
#include "lang_auto.h"
-
-#include "boxes/connection_box.h"
-#include "data/data_session.h"
-#include "main/main_session.h"
+#include "settings_ayu_utils.h"
+#include "ayu/ayu_settings.h"
#include "settings/settings_common.h"
-#include "storage/localstorage.h"
-#include "styles/style_chat_helpers.h"
-#include "styles/style_ayu_styles.h"
-#include "styles/style_basic.h"
-#include "styles/style_boxes.h"
-#include "styles/style_info.h"
-#include "styles/style_menu_icons.h"
#include "styles/style_settings.h"
-#include "styles/style_widgets.h"
-
-#include "../components/icon_picker.h"
-#include "tray.h"
-#include "core/application.h"
-#include "main/main_domain.h"
-#include "styles/style_ayu_icons.h"
-#include "ui/painter.h"
#include "ui/vertical_list.h"
-#include "ui/boxes/confirm_box.h"
-#include "ui/boxes/single_choice_box.h"
-#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
-#include "ui/widgets/continuous_sliders.h"
-#include "ui/widgets/menu/menu_add_action_callback.h"
-#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "window/window_session_controller.h"
-class PainterHighQualityEnabler;
-
-rpl::producer asBeta(rpl::producer text) {
- return std::move(text) | rpl::map([=](const QString &val)
- {
- return val + " β";
- });
-}
-
-not_null AddInnerToggle(not_null container,
- const style::SettingsButton &st,
- std::vector> innerCheckViews,
- not_null*> wrap,
- rpl::producer buttonLabel,
- bool toggledWhenAll) {
- const auto button = container->add(object_ptr(
- container,
- nullptr,
- st::settingsButtonNoIcon));
-
- const auto toggleButton = Ui::CreateChild(
- container.get(),
- nullptr,
- st);
-
- struct State final
- {
- State(const style::Toggle &st, Fn c)
- : checkView(st, false, c) {
- }
-
- Ui::ToggleView checkView;
- Ui::Animations::Simple animation;
- rpl::event_stream<> anyChanges;
- std::vector> innerChecks;
- };
- const auto state = button->lifetime().make_state(
- st.toggle,
- [=]
- {
- toggleButton->update();
- });
- state->innerChecks = std::move(innerCheckViews);
- const auto countChecked = [=]
- {
- return ranges::count_if(
- state->innerChecks,
- [](const auto &v)
- {
- return v->checked();
- });
- };
- for (const auto &innerCheck : state->innerChecks) {
- innerCheck->checkedChanges(
- ) | rpl::to_empty | start_to_stream(
- state->anyChanges,
- button->lifetime());
- }
- const auto checkView = &state->checkView;
- {
- const auto separator = Ui::CreateChild(container.get());
- separator->paintRequest(
- ) | start_with_next([=, bg = st.textBgOver]
- {
- auto p = QPainter(separator);
- p.fillRect(separator->rect(), bg);
- },
- separator->lifetime());
- const auto separatorHeight = 2 * st.toggle.border
- + st.toggle.diameter;
- button->geometryValue(
- ) | start_with_next([=](const QRect &r)
- {
- const auto w = st::rightsButtonToggleWidth;
- constexpr auto kLineWidth = 1;
- toggleButton->setGeometry(
- r.x() + r.width() - w,
- r.y(),
- w,
- r.height());
- separator->setGeometry(
- toggleButton->x() - kLineWidth,
- r.y() + (r.height() - separatorHeight) / 2,
- kLineWidth,
- separatorHeight);
- },
- toggleButton->lifetime());
-
- const auto checkWidget = Ui::CreateChild(toggleButton);
- checkWidget->resize(checkView->getSize());
- checkWidget->paintRequest(
- ) | start_with_next([=]
- {
- auto p = QPainter(checkWidget);
- checkView->paint(p, 0, 0, checkWidget->width());
- },
- checkWidget->lifetime());
- toggleButton->sizeValue(
- ) | start_with_next([=](const QSize &s)
- {
- checkWidget->moveToRight(
- st.toggleSkip,
- (s.height() - checkWidget->height()) / 2);
- },
- toggleButton->lifetime());
- }
-
- const auto totalInnerChecks = state->innerChecks.size();
-
- state->anyChanges.events_starting_with(
- rpl::empty_value()
- ) | rpl::map(countChecked) | start_with_next([=](int count)
- {
- if (toggledWhenAll) {
- checkView->setChecked(count == totalInnerChecks,
- anim::type::normal);
- } else {
- checkView->setChecked(count != 0,
- anim::type::normal);
- }
- },
- toggleButton->lifetime());
- checkView->setLocked(false);
- checkView->finishAnimating();
-
- const auto label = Ui::CreateChild(
- button,
- combine(
- std::move(buttonLabel),
- state->anyChanges.events_starting_with(
- rpl::empty_value()
- ) | rpl::map(countChecked)
- ) | rpl::map([=](const QString &t, int checked)
- {
- auto count = Ui::Text::Bold(" "
- + QString::number(checked)
- + '/'
- + QString::number(totalInnerChecks));
- return TextWithEntities::Simple(t).append(std::move(count));
- }));
- label->setAttribute(Qt::WA_TransparentForMouseEvents);
- const auto arrow = Ui::CreateChild(button);
- {
- const auto &icon = st::permissionsExpandIcon;
- arrow->resize(icon.size());
- arrow->paintRequest(
- ) | start_with_next([=, &icon]
- {
- auto p = QPainter(arrow);
- const auto center = QPointF(
- icon.width() / 2.,
- icon.height() / 2.);
- const auto progress = state->animation.value(
- wrap->toggled() ? 1. : 0.);
- auto hq = std::optional();
- if (progress > 0.) {
- hq.emplace(p);
- p.translate(center);
- p.rotate(progress * 180.);
- p.translate(-center);
- }
- icon.paint(p, 0, 0, arrow->width());
- },
- arrow->lifetime());
- }
- button->sizeValue(
- ) | start_with_next([=, &st](const QSize &s)
- {
- const auto labelLeft = st.padding.left();
- const auto labelRight = s.width() - toggleButton->width();
-
- label->resizeToWidth(labelRight - labelLeft - arrow->width());
- label->moveToLeft(
- labelLeft,
- (s.height() - label->height()) / 2);
- arrow->moveToLeft(
- std::min(
- labelLeft + label->naturalWidth(),
- labelRight - arrow->width()),
- (s.height() - arrow->height()) / 2);
- },
- button->lifetime());
- wrap->toggledValue(
- ) | rpl::skip(1) | start_with_next([=](bool toggled)
- {
- state->animation.start(
- [=]
- {
- arrow->update();
- },
- toggled ? 0. : 1.,
- toggled ? 1. : 0.,
- st::slideWrapDuration,
- anim::easeOutCubic);
- },
- button->lifetime());
- wrap->ease = anim::easeOutCubic;
-
- button->clicks(
- ) | start_with_next([=]
- {
- wrap->toggle(!wrap->toggled(), anim::type::normal);
- },
- button->lifetime());
-
- toggleButton->clicks(
- ) | start_with_next([=]
- {
- const auto checked = !checkView->checked();
- for (const auto &innerCheck : state->innerChecks) {
- innerCheck->setChecked(checked, anim::type::normal);
- }
- },
- toggleButton->lifetime());
-
- return button;
-}
-
-struct NestedEntry
-{
- QString checkboxLabel;
- bool initial;
- std::function callback;
-};
-
-void AddCollapsibleToggle(not_null container,
- rpl::producer title,
- std::vector checkboxes,
- bool toggledWhenAll) {
- const auto addCheckbox = [&](
- not_null verticalLayout,
- const QString &label,
- const bool isCheckedOrig)
- {
- const auto checkView = [&]() -> not_null
- {
- const auto checkbox = verticalLayout->add(
- object_ptr(
- verticalLayout,
- label,
- isCheckedOrig,
- st::settingsCheckbox),
- st::powerSavingButton.padding);
- const auto button = Ui::CreateChild(
- verticalLayout.get(),
- st::defaultRippleAnimation);
- button->stackUnder(checkbox);
- combine(
- verticalLayout->widthValue(),
- checkbox->geometryValue()
- ) | start_with_next([=](int w, const QRect &r)
- {
- button->setGeometry(0, r.y(), w, r.height());
- },
- button->lifetime());
- checkbox->setAttribute(Qt::WA_TransparentForMouseEvents);
- const auto checkView = checkbox->checkView();
- button->setClickedCallback([=]
- {
- checkView->setChecked(
- !checkView->checked(),
- anim::type::normal);
- });
-
- return checkView;
- }();
- checkView->checkedChanges(
- ) | start_with_next([=](bool checked)
- {
- },
- verticalLayout->lifetime());
-
- return checkView;
- };
-
- auto wrap = object_ptr>(
- container,
- object_ptr(container));
- const auto verticalLayout = wrap->entity();
- auto innerChecks = std::vector>();
- for (const auto &entry : checkboxes) {
- const auto c = addCheckbox(verticalLayout, entry.checkboxLabel, entry.initial);
- c->checkedValue(
- ) | start_with_next([=](bool enabled)
- {
- entry.callback(enabled);
- },
- container->lifetime());
- innerChecks.push_back(c);
- }
-
- const auto raw = wrap.data();
- raw->hide(anim::type::instant);
- AddInnerToggle(
- container,
- st::powerSavingButtonNoIcon,
- innerChecks,
- raw,
- std::move(title),
- toggledWhenAll);
- container->add(std::move(wrap));
- container->widthValue(
- ) | start_with_next([=](int w)
- {
- raw->resizeToWidth(w);
- },
- raw->lifetime());
-}
-
-void AddChooseButtonWithIconAndRightTextInner(not_null container,
- not_null controller,
- int initialState,
- std::vector options,
- rpl::producer text,
- rpl::producer boxTitle,
- const style::SettingsButton &st,
- Settings::IconDescriptor &&descriptor,
- const Fn &setter) {
- auto reactiveVal = container->lifetime().make_state>(initialState);
-
- rpl::producer rightTextReactive = reactiveVal->value() | rpl::map(
- [=](int val)
- {
- return options[val];
- });
-
- Settings::AddButtonWithLabel(
- container,
- std::move(text),
- rightTextReactive,
- st,
- std::move(descriptor))->addClickHandler(
- [=]
- {
- controller->show(Box(
- [=](not_null box)
- {
- const auto save = [=](int index) mutable
- {
- setter(index);
-
- reactiveVal->force_assign(index);
- };
- SingleChoiceBox(box,
- {
- .title = boxTitle,
- .options = options,
- .initialSelection = reactiveVal->current(),
- .callback = save,
- });
- }));
- });
-}
-
-void AddChooseButtonWithIconAndRightText(not_null container,
- not_null controller,
- int initialState,
- std::vector options,
- rpl::producer text,
- rpl::producer boxTitle,
- const style::icon &icon,
- const Fn &setter) {
- AddChooseButtonWithIconAndRightTextInner(
- container,
- controller,
- initialState,
- options,
- std::move(text),
- std::move(boxTitle),
- st::settingsButton,
- {&icon},
- setter);
-}
-
-void AddChooseButtonWithIconAndRightText(not_null container,
- not_null controller,
- int initialState,
- std::vector options,
- rpl::producer text,
- rpl::producer boxTitle,
- const Fn &setter) {
- AddChooseButtonWithIconAndRightTextInner(
- container,
- controller,
- initialState,
- options,
- std::move(text),
- std::move(boxTitle),
- st::settingsButtonNoIcon,
- {},
- setter);
-}
-
namespace Settings {
-rpl::producer Ayu::title() {
- return tr::ayu_AyuPreferences();
+rpl::producer AyuGhost::title() {
+ return rpl::single(QString("AyuGram"));
}
-void Ayu::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) {
- addAction(
- tr::ayu_RegisterURLScheme(tr::now),
- [=] { Core::Application::RegisterUrlScheme(); },
- &st::menuIconLinks);
- addAction(
- tr::lng_restart_button(tr::now),
- [=] { Core::Restart(); },
- &st::menuIconRestore);
-}
-
-Ayu::Ayu(
+AyuGhost::AyuGhost(
QWidget *parent,
not_null controller)
: Section(parent) {
@@ -504,7 +73,7 @@ void SetupGhostModeToggle(not_null container) {
},
};
- AddCollapsibleToggle(container, tr::ayu_GhostEssentialsHeader(), checkboxes, true);
+ AddCollapsibleToggle(container, tr::ayu_GhostModeToggle(), checkboxes, true);
}
void SetupGhostEssentials(not_null container) {
@@ -542,6 +111,15 @@ void SetupGhostEssentials(not_null container) {
container->lifetime());
AddSkip(container);
AddDividerText(container, tr::ayu_MarkReadAfterActionDescription());
+}
+
+void SetupScheduleMessages(not_null container) {
+ auto *settings = &AyuSettings::getInstance();
+
+ auto markReadAfterActionVal = container->lifetime().make_state>(
+ settings->markReadAfterAction);
+ auto useScheduledMessagesVal = container->lifetime().make_state>(settings->useScheduledMessages);
AddSkip(container);
AddButtonWithIcon(
@@ -569,6 +147,10 @@ void SetupGhostEssentials(not_null container) {
container->lifetime());
AddSkip(container);
AddDividerText(container, tr::ayu_UseScheduledMessagesDescription());
+}
+
+void SetupSendWithoutSound(not_null container) {
+ auto *settings = &AyuSettings::getInstance();
AddSkip(container);
AddButtonWithIcon(
@@ -685,147 +267,10 @@ void SetupMessageFilters(not_null container) {
container->lifetime());
}
-void SetupQoLToggles(not_null container) {
+void SetupOther(not_null container) {
auto *settings = &AyuSettings::getInstance();
- AddSubsectionTitle(container, tr::ayu_QoLTogglesHeader());
-
- AddButtonWithIcon(
- container,
- tr::ayu_DisableAds(),
- st::settingsButtonNoIcon
- )->toggleOn(
- rpl::single(settings->disableAds)
- )->toggledValue(
- ) | rpl::filter(
- [=](bool enabled)
- {
- return (enabled != settings->disableAds);
- }) | start_with_next(
- [=](bool enabled)
- {
- AyuSettings::set_disableAds(enabled);
- AyuSettings::save();
- },
- container->lifetime());
-
- AddButtonWithIcon(
- container,
- tr::ayu_DisableStories(),
- st::settingsButtonNoIcon
- )->toggleOn(
- rpl::single(settings->disableStories)
- )->toggledValue(
- ) | rpl::filter(
- [=](bool enabled)
- {
- return (enabled != settings->disableStories);
- }) | start_with_next(
- [=](bool enabled)
- {
- AyuSettings::set_disableStories(enabled);
- AyuSettings::save();
- },
- container->lifetime());
-
- AddButtonWithIcon(
- container,
- tr::ayu_DisableCustomBackgrounds(),
- st::settingsButtonNoIcon
- )->toggleOn(
- rpl::single(settings->disableCustomBackgrounds)
- )->toggledValue(
- ) | rpl::filter(
- [=](bool enabled)
- {
- return (enabled != settings->disableCustomBackgrounds);
- }) | start_with_next(
- [=](bool enabled)
- {
- AyuSettings::set_disableCustomBackgrounds(enabled);
- AyuSettings::save();
- },
- container->lifetime());
-
- AddButtonWithIcon(
- container,
- tr::ayu_SimpleQuotesAndReplies(),
- st::settingsButtonNoIcon
- )->toggleOn(
- rpl::single(settings->simpleQuotesAndReplies)
- )->toggledValue(
- ) | rpl::filter(
- [=](bool enabled)
- {
- return (enabled != settings->simpleQuotesAndReplies);
- }) | start_with_next(
- [=](bool enabled)
- {
- AyuSettings::set_simpleQuotesAndReplies(enabled);
- AyuSettings::save();
- },
- container->lifetime());
-
- std::vector checkboxes = {
- NestedEntry{
- tr::ayu_CollapseSimilarChannels(tr::now), settings->collapseSimilarChannels, [=](bool enabled)
- {
- AyuSettings::set_collapseSimilarChannels(enabled);
- AyuSettings::save();
- }
- },
- NestedEntry{
- tr::ayu_HideSimilarChannelsTab(tr::now), settings->hideSimilarChannels, [=](bool enabled)
- {
- AyuSettings::set_hideSimilarChannels(enabled);
- AyuSettings::save();
- }
- }
- };
-
- AddCollapsibleToggle(container, tr::ayu_DisableSimilarChannels(), checkboxes, true);
-
- AddSkip(container);
- AddDivider(container);
- AddSkip(container);
-
- AddButtonWithIcon(
- container,
- tr::ayu_DisableNotificationsDelay(),
- st::settingsButtonNoIcon
- )->toggleOn(
- rpl::single(settings->disableNotificationsDelay)
- )->toggledValue(
- ) | rpl::filter(
- [=](bool enabled)
- {
- return (enabled != settings->disableNotificationsDelay);
- }) | start_with_next(
- [=](bool enabled)
- {
- AyuSettings::set_disableNotificationsDelay(enabled);
- AyuSettings::save();
- },
- container->lifetime());
-
- AddButtonWithIcon(
- container,
- tr::ayu_ShowOnlyAddedEmojisAndStickers(),
- st::settingsButtonNoIcon
- )->toggleOn(
- rpl::single(settings->showOnlyAddedEmojisAndStickers)
- )->toggledValue(
- ) | rpl::filter(
- [=](bool enabled)
- {
- return (enabled != settings->showOnlyAddedEmojisAndStickers);
- }) | start_with_next(
- [=](bool enabled)
- {
- AyuSettings::set_showOnlyAddedEmojisAndStickers(enabled);
- AyuSettings::save();
- },
- container->lifetime());
+ AddSubsectionTitle(container, tr::ayu_MessageSavingOtherHeader());
AddButtonWithIcon(
container,
@@ -846,1025 +291,52 @@ void SetupQoLToggles(not_null container) {
},
container->lifetime());
- AddSkip(container);
- AddDivider(container);
- AddSkip(container);
-
AddButtonWithIcon(
container,
- tr::ayu_HideChannelReactions(),
+ tr::ayu_DisableAds(),
st::settingsButtonNoIcon
)->toggleOn(
- rpl::single(!settings->showChannelReactions)
+ rpl::single(settings->disableAds)
)->toggledValue(
) | rpl::filter(
[=](bool enabled)
{
- return (!enabled != settings->showChannelReactions);
+ return (enabled != settings->disableAds);
}) | start_with_next(
[=](bool enabled)
{
- AyuSettings::set_hideChannelReactions(!enabled);
- AyuSettings::save();
- },
- container->lifetime());
-
- AddButtonWithIcon(
- container,
- tr::ayu_HideGroupReactions(),
- st::settingsButtonNoIcon
- )->toggleOn(
- rpl::single(!settings->showGroupReactions)
- )->toggledValue(
- ) | rpl::filter(
- [=](bool enabled)
- {
- return (!enabled != settings->showGroupReactions);
- }) | start_with_next(
- [=](bool enabled)
- {
- AyuSettings::set_hideGroupReactions(!enabled);
+ AyuSettings::set_disableAds(enabled);
AyuSettings::save();
},
container->lifetime());
}
-void SetupAppIcon(not_null container) {
- container->add(
- object_ptr(container),
- st::settingsCheckboxPadding);
-}
-
-void SetupContextMenuElements(not_null container,
- not_null