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 controller) { - auto *settings = &AyuSettings::getInstance(); - - AddSkip(container); - AddSubsectionTitle(container, tr::ayu_ContextMenuElementsHeader()); - - const auto options = std::vector{ - tr::ayu_SettingsContextMenuItemHidden(tr::now), - tr::ayu_SettingsContextMenuItemShown(tr::now), - tr::ayu_SettingsContextMenuItemExtended(tr::now), - }; - - AddChooseButtonWithIconAndRightText( - container, - controller, - settings->showReactionsPanelInContextMenu, - options, - tr::ayu_SettingsContextMenuReactionsPanel(), - tr::ayu_SettingsContextMenuTitle(), - st::menuIconReactions, - [=](int index) - { - AyuSettings::set_showReactionsPanelInContextMenu(index); - AyuSettings::save(); - }); - AddChooseButtonWithIconAndRightText( - container, - controller, - settings->showViewsPanelInContextMenu, - options, - tr::ayu_SettingsContextMenuViewsPanel(), - tr::ayu_SettingsContextMenuTitle(), - st::menuIconShowInChat, - [=](int index) - { - AyuSettings::set_showViewsPanelInContextMenu(index); - AyuSettings::save(); - }); - - AddChooseButtonWithIconAndRightText( - container, - controller, - settings->showHideMessageInContextMenu, - options, - tr::ayu_ContextHideMessage(), - tr::ayu_SettingsContextMenuTitle(), - st::menuIconClear, - [=](int index) - { - AyuSettings::set_showHideMessageInContextMenu(index); - AyuSettings::save(); - }); - AddChooseButtonWithIconAndRightText( - container, - controller, - settings->showUserMessagesInContextMenu, - options, - tr::ayu_UserMessagesMenuText(), - tr::ayu_SettingsContextMenuTitle(), - st::menuIconTTL, - [=](int index) - { - AyuSettings::set_showUserMessagesInContextMenu(index); - AyuSettings::save(); - }); - AddChooseButtonWithIconAndRightText( - container, - controller, - settings->showMessageDetailsInContextMenu, - options, - tr::ayu_MessageDetailsPC(), - tr::ayu_SettingsContextMenuTitle(), - st::menuIconInfo, - [=](int index) - { - AyuSettings::set_showMessageDetailsInContextMenu(index); - AyuSettings::save(); - }); - - AddSkip(container); - AddDividerText(container, tr::ayu_SettingsContextMenuDescription()); -} - -void SetupMessageFieldElements(not_null container) { - auto *settings = &AyuSettings::getInstance(); - - AddSkip(container); - AddSubsectionTitle(container, tr::ayu_MessageFieldElementsHeader()); - - AddButtonWithIcon( - container, - tr::ayu_MessageFieldElementAttach(), - st::settingsButton, - {&st::messageFieldAttachIcon} - )->toggleOn( - rpl::single(settings->showAttachButtonInMessageField) - )->toggledValue( - ) | rpl::filter( - [=](bool enabled) - { - return (enabled != settings->showAttachButtonInMessageField); - }) | start_with_next( - [=](bool enabled) - { - AyuSettings::set_showAttachButtonInMessageField(enabled); - AyuSettings::save(); - }, - container->lifetime()); - - AddButtonWithIcon( - container, - tr::ayu_MessageFieldElementCommands(), - st::settingsButton, - {&st::messageFieldCommandsIcon} - )->toggleOn( - rpl::single(settings->showCommandsButtonInMessageField) - )->toggledValue( - ) | rpl::filter( - [=](bool enabled) - { - return (enabled != settings->showCommandsButtonInMessageField); - }) | start_with_next( - [=](bool enabled) - { - AyuSettings::set_showCommandsButtonInMessageField(enabled); - AyuSettings::save(); - }, - container->lifetime()); - - AddButtonWithIcon( - container, - tr::ayu_MessageFieldElementTTL(), - st::settingsButton, - {&st::messageFieldTTLIcon} - )->toggleOn( - rpl::single(settings->showAutoDeleteButtonInMessageField) - )->toggledValue( - ) | rpl::filter( - [=](bool enabled) - { - return (enabled != settings->showAutoDeleteButtonInMessageField); - }) | start_with_next( - [=](bool enabled) - { - AyuSettings::set_showAutoDeleteButtonInMessageField(enabled); - AyuSettings::save(); - }, - container->lifetime()); - - AddButtonWithIcon( - container, - tr::ayu_MessageFieldElementEmoji(), - st::settingsButton, - {&st::messageFieldEmojiIcon} - )->toggleOn( - rpl::single(settings->showEmojiButtonInMessageField) - )->toggledValue( - ) | rpl::filter( - [=](bool enabled) - { - return (enabled != settings->showEmojiButtonInMessageField); - }) | start_with_next( - [=](bool enabled) - { - AyuSettings::set_showEmojiButtonInMessageField(enabled); - AyuSettings::save(); - }, - container->lifetime()); - - AddButtonWithIcon( - container, - tr::ayu_MessageFieldElementVoice(), - st::settingsButton, - {&st::messageFieldVoiceIcon} - )->toggleOn( - rpl::single(settings->showMicrophoneButtonInMessageField) - )->toggledValue( - ) | rpl::filter( - [=](bool enabled) - { - return (enabled != settings->showMicrophoneButtonInMessageField); - }) | start_with_next( - [=](bool enabled) - { - AyuSettings::set_showMicrophoneButtonInMessageField(enabled); - AyuSettings::save(); - }, - container->lifetime()); - - AddSkip(container); - AddDivider(container); -} - -void SetupMessageFieldPopups(not_null container) { - auto *settings = &AyuSettings::getInstance(); - - AddSkip(container); - AddSubsectionTitle(container, tr::ayu_MessageFieldPopupsHeader()); - - AddButtonWithIcon( - container, - tr::ayu_MessageFieldElementAttach(), - st::settingsButton, - {&st::messageFieldAttachIcon} - )->toggleOn( - rpl::single(settings->showAttachPopup) - )->toggledValue( - ) | rpl::filter( - [=](bool enabled) - { - return (enabled != settings->showAttachPopup); - }) | start_with_next( - [=](bool enabled) - { - AyuSettings::set_showAttachPopup(enabled); - AyuSettings::save(); - }, - container->lifetime()); - - AddButtonWithIcon( - container, - tr::ayu_MessageFieldElementEmoji(), - st::settingsButton, - {&st::messageFieldEmojiIcon} - )->toggleOn( - rpl::single(settings->showEmojiPopup) - )->toggledValue( - ) | rpl::filter( - [=](bool enabled) - { - return (enabled != settings->showEmojiPopup); - }) | start_with_next( - [=](bool enabled) - { - AyuSettings::set_showEmojiPopup(enabled); - AyuSettings::save(); - }, - container->lifetime()); - - AddSkip(container); - AddDivider(container); -} - -void SetupDrawerElements(not_null container) { - auto *settings = &AyuSettings::getInstance(); - - AddSkip(container); - AddSubsectionTitle(container, tr::ayu_DrawerElementsHeader()); - - AddButtonWithIcon( - container, - tr::ayu_LReadMessages(), - st::settingsButton, - {&st::ayuLReadMenuIcon} - )->toggleOn( - rpl::single(settings->showLReadToggleInDrawer) - )->toggledValue( - ) | rpl::filter( - [=](bool enabled) - { - return (enabled != settings->showLReadToggleInDrawer); - }) | 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); - }) | start_with_next( - [=](bool enabled) - { - AyuSettings::set_showSReadToggleInDrawer(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); - }) | 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); - }) | start_with_next( - [=](bool enabled) - { - AyuSettings::set_showStreamerToggleInDrawer(enabled); - AyuSettings::save(); - }, - container->lifetime()); -#endif -} - -void SetupTrayElements(not_null container) { - auto *settings = &AyuSettings::getInstance(); - - AddSkip(container); - 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); - }) | 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); - }) | start_with_next( - [=](bool enabled) - { - AyuSettings::set_showStreamerToggleInTray(enabled); - AyuSettings::save(); - }, - container->lifetime()); -#endif -} - -void SetupShowPeerId(not_null container, - not_null controller) { - auto *settings = &AyuSettings::getInstance(); - - const auto options = std::vector{ - QString(tr::ayu_SettingsShowID_Hide(tr::now)), - QString("Telegram API"), - QString("Bot API") - }; - - auto currentVal = AyuSettings::get_showPeerIdReactive() | rpl::map( - [=](int val) - { - return options[val]; - }); - - const auto button = AddButtonWithLabel( - container, - tr::ayu_SettingsShowID(), - currentVal, - st::settingsButtonNoIcon); - button->addClickHandler( - [=] - { - controller->show(Box( - [=](not_null box) - { - const auto save = [=](int index) - { - AyuSettings::set_showPeerId(index); - AyuSettings::save(); - }; - SingleChoiceBox(box, - { - .title = tr::ayu_SettingsShowID(), - .options = options, - .initialSelection = settings->showPeerId, - .callback = save, - }); - })); - }); -} - -void SetupRecentStickersLimitSlider(not_null container) { - auto *settings = &AyuSettings::getInstance(); - - container->add( - object_ptr