feat: new settings ui & features

translator, remove message tail, hide fast share, drawer customization, quick admin shortcuts, disable crash reports, donations
This commit is contained in:
AlexeyZavar 2025-07-29 05:02:23 +03:00
parent 1dfe68e9f3
commit dfb9ac91e9
78 changed files with 5144 additions and 1677 deletions

View file

@ -127,8 +127,20 @@ set(ayugram_files
ayu/ui/ayu_logo.h ayu/ui/ayu_logo.h
ayu/ui/utils/ayu_profile_values.cpp ayu/ui/utils/ayu_profile_values.cpp
ayu/ui/utils/ayu_profile_values.h 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.cpp
ayu/ui/settings/settings_ayu.h 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.cpp
ayu/ui/context_menu/context_menu.h ayu/ui/context_menu/context_menu.h
ayu/ui/context_menu/menu_item_subtext.cpp 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/theme_selector_box.h
ayu/ui/boxes/message_shot_box.cpp ayu/ui/boxes/message_shot_box.cpp
ayu/ui/boxes/message_shot_box.h 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.cpp
ayu/ui/components/image_view.h ayu/ui/components/image_view.h
ayu/ui/components/icon_picker.cpp ayu/ui/components/icon_picker.cpp
@ -170,6 +186,18 @@ set(ayugram_files
ayu/features/forward/ayu_forward.h ayu/features/forward/ayu_forward.h
ayu/features/forward/ayu_sync.cpp ayu/features/forward/ayu_sync.cpp
ayu/features/forward/ayu_sync.h 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.cpp
ayu/data/messages_storage.h ayu/data/messages_storage.h
ayu/data/entities.h ayu/data/entities.h

View file

@ -0,0 +1,4 @@
<svg width="72" height="72" viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M65.0957 43.2571C61.089 59.3287 44.8111 69.1096 28.7376 65.1018C12.6708 61.0951 2.88998 44.8162 6.89846 28.7459C10.9034 12.6726 27.1813 2.89099 43.2498 6.8977C59.3222 10.9044 69.1024 27.1851 65.0953 43.2574L65.0956 43.2571H65.0957Z" fill="#F7931A"/>
<path d="M49.2257 31.7258C49.8228 27.7335 46.7833 25.5875 42.627 24.1558L43.9753 18.7479L40.6833 17.9276L39.3707 23.1932C38.5053 22.9773 37.6165 22.7739 36.7332 22.5723L38.0553 17.2719L34.7653 16.4517L33.4162 21.8579C32.7001 21.6948 31.9967 21.5337 31.3142 21.3639L31.318 21.3469L26.7783 20.2132L25.9026 23.7293C25.9026 23.7293 28.3449 24.2891 28.2935 24.3236C29.6266 24.6563 29.8676 25.5388 29.8276 26.2382L28.2917 32.3992C28.3835 32.4225 28.5026 32.4562 28.634 32.509C28.5241 32.4818 28.4073 32.452 28.286 32.423L26.1332 41.0536C25.9703 41.4586 25.5568 42.0664 24.6248 41.8356C24.6577 41.8834 22.2321 41.2385 22.2321 41.2385L20.5977 45.0068L24.8817 46.0747C25.6786 46.2746 26.4596 46.4837 27.2287 46.6803L25.8665 52.1504L29.1547 52.9706L30.5037 47.5587C31.402 47.8026 32.2738 48.0275 33.1272 48.2395L31.7827 53.626L35.0749 54.4463L36.437 48.9865C42.0505 50.0489 46.2715 49.6206 48.0482 44.5431C49.4798 40.4552 47.9769 38.0972 45.0236 36.5596C47.1746 36.0635 48.7948 34.6488 49.2268 31.7263L49.2258 31.7255L49.2257 31.7258ZM41.7042 42.2729C40.6868 46.3608 33.804 44.151 31.5724 43.5969L33.3802 36.35C35.6116 36.9071 42.7675 38.0095 41.7043 42.2729H41.7042ZM42.7223 31.6666C41.7942 35.385 36.0655 33.4959 34.2072 33.0327L35.8462 26.4602C37.7045 26.9235 43.6891 27.788 42.7226 31.6666H42.7223Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,12 @@
<svg width="72" height="72" viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5796 41.8413L22.9288 6H38.8335L35.6272 17.1111C35.5954 17.1746 35.5637 17.2381 35.532 17.3016L27.0875 46.6349H34.9605C31.6589 54.8571 29.0875 61.3016 27.2462 65.9683C12.7066 65.8095 8.64307 55.3968 12.1986 43.0794M27.3097 66L46.4843 38.4127H38.3574L45.4367 20.7302C57.5637 22 63.278 31.5556 59.9129 43.1111C56.3256 55.5238 41.7859 66 27.6272 66C27.5002 66 27.405 66 27.3097 66Z" fill="url(#paint0_linear_1491_36)"/>
<defs>
<linearGradient id="paint0_linear_1491_36" x1="44.5725" y1="13.793" x2="24.0992" y2="83.6122" gradientUnits="userSpaceOnUse">
<stop stop-color="#EF7829"/>
<stop offset="0.0518954" stop-color="#F07529"/>
<stop offset="0.3551" stop-color="#F0672B"/>
<stop offset="0.6673" stop-color="#F15E2C"/>
<stop offset="1" stop-color="#F15A2C"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 898 B

View file

@ -0,0 +1,7 @@
<svg width="72" height="72" viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M36.414 28.1822V6L18 36.5512L36.414 28.1822Z" fill="#8A92B2"/>
<path d="M36.414 47.4398V28.1822L18 36.5512L36.414 47.4398ZM36.414 28.1822L54.8279 36.5512L36.414 6V28.1822Z" fill="#62688F"/>
<path d="M36.4141 28.1824V47.44L54.828 36.5513L36.4141 28.1824Z" fill="#454A75"/>
<path d="M36.414 50.927L18 40.0496L36.414 66.0001V50.927Z" fill="#8A92B2"/>
<path d="M54.8393 40.0496L36.4141 50.927V66.0001L54.8393 40.0496Z" fill="#62688F"/>
</svg>

After

Width:  |  Height:  |  Size: 544 B

View file

@ -0,0 +1,19 @@
<svg width="72" height="72" viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.7481 47.8879C16.1101 47.5259 16.608 47.3147 17.1359 47.3147H65.0164C65.8914 47.3147 66.3288 48.3707 65.7103 48.9892L56.2519 58.4476C55.8899 58.8096 55.3921 59.0208 54.8641 59.0208H6.98358C6.10864 59.0208 5.67117 57.9649 6.28966 57.3464L15.7481 47.8879Z" fill="url(#paint0_linear_1498_44)"/>
<path d="M15.7481 12.5732C16.1252 12.2112 16.623 12 17.1359 12H65.0164C65.8914 12 66.3288 13.056 65.7103 13.6745L56.2519 23.1329C55.8899 23.4949 55.3921 23.7061 54.8641 23.7061H6.98358C6.10864 23.7061 5.67117 22.6502 6.28966 22.0317L15.7481 12.5732Z" fill="url(#paint1_linear_1498_44)"/>
<path d="M56.2519 30.1174C55.8899 29.7554 55.3921 29.5442 54.8641 29.5442H6.98358C6.10864 29.5442 5.67117 30.6002 6.28966 31.2186L15.7481 40.6771C16.1101 41.0391 16.608 41.2503 17.1359 41.2503H65.0164C65.8914 41.2503 66.3288 40.1944 65.7103 39.5759L56.2519 30.1174Z" fill="url(#paint2_linear_1498_44)"/>
<defs>
<linearGradient id="paint0_linear_1498_44" x1="60.4424" y1="6.34998" x2="27.3053" y2="69.8209" gradientUnits="userSpaceOnUse">
<stop stop-color="#00FFA3"/>
<stop offset="1" stop-color="#DC1FFF"/>
</linearGradient>
<linearGradient id="paint1_linear_1498_44" x1="45.9531" y1="-1.21486" x2="12.816" y2="62.256" gradientUnits="userSpaceOnUse">
<stop stop-color="#00FFA3"/>
<stop offset="1" stop-color="#DC1FFF"/>
</linearGradient>
<linearGradient id="paint2_linear_1498_44" x1="53.1517" y1="2.54346" x2="20.0145" y2="66.0144" gradientUnits="userSpaceOnUse">
<stop stop-color="#00FFA3"/>
<stop offset="1" stop-color="#DC1FFF"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,4 @@
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="80" height="80" rx="40" fill="#E83030"/>
<path d="M39.999 27.5657C50.4546 16.7028 63.2343 24.7157 63.2344 36.0149C63.2344 47.3141 53.8952 53.3355 47.0586 58.7249C44.6462 60.6266 42.3226 62.4172 39.999 62.4172C37.6755 62.4172 35.3519 60.6266 32.9395 58.7249C26.1029 53.3355 16.7637 47.3141 16.7637 36.0149C16.7638 24.7157 29.5435 16.7028 39.999 27.5657ZM48.2627 35.677C48.7923 34.089 47.2803 32.5775 45.6924 33.1077L30.1621 38.2932C28.6596 38.7949 28.6067 41.0213 30.0391 41.6975C32.1559 42.6961 34.5207 44.0094 35.9287 45.4172C37.3438 46.8324 38.6704 49.2208 39.6777 51.3528C40.354 52.7834 42.5744 52.728 43.0752 51.2268L48.2627 35.677Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 769 B

View file

@ -0,0 +1,11 @@
<svg width="72" height="72" viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1491_40)">
<path d="M36 66C52.5686 66 66 52.5686 66 36C66 19.4314 52.5686 6 36 6C19.4314 6 6 19.4314 6 36C6 52.5686 19.4314 66 36 66Z" fill="#0098EA"/>
<path d="M46.2396 22.7437H25.7521C21.9851 22.7437 19.5976 26.807 21.4927 30.0919L34.1369 52.0078C34.962 53.4388 37.0297 53.4388 37.8548 52.0078L50.5015 30.0919C52.3941 26.8123 50.0065 22.7437 46.2422 22.7437H46.2396ZM34.1266 45.4355L31.3729 40.1062L24.7285 28.2226C24.2902 27.462 24.8316 26.4873 25.7495 26.4873H34.124V45.4381L34.1266 45.4355ZM47.258 28.22L40.6162 40.1087L37.8625 45.4355V26.4848H46.237C47.1549 26.4848 47.6963 27.4594 47.258 28.22Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_1491_40">
<rect width="60" height="60" fill="white" transform="translate(6 6)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 879 B

View file

@ -0,0 +1,3 @@
<svg width="72" height="72" viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M63.2493 24.0756C60.4368 21.4787 56.5462 17.5131 53.3775 14.7007L53.19 14.5694C52.878 14.3189 52.5263 14.1224 52.1494 13.9882C44.5089 12.5632 8.94997 5.91642 8.25623 6.0008C8.06184 6.02802 7.87604 6.0985 7.71249 6.20704L7.53437 6.34767C7.31504 6.57041 7.14845 6.83951 7.04687 7.13516L7 7.25703V7.92265V8.02577C11.0031 19.1725 26.8091 55.6876 29.9216 64.2563C30.1091 64.8375 30.4653 65.9438 31.1309 66H31.2809C31.6372 66 33.1559 63.9938 33.1559 63.9938C33.1559 63.9938 60.3055 31.0692 63.0524 27.563C63.4079 27.1311 63.7218 26.6666 63.9899 26.1755C64.0583 25.7913 64.026 25.396 63.8962 25.028C63.7664 24.66 63.5435 24.3318 63.2493 24.0756ZM40.1214 27.9099L51.7088 18.3006L58.5056 24.563L40.1214 27.9099ZM35.6215 27.2818L15.6718 10.932L47.9495 16.885L35.6215 27.2818ZM37.4215 31.5661L57.8399 28.2755L34.4965 56.4001L37.4215 31.5661ZM12.9624 12.5632L33.9528 30.3755L30.9153 56.4189L12.9624 12.5632Z" fill="#FF060A"/>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -6831,6 +6831,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"ayu_AyuPreferences" = "AyuGram Preferences"; "ayu_AyuPreferences" = "AyuGram Preferences";
"ayu_DocsText" = "Documentation"; "ayu_DocsText" = "Documentation";
"ayu_CategoryGeneral" = "General";
"ayu_CategoryAppearance" = "Appearance";
"ayu_CategoryChats" = "Chats";
"ayu_CategoryOther" = "Other";
"ayu_CategoryGhostMode" = "Ghost Mode"; "ayu_CategoryGhostMode" = "Ghost Mode";
"ayu_CategorySpy" = "Spy"; "ayu_CategorySpy" = "Spy";
"ayu_CategoryFilters" = "Filters"; "ayu_CategoryFilters" = "Filters";
@ -6886,12 +6890,39 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"ayu_DisableAds" = "Disable Ads"; "ayu_DisableAds" = "Disable Ads";
"ayu_DisableStories" = "Disable Stories"; "ayu_DisableStories" = "Disable Stories";
"ayu_DisableCustomBackgrounds" = "Disable Custom Backgrounds"; "ayu_DisableCustomBackgrounds" = "Disable Custom Backgrounds";
"ayu_SmoothScroll" = "Smooth Scroll";
"ayu_DisableNotificationsDelay" = "Disable Notify Delay"; "ayu_DisableNotificationsDelay" = "Disable Notify Delay";
"ayu_LocalPremium" = "Local Telegram Premium"; "ayu_LocalPremium" = "Local Telegram Premium";
"ayu_DisplayGhostStatus" = "Display Ghost Mode Status"; "ayu_DisplayGhostStatus" = "Display Ghost Mode Status";
"ayu_CopyUsernameAsLink" = "Copy Username as Link"; "ayu_CopyUsernameAsLink" = "Copy Username as Link";
"ayu_HideChannelReactions" = "Hide Reactions in Channels"; "ayu_HideChannelReactions" = "Hide Reactions in Channels";
"ayu_HideGroupReactions" = "Hide Reactions in Groups"; "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_CustomizationHeader" = "Customization";
"ayu_DeletedMarkText" = "Deleted Mark"; "ayu_DeletedMarkText" = "Deleted Mark";
"ayu_DeletedMarkNothing" = "Nothing"; "ayu_DeletedMarkNothing" = "Nothing";
@ -6903,6 +6934,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"ayu_SemiTransparentDeletedMessages" = "Translucent Deleted Messages"; "ayu_SemiTransparentDeletedMessages" = "Translucent Deleted Messages";
"ayu_HideNotificationCounters" = "Hide Notification Counters"; "ayu_HideNotificationCounters" = "Hide Notification Counters";
"ayu_HideNotificationBadge" = "Hide Notification Badge"; "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_HideAllChats" = "Hide \"All Chats\" Tab";
"ayu_ChannelBottomButton" = "Channel Bottom Button"; "ayu_ChannelBottomButton" = "Channel Bottom Button";
"ayu_ChannelBottomButtonHide" = "Hide"; "ayu_ChannelBottomButtonHide" = "Hide";
@ -6911,8 +6943,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"ayu_SettingsShowID" = "Show Peer ID"; "ayu_SettingsShowID" = "Show Peer ID";
"ayu_SettingsShowID_Hide" = "Hide"; "ayu_SettingsShowID_Hide" = "Hide";
"ayu_SettingsShowMessageShot" = "Show Message Shot"; "ayu_SettingsShowMessageShot" = "Show Message Shot";
"ayu_SettingsShowMessageShotDescription" = "Allows taking a screenshot of the selected messages.";
"ayu_SettingsRecentStickersCount" = "Recent Stickers Count"; "ayu_SettingsRecentStickersCount" = "Recent Stickers Count";
"ayu_SettingsCustomizationHint" = "You must restart the application after making changes in the \"Customization\" section."; "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_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_SettingsContextMenuDescription" = "Extended menu items will be displayed if you hold CTRL or SHIFT while right-clicking on the message.";
"ayu_SettingsContextMenuItemHidden" = "Hidden"; "ayu_SettingsContextMenuItemHidden" = "Hidden";
@ -6931,6 +6968,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"ayu_DrawerElementsHeader" = "Drawer Elements"; "ayu_DrawerElementsHeader" = "Drawer Elements";
"ayu_TrayElementsHeader" = "Tray Elements"; "ayu_TrayElementsHeader" = "Tray Elements";
"ayu_SettingsWideMultiplier" = "Wide Messages Multiplier"; "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_SettingsSpoofWebviewAsAndroid" = "Spoof Client as Android";
"ayu_SettingsBiggerWindow" = "Bigger Window"; "ayu_SettingsBiggerWindow" = "Bigger Window";
"ayu_SettingsIncreaseWebviewHeight" = "Increase Content Height"; "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_FiltersClearPopupText" = "Are you sure you want to clear all filters?";
"ayu_FiltersClearPopupActionText" = "Clear"; "ayu_FiltersClearPopupActionText" = "Clear";
"ayu_FiltersClearPopupAltActionText" = "Clear unknown"; "ayu_FiltersClearPopupAltActionText" = "Clear unknown";
"ayu_AppIconHeader" = "App Icon";
"ayu_IconDefault" = "Default"; "ayu_IconDefault" = "Default";
"ayu_IconAlternative" = "AyuGram Alt"; "ayu_IconAlternative" = "AyuGram Alt";
"ayu_IconDiscord" = "Discord"; "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_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_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_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_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_ConfirmationSticker" = "Do you want to send this sticker?";
"ayu_ConfirmationGIF" = "Do you want to send this GIF?"; "ayu_ConfirmationGIF" = "Do you want to send this GIF?";
"ayu_ConfirmationVoice" = "Do you want to send this voice message?"; "ayu_ConfirmationVoice" = "Do you want to send this voice message?";

View file

@ -52,5 +52,12 @@
<file alias="art/ayu/yaplus/app_preview.png">../../art/ayu/yaplus/app_preview.png</file> <file alias="art/ayu/yaplus/app_preview.png">../../art/ayu/yaplus/app_preview.png</file>
<file alias="art/ayu/yaplus/app_macos.png">../../art/ayu/yaplus/app_macos.png</file> <file alias="art/ayu/yaplus/app_macos.png">../../art/ayu/yaplus/app_macos.png</file>
<file alias="art/ayu/yaplus/app_icon.ico">../../art/ayu/yaplus/app_icon.ico</file> <file alias="art/ayu/yaplus/app_icon.ico">../../art/ayu/yaplus/app_icon.ico</file>
<file alias="icons/ayu/donates/boosty.svg">../../icons/ayu/donates/boosty.svg</file>
<file alias="icons/ayu/donates/ton.svg">../../icons/ayu/donates/ton.svg</file>
<file alias="icons/ayu/donates/bitcoin.svg">../../icons/ayu/donates/bitcoin.svg</file>
<file alias="icons/ayu/donates/ethereum.svg">../../icons/ayu/donates/ethereum.svg</file>
<file alias="icons/ayu/donates/solana.svg">../../icons/ayu/donates/solana.svg</file>
<file alias="icons/ayu/donates/tron.svg">../../icons/ayu/donates/tron.svg</file>
<file alias="icons/ayu/donates/support_logo.svg">../../icons/ayu/donates/support_logo.svg</file>
</qresource> </qresource>
</RCC> </RCC>

View file

@ -14,7 +14,7 @@ class ChannelData;
namespace Info::Profile { namespace Info::Profile {
class Badge; class Badge;
enum class BadgeType : uchar; enum class BadgeType : ushort;
} // namespace Info::Profile } // namespace Info::Profile
namespace Main { namespace Main {

View file

@ -11,6 +11,7 @@
#include "ayu/ayu_ui_settings.h" #include "ayu/ayu_ui_settings.h"
#include "ayu/ayu_worker.h" #include "ayu/ayu_worker.h"
#include "ayu/data/ayu_database.h" #include "ayu/data/ayu_database.h"
#include "features/translator/ayu_translator.h"
#include "lang/lang_instance.h" #include "lang/lang_instance.h"
#include "utils/rc_manager.h" #include "utils/rc_manager.h"
@ -32,6 +33,7 @@ void initUiSettings() {
AyuUiSettings::setMonoFont(settings.monoFont); AyuUiSettings::setMonoFont(settings.monoFont);
AyuUiSettings::setWideMultiplier(settings.wideMultiplier); AyuUiSettings::setWideMultiplier(settings.wideMultiplier);
AyuUiSettings::setMaterialSwitches(settings.materialSwitches);
} }
void initDatabase() { void initDatabase() {
@ -46,12 +48,17 @@ void initRCManager() {
RCManager::getInstance().start(); RCManager::getInstance().start();
} }
void initTranslator() {
Ayu::Translator::TranslateManager::init();
}
void init() { void init() {
initLang(); initLang();
initDatabase(); initDatabase();
initUiSettings(); initUiSettings();
initWorker(); initWorker();
initRCManager(); initRCManager();
initTranslator();
} }
} }

View file

@ -18,6 +18,7 @@
#include <fstream> #include <fstream>
#include "ayu_worker.h" #include "ayu_worker.h"
#include "features/translator/ayu_translator.h"
#include "window/window_controller.h" #include "window/window_controller.h"
using json = nlohmann::json; using json = nlohmann::json;
@ -41,6 +42,8 @@ rpl::variable<QString> editedMarkReactive;
rpl::variable<int> showPeerIdReactive; rpl::variable<int> showPeerIdReactive;
rpl::variable<QString> translationProviderReactive;
rpl::variable<bool> hideFromBlockedReactive; rpl::variable<bool> hideFromBlockedReactive;
rpl::event_stream<> historyUpdateReactive; rpl::event_stream<> historyUpdateReactive;
@ -137,6 +140,7 @@ void postinitialize() {
deletedMarkReactive = settings->deletedMark; deletedMarkReactive = settings->deletedMark;
editedMarkReactive = settings->editedMark; editedMarkReactive = settings->editedMark;
showPeerIdReactive = settings->showPeerId; showPeerIdReactive = settings->showPeerId;
translationProviderReactive = settings->translationProvider;
hideFromBlockedReactive = settings->hideFromBlocked; hideFromBlockedReactive = settings->hideFromBlocked;
@ -194,6 +198,15 @@ void save() {
postinitialize(); postinitialize();
} }
void reset() {
lifetime.destroy();
lifetime = rpl::lifetime();
settings = std::nullopt;
initialize();
postinitialize();
save();
}
AyuGramSettings::AyuGramSettings() { AyuGramSettings::AyuGramSettings() {
// ~ Ghost essentials // ~ Ghost essentials
sendReadMessages = true; sendReadMessages = true;
@ -229,6 +242,9 @@ AyuGramSettings::AyuGramSettings() {
increaseWebviewHeight = false; increaseWebviewHeight = false;
increaseWebviewWidth = false; increaseWebviewWidth = false;
materialSwitches = true;
removeMessageTail = false;
disableNotificationsDelay = false; disableNotificationsDelay = false;
localPremium = false; localPremium = false;
showChannelReactions = true; showChannelReactions = true;
@ -243,6 +259,7 @@ AyuGramSettings::AyuGramSettings() {
#endif #endif
; ;
simpleQuotesAndReplies = true; simpleQuotesAndReplies = true;
hideFastShare = false;
replaceBottomInfoWithIcons = true; replaceBottomInfoWithIcons = true;
deletedMark = "🧹"; deletedMark = "🧹";
editedMark = Core::IsAppLaunched() ? tr::lng_edited(tr::now) : QString("edited"); editedMark = Core::IsAppLaunched() ? tr::lng_edited(tr::now) : QString("edited");
@ -267,8 +284,17 @@ AyuGramSettings::AyuGramSettings() {
showAttachPopup = true; showAttachPopup = true;
showEmojiPopup = true; showEmojiPopup = true;
// ~ Drawer Elements
showMyProfileInDrawer = true;
showBotsInDrawer = true;
showNewGroupInDrawer = true;
showNewChannelInDrawer = true;
showContactsInDrawer = true;
showCallsInDrawer = true;
showSavedMessagesInDrawer = true;
showLReadToggleInDrawer = false; showLReadToggleInDrawer = false;
showSReadToggleInDrawer = true; showSReadToggleInDrawer = true;
showNightModeToggleInDrawer = true;
showGhostToggleInDrawer = true; showGhostToggleInDrawer = true;
showStreamerToggleInDrawer = false; showStreamerToggleInDrawer = false;
@ -287,6 +313,7 @@ AyuGramSettings::AyuGramSettings() {
* channelBottomButton = 2 means "Discuss" + fallback to "Mute"/"Unmute" * channelBottomButton = 2 means "Discuss" + fallback to "Mute"/"Unmute"
*/ */
channelBottomButton = 2; channelBottomButton = 2;
quickAdminShortcuts = true;
/* /*
* showPeerId = 0 means no ID shown * showPeerId = 0 means no ID shown
@ -301,6 +328,10 @@ AyuGramSettings::AyuGramSettings() {
stickerConfirmation = false; stickerConfirmation = false;
gifConfirmation = false; gifConfirmation = false;
voiceConfirmation = false; voiceConfirmation = false;
translationProvider = "telegram"; // telegram, google, yandex
crashReporting = true;
} }
void set_sendReadMessages(bool val) { void set_sendReadMessages(bool val) {
@ -411,6 +442,14 @@ void set_increaseWebviewWidth(bool val) {
settings->increaseWebviewWidth = 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) { void set_disableNotificationsDelay(bool val) {
settings->disableNotificationsDelay = val; settings->disableNotificationsDelay = val;
} }
@ -435,6 +474,10 @@ void set_simpleQuotesAndReplies(bool val) {
settings->simpleQuotesAndReplies = val; settings->simpleQuotesAndReplies = val;
} }
void set_hideFastShare(bool val) {
settings->hideFastShare = val;
}
void set_replaceBottomInfoWithIcons(bool val) { void set_replaceBottomInfoWithIcons(bool val) {
settings->replaceBottomInfoWithIcons = val; settings->replaceBottomInfoWithIcons = val;
} }
@ -508,6 +551,34 @@ void set_showEmojiPopup(bool val) {
triggerHistoryUpdate(); 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) { void set_showLReadToggleInDrawer(bool val) {
settings->showLReadToggleInDrawer = val; settings->showLReadToggleInDrawer = val;
} }
@ -516,6 +587,10 @@ void set_showSReadToggleInDrawer(bool val) {
settings->showSReadToggleInDrawer = val; settings->showSReadToggleInDrawer = val;
} }
void set_showNightModeToggleInDrawer(bool val) {
settings->showNightModeToggleInDrawer = val;
}
void set_showGhostToggleInDrawer(bool val) { void set_showGhostToggleInDrawer(bool val) {
settings->showGhostToggleInDrawer = val; settings->showGhostToggleInDrawer = val;
} }
@ -557,6 +632,10 @@ void set_channelBottomButton(int val) {
settings->channelBottomButton = val; settings->channelBottomButton = val;
} }
void set_quickAdminShortcuts(bool val) {
settings->quickAdminShortcuts = val;
}
void set_showMessageSeconds(bool val) { void set_showMessageSeconds(bool val) {
settings->showMessageSeconds = val; settings->showMessageSeconds = val;
} }
@ -577,6 +656,16 @@ void set_voiceConfirmation(bool val) {
settings->voiceConfirmation = 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() { bool isUseScheduledMessages() {
return isGhostModeActive() && settings->useScheduledMessages; return isGhostModeActive() && settings->useScheduledMessages;
} }
@ -597,6 +686,10 @@ rpl::producer<int> get_showPeerIdReactive() {
return showPeerIdReactive.value(); return showPeerIdReactive.value();
} }
rpl::producer<QString> get_translationProviderReactive() {
return translationProviderReactive.value();
}
rpl::producer<bool> get_ghostModeEnabledReactive() { rpl::producer<bool> get_ghostModeEnabledReactive() {
return ghostModeEnabled.value(); return ghostModeEnabled.value();
} }

View file

@ -74,6 +74,9 @@ public:
bool increaseWebviewHeight; bool increaseWebviewHeight;
bool increaseWebviewWidth; bool increaseWebviewWidth;
bool materialSwitches;
bool removeMessageTail;
bool disableNotificationsDelay; bool disableNotificationsDelay;
bool localPremium; bool localPremium;
bool showChannelReactions; bool showChannelReactions;
@ -81,6 +84,7 @@ public:
QString appIcon; QString appIcon;
bool simpleQuotesAndReplies; bool simpleQuotesAndReplies;
bool hideFastShare;
bool replaceBottomInfoWithIcons; bool replaceBottomInfoWithIcons;
QString deletedMark; QString deletedMark;
QString editedMark; QString editedMark;
@ -101,8 +105,16 @@ public:
bool showAttachPopup; bool showAttachPopup;
bool showEmojiPopup; bool showEmojiPopup;
bool showMyProfileInDrawer;
bool showBotsInDrawer;
bool showNewGroupInDrawer;
bool showNewChannelInDrawer;
bool showContactsInDrawer;
bool showCallsInDrawer;
bool showSavedMessagesInDrawer;
bool showLReadToggleInDrawer; bool showLReadToggleInDrawer;
bool showSReadToggleInDrawer; bool showSReadToggleInDrawer;
bool showNightModeToggleInDrawer;
bool showGhostToggleInDrawer; bool showGhostToggleInDrawer;
bool showStreamerToggleInDrawer; bool showStreamerToggleInDrawer;
@ -116,6 +128,7 @@ public:
bool hideAllChatsFolder; bool hideAllChatsFolder;
int channelBottomButton; int channelBottomButton;
bool quickAdminShortcuts;
int showPeerId; int showPeerId;
bool showMessageSeconds; bool showMessageSeconds;
@ -124,6 +137,10 @@ public:
bool stickerConfirmation; bool stickerConfirmation;
bool gifConfirmation; bool gifConfirmation;
bool voiceConfirmation; bool voiceConfirmation;
QString translationProvider;
bool crashReporting;
}; };
void set_sendReadMessages(bool val); void set_sendReadMessages(bool val);
@ -158,6 +175,9 @@ void set_spoofWebviewAsAndroid(bool val);
void set_increaseWebviewHeight(bool val); void set_increaseWebviewHeight(bool val);
void set_increaseWebviewWidth(bool val); void set_increaseWebviewWidth(bool val);
void set_materialSwitches(bool val);
void set_removeMessageTail(bool val);
void set_disableNotificationsDelay(bool val); void set_disableNotificationsDelay(bool val);
void set_localPremium(bool val); void set_localPremium(bool val);
void set_hideChannelReactions(bool val); void set_hideChannelReactions(bool val);
@ -165,6 +185,7 @@ void set_hideGroupReactions(bool val);
void set_appIcon(const QString &val); void set_appIcon(const QString &val);
void set_simpleQuotesAndReplies(bool val); void set_simpleQuotesAndReplies(bool val);
void set_hideFastShare(bool val);
void set_replaceBottomInfoWithIcons(bool val); void set_replaceBottomInfoWithIcons(bool val);
void set_deletedMark(const QString &val); void set_deletedMark(const QString &val);
void set_editedMark(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_showAttachPopup(bool val);
void set_showEmojiPopup(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_showLReadToggleInDrawer(bool val);
void set_showSReadToggleInDrawer(bool val); void set_showSReadToggleInDrawer(bool val);
void set_showNightModeToggleInDrawer(bool val);
void set_showGhostToggleInDrawer(bool val); void set_showGhostToggleInDrawer(bool val);
void set_showStreamerToggleInDrawer(bool val); void set_showStreamerToggleInDrawer(bool val);
@ -200,6 +229,7 @@ void set_hideNotificationBadge(bool val);
void set_hideAllChatsFolder(bool val); void set_hideAllChatsFolder(bool val);
void set_channelBottomButton(int val); void set_channelBottomButton(int val);
void set_quickAdminShortcuts(bool val);
void set_showPeerId(int val); void set_showPeerId(int val);
void set_showMessageSeconds(bool val); void set_showMessageSeconds(bool val);
@ -209,6 +239,10 @@ void set_stickerConfirmation(bool val);
void set_gifConfirmation(bool val); void set_gifConfirmation(bool val);
void set_voiceConfirmation(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) { inline void to_json(nlohmann::json &nlohmann_json_j, const AyuGramSettings &nlohmann_json_t) {
NLOHMANN_JSON_TO(sendReadMessages) NLOHMANN_JSON_TO(sendReadMessages)
NLOHMANN_JSON_TO(sendReadStories) 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(spoofWebviewAsAndroid)
NLOHMANN_JSON_TO(increaseWebviewHeight) NLOHMANN_JSON_TO(increaseWebviewHeight)
NLOHMANN_JSON_TO(increaseWebviewWidth) NLOHMANN_JSON_TO(increaseWebviewWidth)
NLOHMANN_JSON_TO(materialSwitches)
NLOHMANN_JSON_TO(removeMessageTail)
NLOHMANN_JSON_TO(disableNotificationsDelay) NLOHMANN_JSON_TO(disableNotificationsDelay)
NLOHMANN_JSON_TO(localPremium) NLOHMANN_JSON_TO(localPremium)
NLOHMANN_JSON_TO(appIcon) NLOHMANN_JSON_TO(appIcon)
NLOHMANN_JSON_TO(simpleQuotesAndReplies) NLOHMANN_JSON_TO(simpleQuotesAndReplies)
NLOHMANN_JSON_TO(hideFastShare)
NLOHMANN_JSON_TO(replaceBottomInfoWithIcons) NLOHMANN_JSON_TO(replaceBottomInfoWithIcons)
NLOHMANN_JSON_TO(deletedMark) NLOHMANN_JSON_TO(deletedMark)
NLOHMANN_JSON_TO(editedMark) 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(showAutoDeleteButtonInMessageField)
NLOHMANN_JSON_TO(showAttachPopup) NLOHMANN_JSON_TO(showAttachPopup)
NLOHMANN_JSON_TO(showEmojiPopup) 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(showLReadToggleInDrawer)
NLOHMANN_JSON_TO(showSReadToggleInDrawer) NLOHMANN_JSON_TO(showSReadToggleInDrawer)
NLOHMANN_JSON_TO(showNightModeToggleInDrawer)
NLOHMANN_JSON_TO(showGhostToggleInDrawer) NLOHMANN_JSON_TO(showGhostToggleInDrawer)
NLOHMANN_JSON_TO(showStreamerToggleInDrawer) NLOHMANN_JSON_TO(showStreamerToggleInDrawer)
NLOHMANN_JSON_TO(showGhostToggleInTray) 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(hideNotificationBadge)
NLOHMANN_JSON_TO(hideAllChatsFolder) NLOHMANN_JSON_TO(hideAllChatsFolder)
NLOHMANN_JSON_TO(channelBottomButton) NLOHMANN_JSON_TO(channelBottomButton)
NLOHMANN_JSON_TO(quickAdminShortcuts)
NLOHMANN_JSON_TO(showPeerId) NLOHMANN_JSON_TO(showPeerId)
NLOHMANN_JSON_TO(showMessageSeconds) NLOHMANN_JSON_TO(showMessageSeconds)
NLOHMANN_JSON_TO(showMessageShot) NLOHMANN_JSON_TO(showMessageShot)
NLOHMANN_JSON_TO(stickerConfirmation) NLOHMANN_JSON_TO(stickerConfirmation)
NLOHMANN_JSON_TO(gifConfirmation) NLOHMANN_JSON_TO(gifConfirmation)
NLOHMANN_JSON_TO(voiceConfirmation) 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) { 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(spoofWebviewAsAndroid)
NLOHMANN_JSON_FROM_WITH_DEFAULT(increaseWebviewHeight) NLOHMANN_JSON_FROM_WITH_DEFAULT(increaseWebviewHeight)
NLOHMANN_JSON_FROM_WITH_DEFAULT(increaseWebviewWidth) 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(disableNotificationsDelay)
NLOHMANN_JSON_FROM_WITH_DEFAULT(localPremium) NLOHMANN_JSON_FROM_WITH_DEFAULT(localPremium)
NLOHMANN_JSON_FROM_WITH_DEFAULT(appIcon) NLOHMANN_JSON_FROM_WITH_DEFAULT(appIcon)
NLOHMANN_JSON_FROM_WITH_DEFAULT(simpleQuotesAndReplies) NLOHMANN_JSON_FROM_WITH_DEFAULT(simpleQuotesAndReplies)
NLOHMANN_JSON_FROM_WITH_DEFAULT(hideFastShare)
NLOHMANN_JSON_FROM_WITH_DEFAULT(replaceBottomInfoWithIcons) NLOHMANN_JSON_FROM_WITH_DEFAULT(replaceBottomInfoWithIcons)
NLOHMANN_JSON_FROM_WITH_DEFAULT(deletedMark) NLOHMANN_JSON_FROM_WITH_DEFAULT(deletedMark)
NLOHMANN_JSON_FROM_WITH_DEFAULT(editedMark) 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(showAutoDeleteButtonInMessageField)
NLOHMANN_JSON_FROM_WITH_DEFAULT(showAttachPopup) NLOHMANN_JSON_FROM_WITH_DEFAULT(showAttachPopup)
NLOHMANN_JSON_FROM_WITH_DEFAULT(showEmojiPopup) 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(showLReadToggleInDrawer)
NLOHMANN_JSON_FROM_WITH_DEFAULT(showSReadToggleInDrawer) NLOHMANN_JSON_FROM_WITH_DEFAULT(showSReadToggleInDrawer)
NLOHMANN_JSON_FROM_WITH_DEFAULT(showNightModeToggleInDrawer)
NLOHMANN_JSON_FROM_WITH_DEFAULT(showGhostToggleInDrawer) NLOHMANN_JSON_FROM_WITH_DEFAULT(showGhostToggleInDrawer)
NLOHMANN_JSON_FROM_WITH_DEFAULT(showStreamerToggleInDrawer) NLOHMANN_JSON_FROM_WITH_DEFAULT(showStreamerToggleInDrawer)
NLOHMANN_JSON_FROM_WITH_DEFAULT(showGhostToggleInTray) 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(hideNotificationBadge)
NLOHMANN_JSON_FROM_WITH_DEFAULT(hideAllChatsFolder) NLOHMANN_JSON_FROM_WITH_DEFAULT(hideAllChatsFolder)
NLOHMANN_JSON_FROM_WITH_DEFAULT(channelBottomButton) NLOHMANN_JSON_FROM_WITH_DEFAULT(channelBottomButton)
NLOHMANN_JSON_FROM_WITH_DEFAULT(quickAdminShortcuts)
NLOHMANN_JSON_FROM_WITH_DEFAULT(showPeerId) NLOHMANN_JSON_FROM_WITH_DEFAULT(showPeerId)
NLOHMANN_JSON_FROM_WITH_DEFAULT(showMessageSeconds) NLOHMANN_JSON_FROM_WITH_DEFAULT(showMessageSeconds)
NLOHMANN_JSON_FROM_WITH_DEFAULT(showMessageShot) NLOHMANN_JSON_FROM_WITH_DEFAULT(showMessageShot)
NLOHMANN_JSON_FROM_WITH_DEFAULT(stickerConfirmation) NLOHMANN_JSON_FROM_WITH_DEFAULT(stickerConfirmation)
NLOHMANN_JSON_FROM_WITH_DEFAULT(gifConfirmation) NLOHMANN_JSON_FROM_WITH_DEFAULT(gifConfirmation)
NLOHMANN_JSON_FROM_WITH_DEFAULT(voiceConfirmation) NLOHMANN_JSON_FROM_WITH_DEFAULT(voiceConfirmation)
NLOHMANN_JSON_FROM_WITH_DEFAULT(translationProvider)
NLOHMANN_JSON_FROM_WITH_DEFAULT(crashReporting)
} }
AyuGramSettings &getInstance(); AyuGramSettings &getInstance();
void load(); void load();
void save(); void save();
void reset();
rpl::producer<QString> get_deletedMarkReactive(); rpl::producer<QString> get_deletedMarkReactive();
rpl::producer<QString> get_editedMarkReactive(); rpl::producer<QString> get_editedMarkReactive();
rpl::producer<int> get_showPeerIdReactive(); rpl::producer<int> get_showPeerIdReactive();
rpl::producer<QString> get_translationProviderReactive();
bool isGhostModeActive(); bool isGhostModeActive();
bool isUseScheduledMessages(); bool isUseScheduledMessages();

View file

@ -20,6 +20,9 @@
#include <QDesktopServices> #include <QDesktopServices>
#include "main/main_session.h" #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 { namespace AyuUrlHandlers {
@ -68,7 +71,40 @@ bool HandleAyu(
if (!controller) { if (!controller) {
return false; return false;
} }
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); 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; return true;
} }

View file

@ -23,6 +23,11 @@ bool HandleAyu(
const Match &match, const Match &match,
const QVariant &context); const QVariant &context);
bool HandleSupport(
Window::SessionController *controller,
const Match &match,
const QVariant &context);
bool TryHandleSpotify(const QString &url); bool TryHandleSpotify(const QString &url);
} }

View file

@ -6,8 +6,8 @@
// Copyright @Radolyn, 2025 // Copyright @Radolyn, 2025
#include "message_shot.h" #include "message_shot.h"
#include "styles/style_layers.h"
#include "styles/style_ayu_styles.h" #include "styles/style_ayu_styles.h"
#include "styles/style_layers.h"
#include "qguiapplication.h" #include "qguiapplication.h"
#include "ayu/ui/boxes/message_shot_box.h" #include "ayu/ui/boxes/message_shot_box.h"
@ -20,8 +20,8 @@
#include "history/history.h" #include "history/history.h"
#include "history/history_inner_widget.h" #include "history/history_inner_widget.h"
#include "history/history_item.h" #include "history/history_item.h"
#include "history/view/history_view_element.h"
#include "history/history_item_components.h" #include "history/history_item_components.h"
#include "history/view/history_view_element.h"
#include "history/view/media/history_view_media.h" #include "history/view/media/history_view_media.h"
#include "main/main_session.h" #include "main/main_session.h"
#include "styles/style_chat.h" #include "styles/style_chat.h"

View file

@ -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 <optional>
#include <QtCore/QCryptographicHash>
#include <QtCore/QString>
#include <QtNetwork/QNetworkReply>
#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<MTPmessages_translateText::Flags> &flags,
const MTPInputPeer &peer,
const MTPVector<MTPint> &id,
const MTPVector<MTPTextWithEntities> &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<void()> 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<MTPmessages_translateText::Flags> &flags,
const MTPInputPeer &peer,
const MTPVector<MTPint> &id,
const MTPVector<MTPTextWithEntities> &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<TextWithEntities> texts;
std::vector<QString> cacheKeys;
std::vector<TextWithEntities> resultTexts;
std::vector<int> uncachedIndices;
std::vector<TextWithEntities> 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<MTPTextWithEntities>();
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<MTPTextWithEntities>(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<TextWithEntities> &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<MTPTextWithEntities>();
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<MTPTextWithEntities>(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::CacheEntry> 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();
}
}

View file

@ -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 <functional>
#include <list>
#include <unordered_map>
#include <QtCore/QString>
#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<MTPmessages_translateText::Flags> &flags,
const MTPInputPeer &peer,
const MTPVector<MTPint> &id,
const MTPVector<MTPTextWithEntities> &text,
const MTPstring &to_lang
);
Builder(Builder &&) noexcept = default;
Builder &operator=(Builder &&) noexcept = default;
Builder &done(std::function<void(const Result &)> cb);
Builder &fail(std::function<void(const MTP::Error &)> cb);
Builder &fail(std::function<void()> 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<Main::Session*> _session;
mtpRequestId _id = 0;
MTPflags<MTPmessages_translateText::Flags> _flags;
MTPInputPeer _peer;
MTPVector<MTPint> _idList;
MTPVector<MTPTextWithEntities> _text;
MTPstring _toLang;
std::function<void(const Result &)> _done;
std::function<void(const MTP::Error &)> _fail;
friend class TranslateManager;
};
TranslateManager() = default;
~TranslateManager() = default;
Builder request(
Main::Session &session,
const MTPflags<MTPmessages_translateText::Flags> &flags,
const MTPInputPeer &peer,
const MTPVector<MTPint> &id,
const MTPVector<MTPTextWithEntities> &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<std::pair<CacheKey, CacheEntry>>::iterator;
std::list<std::pair<CacheKey, CacheEntry>> _cacheList;
std::unordered_map<CacheKey, CacheIterator> _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<CacheEntry> getFromCache(const QString &key);
void removeLeastRecentlyUsed();
struct Pending
{
std::function<void(const Result &)> done;
std::function<void(const MTP::Error &)> fail;
CallbackCancel cancel;
};
mtpRequestId _nextId = 1;
std::unordered_map<mtpRequestId, Pending> _pending;
};
} // namespace Ayu::Translator

View file

@ -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;
}
}

View file

@ -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 <QtCore/QString>
#include "ui/text/text_entity.h"
namespace Ayu::Translator::Html {
[[nodiscard]] QString entitiesToHtml(const TextWithEntities &text);
[[nodiscard]] TextWithEntities htmlToEntities(const QString &text);
}

View file

@ -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 <QtCore/QJsonArray>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonValue>
#include <QtCore/QString>
#include <QtCore/QTimer>
#include "base/random.h"
#include <cmath>
#include <memory>
#include <vector>
#include "./base.h"
namespace Ayu::Translator {
std::vector<QString> 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<int>(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<TextWithEntities> inputs;
QString from;
QString to;
CallbackSuccess onSuccess;
CallbackFail onFail;
std::vector<TextWithEntities> results;
std::vector<QPointer<QNetworkReply>> replies;
std::vector<int> retryCount;
std::vector<QPointer<QTimer>> retryTimers;
int total = 0;
int nextIndex = 0;
int inProgress = 0;
bool finished = false;
std::function<void()> pump;
std::function<void(int)> 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<BatchState>();
state->self = this;
state->inputs = texts;
state->from = fromLang;
state->to = toLang;
state->onSuccess = args.onSuccess;
state->onFail = args.onFail;
state->total = static_cast<int>(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<int>(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();
};
}
}

View file

@ -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 <QtCore/QObject>
#include <QtCore/QPointer>
#include <QtCore/QSet>
#include <QtCore/QString>
#include <QtNetwork/QNetworkReply>
#include <functional>
#include "apiwrap.h"
namespace Main {
class Session;
}
namespace Ayu::Translator {
using CallbackSuccess = std::function<void(const std::vector<TextWithEntities> &)>;
using CallbackFail = std::function<void()>;
using CallbackCancel = std::function<void()>;
using MultiThreadCallbackSuccess = std::function<void(const TextWithEntities &)>;
QString randomDesktopUserAgent();
bool shouldWrapInHtml();
QString parseJsonPath(const QByteArray &body, const QString &jsonPath, bool *ok);
struct PassedData
{
MTPflags<MTPmessages_translateText::Flags> flags;
MTPInputPeer peer;
MTPVector<MTPint> idList;
MTPVector<MTPTextWithEntities> text;
MTPstring toLang;
};
struct ParsedData
{
std::vector<TextWithEntities> 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<QString> 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<QNetworkReply> startSingleTranslation(
const MultiThreadArgs &args
) = 0;
};
}

View file

@ -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 <memory>
#include <QtCore/QJsonArray>
#include <QtCore/QPointer>
#include <QtCore/QTimer>
#include <QtNetwork/QNetworkRequest>
#include <QtNetwork/QNetworkReply>
#include <QtCore/QUrl>
#include <QtCore/QUrlQuery>
#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<QNetworkReply> 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<QNetworkReply> 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<QNetworkReply, void(*)(QNetworkReply *)>(
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;
}
}

View file

@ -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 <QtCore/QObject>
#include <QtCore/QPointer>
#include <QtCore/QSet>
#include <QtCore/QString>
#include "./base.h"
namespace Ayu::Translator {
class GoogleTranslator final : public MultiThreadTranslator
{
Q_OBJECT
public:
static GoogleTranslator &instance();
// all languages
[[nodiscard]] QSet<QString> supportedLanguages() const override { return {}; }
[[nodiscard]] QPointer<QNetworkReply> startSingleTranslation(
const MultiThreadArgs &args
) override;
private:
explicit GoogleTranslator(QObject *parent = nullptr);
QNetworkAccessManager _nam;
};
}

View file

@ -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 <QtNetwork/QNetworkReply>
#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<TextWithEntities>();
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();
};
}
}

View file

@ -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 <QtCore/QObject>
#include <QtCore/QSet>
#include <QtCore/QString>
#include "./base.h"
namespace Ayu::Translator {
class TelegramTranslator final : public BaseTranslator
{
Q_OBJECT
public:
static TelegramTranslator &instance();
[[nodiscard]] QSet<QString> supportedLanguages() const override { return {}; }
[[nodiscard]] CallbackCancel startTranslation(
const StartTranslationArgs &args
) override;
private:
explicit TelegramTranslator(QObject *parent = nullptr);
};
}

View file

@ -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 <memory>
#include <QtCore/QPointer>
#include <QtCore/QTimer>
#include <QtCore/QUrl>
#include <QtCore/QUrlQuery>
#include <QtCore/QUuid>
#include <QtNetwork/QNetworkReply>
#include <QtNetwork/QNetworkRequest>
#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<QString> YandexTranslator::supportedLanguages() const {
static const QSet<QString> 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<QNetworkReply> 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<QNetworkReply> 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<QNetworkReply, void(*)(QNetworkReply *)>(
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;
}
}

View file

@ -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 <QtCore/QObject>
#include <QtCore/QPointer>
#include <QtCore/QSet>
#include <QtCore/QString>
#include "./base.h"
namespace Ayu::Translator {
class YandexTranslator final : public MultiThreadTranslator
{
Q_OBJECT
public:
static YandexTranslator &instance();
[[nodiscard]] QSet<QString> supportedLanguages() const override;
[[nodiscard]] QPointer<QNetworkReply> startSingleTranslation(
const MultiThreadArgs &args
) override;
private:
explicit YandexTranslator(QObject *parent = nullptr);
QNetworkAccessManager _nam;
QString _uuid;
};
}

View file

@ -12,7 +12,7 @@ using "ui/colors.palette";
using "ui/widgets/widgets.style"; using "ui/widgets/widgets.style";
using "info/info.style"; using "info/info.style";
/* Color Picker */ /* Icon Picker */
cpPadding: 14px; cpPadding: 14px;
cpSelectedPadding: 2px; cpSelectedPadding: 2px;
cpSelectedRounding: 12px; cpSelectedRounding: 12px;
@ -69,3 +69,5 @@ exteraBadgeToast: Toast(defaultToast) {
icon: icon {{ "ayu/extera_badge", toastFg }}; icon: icon {{ "ayu/extera_badge", toastFg }};
iconPosition: point(13px, 13px); iconPosition: point(13px, 13px);
} }
supportLogoSize: 96px;

View file

@ -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 <QSvgRenderer>
#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<Ui::RpWidget> CreateTopLogoWidget(
not_null<Ui::RpWidget*> parent) {
auto w = object_ptr<Ui::RpWidget>(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<Ui::RpWidget> InfoRow(
not_null<Ui::RpWidget*> parent,
not_null<Main::Session*> session,
const QString &title,
const TextWithEntities &text,
not_null<const style::icon*> icon) {
auto row = object_ptr<Ui::VerticalLayout>(parent);
const auto raw = row.data();
raw->add(
object_ptr<Ui::FlatLabel>(
raw,
rpl::single(title) | Ui::Text::ToBold(),
st::defaultFlatLabel),
st::settingsPremiumRowTitlePadding);
const auto label = raw->add(
object_ptr<Ui::FlatLabel>(
raw,
st::boxDividerLabel),
st::settingsPremiumRowAboutPadding);
label->setMarkedText(
text,
Core::TextContext({
.session = std::move(session),
})
);
object_ptr<Info::Profile::FloatingIcon>(
raw,
*icon,
st::starrefInfoIconPosition);
return row;
}
} // namespace
void FillDonateInfoBox(not_null<Ui::GenericBox*> box, not_null<Window::SessionController*> 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<Ui::CenterWrap<Ui::FlatLabel>>(
box,
object_ptr<Ui::FlatLabel>(
box->verticalLayout(),
tr::ayu_SupportBoxHeader()
| Ui::Text::ToBold(),
st::boxTitle)),
st::boxRowPadding);
box->verticalLayout()->add(
object_ptr<Ui::FlatLabel>(
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

View file

@ -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<Ui::GenericBox*> box, not_null<Window::SessionController*> controller);
} // namespace Ui

View file

@ -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 <QtGui/QClipboard>
#include <QtGui/QGuiApplication>
#include <QtSvg/QSvgRenderer>
#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<Ui::GenericBox*> 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<Ui::RpWidget>(box->verticalLayout()));
struct State {
QImage qrImage;
int qrMaxSize = 0;
};
const auto state = qrWidget->lifetime().make_state<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<Ui::InviteLinkLabel>(
box->verticalLayout(),
rpl::single(address),
Fn<base::unique_qptr<Ui::PopupMenu>()>());
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

View file

@ -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<Ui::GenericBox*> box,
const QString &address,
const QString &iconResourcePath);
} // namespace Ui

View file

@ -58,7 +58,7 @@ void drawIcon(QPainter &p, const QImage &icon, int xOffset, int yOffset, float s
); );
p.restore(); p.restore();
auto rect = QRect( const auto rect = QRect(
xOffset + st::cpImagePadding, xOffset + st::cpImagePadding,
yOffset + st::cpImagePadding, yOffset + st::cpImagePadding,
st::cpIconSize, st::cpIconSize,
@ -87,6 +87,7 @@ IconPicker::IconPicker(QWidget *parent)
IconPicker::~IconPicker() { IconPicker::~IconPicker() {
cachedIcons.clear(); cachedIcons.clear();
} }
void IconPicker::paintEvent(QPaintEvent *e) { void IconPicker::paintEvent(QPaintEvent *e) {
Painter p(this); Painter p(this);
PainterHighQualityEnabler hq(p); PainterHighQualityEnabler hq(p);

View file

@ -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;
}

View file

@ -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<Window::SessionController*> 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<QString> AyuAppearance::title() {
return tr::ayu_CategoryAppearance();
}
AyuAppearance::AyuAppearance(
QWidget *parent,
not_null<Window::SessionController*> controller)
: Section(parent) {
setupContent(controller);
}
void SetupAppIcon(not_null<Ui::VerticalLayout*> container) {
AddSubsectionTitle(container, tr::ayu_AppIconHeader());
container->add(
object_ptr<IconPicker>(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<Ui::VerticalLayout*> container, not_null<Window::SessionController*> 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<base::binary_guard>(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<Ui::VerticalLayout*> 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<Ui::VerticalLayout*> container, not_null<Window::SessionController*> 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<Ui::VerticalLayout*> 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<Window::SessionController*> controller) {
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
AddSkip(content);
SetupAppIcon(content);
SetupAppearance(content, controller);
SetupChatFolders(content);
SetupTrayElements(content);
SetupDrawerElements(content, controller);
AddSkip(content);
ResizeFitChild(this, content);
}
} // namespace Settings

View file

@ -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<AyuAppearance>
{
public:
AyuAppearance(QWidget *parent, not_null<Window::SessionController*> controller);
[[nodiscard]] rpl::producer<QString> title() override;
private:
void setupContent(not_null<Window::SessionController*> controller);
};
} // namespace Settings

File diff suppressed because it is too large Load diff

View file

@ -9,25 +9,19 @@
#include "settings/settings_common.h" #include "settings/settings_common.h"
#include "settings/settings_common_session.h" #include "settings/settings_common_session.h"
class BoxContent;
namespace Window { namespace Window {
class Controller;
class SessionController; class SessionController;
} // namespace Window } // namespace Window
namespace Settings { namespace Settings {
class Ayu : public Section<Ayu> class AyuGhost : public Section<AyuGhost>
{ {
public: public:
Ayu(QWidget *parent, not_null<Window::SessionController*> controller); AyuGhost(QWidget *parent, not_null<Window::SessionController*> controller);
[[nodiscard]] rpl::producer<QString> title() override; [[nodiscard]] rpl::producer<QString> title() override;
void fillTopBarMenu(
const Ui::Menu::MenuCallback &addAction) override;
private: private:
void setupContent(not_null<Window::SessionController*> controller); void setupContent(not_null<Window::SessionController*> controller);
}; };

View file

@ -0,0 +1,412 @@
// 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_ayu_utils.h"
#include "settings/settings_common.h"
#include "styles/style_info.h"
#include "styles/style_layers.h"
#include "styles/style_settings.h"
#include "ui/painter.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/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "window/window_session_controller.h"
class PainterHighQualityEnabler;
namespace Settings {
rpl::producer<QString> asBeta(rpl::producer<QString> text) {
return std::move(text) | rpl::map([=](const QString &val)
{
return val + " β";
});
}
not_null<Ui::RpWidget*> AddInnerToggle(not_null<Ui::VerticalLayout*> container,
const style::SettingsButton &st,
std::vector<not_null<Ui::AbstractCheckView*>> innerCheckViews,
not_null<Ui::SlideWrap<>*> wrap,
rpl::producer<QString> buttonLabel,
bool toggledWhenAll) {
const auto button = container->add(object_ptr<Ui::SettingsButton>(
container,
nullptr,
st::settingsButtonNoIcon));
const auto toggleButton = Ui::CreateChild<Ui::SettingsButton>(
container.get(),
nullptr,
st);
struct State final
{
State(const style::Toggle &st, Fn<void()> c)
: checkView(st, false, c) {
}
Ui::ToggleView checkView;
Ui::Animations::Simple animation;
rpl::event_stream<> anyChanges;
std::vector<not_null<Ui::AbstractCheckView*>> innerChecks;
};
const auto state = button->lifetime().make_state<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<Ui::RpWidget>(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<Ui::RpWidget>(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<Ui::FlatLabel>(
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));
}),
st::boxLabel);
label->setAttribute(Qt::WA_TransparentForMouseEvents);
const auto arrow = Ui::CreateChild<Ui::RpWidget>(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<PainterHighQualityEnabler>();
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;
}
void AddCollapsibleToggle(not_null<Ui::VerticalLayout*> container,
rpl::producer<QString> title,
std::vector<NestedEntry> checkboxes,
bool toggledWhenAll) {
const auto addCheckbox = [&](
not_null<Ui::VerticalLayout*> verticalLayout,
const QString &label,
const bool isCheckedOrig)
{
const auto checkView = [&]() -> not_null<Ui::AbstractCheckView*>
{
const auto checkbox = verticalLayout->add(
object_ptr<Ui::Checkbox>(
verticalLayout,
label,
isCheckedOrig,
st::settingsCheckbox),
st::powerSavingButton.padding);
const auto button = Ui::CreateChild<Ui::RippleButton>(
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<Ui::SlideWrap<Ui::VerticalLayout>>(
container,
object_ptr<Ui::VerticalLayout>(container));
const auto verticalLayout = wrap->entity();
auto innerChecks = std::vector<not_null<Ui::AbstractCheckView*>>();
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<Ui::VerticalLayout*> container,
not_null<Window::SessionController*> controller,
int initialState,
std::vector<QString> options,
rpl::producer<QString> text,
rpl::producer<QString> boxTitle,
const style::SettingsButton &st,
Settings::IconDescriptor &&descriptor,
const Fn<void(int)> &setter) {
auto reactiveVal = container->lifetime().make_state<rpl::variable<int>>(initialState);
rpl::producer<QString> 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<Ui::GenericBox*> 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<Ui::VerticalLayout*> container,
not_null<Window::SessionController*> controller,
int initialState,
std::vector<QString> options,
rpl::producer<QString> text,
rpl::producer<QString> boxTitle,
const style::icon &icon,
const Fn<void(int)> &setter) {
AddChooseButtonWithIconAndRightTextInner(
container,
controller,
initialState,
options,
std::move(text),
std::move(boxTitle),
st::settingsButton,
{&icon},
setter);
}
void AddChooseButtonWithIconAndRightText(not_null<Ui::VerticalLayout*> container,
not_null<Window::SessionController*> controller,
int initialState,
std::vector<QString> options,
rpl::producer<QString> text,
rpl::producer<QString> boxTitle,
const Fn<void(int)> &setter) {
AddChooseButtonWithIconAndRightTextInner(
container,
controller,
initialState,
options,
std::move(text),
std::move(boxTitle),
st::settingsButtonNoIcon,
{},
setter);
}
} // namespace Settings

View file

@ -0,0 +1,68 @@
// 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 "ui/widgets/buttons.h"
#include "ui/widgets/checkbox.h"
#include "ui/wrap/slide_wrap.h"
namespace Window {
class SessionController;
} // namespace Window
namespace Settings {
struct NestedEntry
{
QString checkboxLabel;
bool initial;
std::function<void(bool)> callback;
};
rpl::producer<QString> asBeta(rpl::producer<QString> text);
not_null<Ui::RpWidget*> AddInnerToggle(not_null<Ui::VerticalLayout*> container,
const style::SettingsButton &st,
std::vector<not_null<Ui::AbstractCheckView*>> innerCheckViews,
not_null<Ui::SlideWrap<>*> wrap,
rpl::producer<QString> buttonLabel,
bool toggledWhenAll);
void AddCollapsibleToggle(not_null<Ui::VerticalLayout*> container,
rpl::producer<QString> title,
std::vector<NestedEntry> checkboxes,
bool toggledWhenAll);
void AddChooseButtonWithIconAndRightTextInner(not_null<Ui::VerticalLayout*> container,
not_null<Window::SessionController*> controller,
int initialState,
std::vector<QString> options,
rpl::producer<QString> text,
rpl::producer<QString> boxTitle,
const style::SettingsButton &st,
Settings::IconDescriptor &&descriptor,
const Fn<void(int)> &setter);
void AddChooseButtonWithIconAndRightText(not_null<Ui::VerticalLayout*> container,
not_null<Window::SessionController*> controller,
int initialState,
std::vector<QString> options,
rpl::producer<QString> text,
rpl::producer<QString> boxTitle,
const style::icon &icon,
const Fn<void(int)> &setter);
void AddChooseButtonWithIconAndRightText(not_null<Ui::VerticalLayout*> container,
not_null<Window::SessionController*> controller,
int initialState,
std::vector<QString> options,
rpl::producer<QString> text,
rpl::producer<QString> boxTitle,
const Fn<void(int)> &setter);
} // namespace Settings

View file

@ -0,0 +1,655 @@
// 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_chats.h"
#include <styles/style_ayu_icons.h>
#include "lang_auto.h"
#include "ayu/ayu_settings.h"
#include "ayu/ui/boxes/edit_mark_box.h"
#include "ayu/ui/settings/settings_ayu_utils.h"
#include "core/application.h"
#include "settings/settings_common.h"
#include "styles/style_ayu_styles.h"
#include "styles/style_boxes.h"
#include "styles/style_menu_icons.h"
#include "styles/style_settings.h"
#include "ui/vertical_list.h"
#include "ui/boxes/confirm_box.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/continuous_sliders.h"
#include "ui/widgets/labels.h"
#include "ui/wrap/vertical_layout.h"
#include "window/window_session_controller.h"
namespace Settings {
rpl::producer<QString> AyuChats::title() {
return tr::ayu_CategoryChats();
}
AyuChats::AyuChats(
QWidget *parent,
not_null<Window::SessionController*> controller)
: Section(parent) {
setupContent(controller);
}
void SetupStickersAndEmojiSettings(not_null<Ui::VerticalLayout*> container) {
auto *settings = &AyuSettings::getInstance();
AddSubsectionTitle(container, tr::lng_settings_stickers_emoji()/*rpl::single(QString("Stickers and Emoji"))*/);
AddButtonWithIcon(
container,
tr::ayu_ShowOnlyAddedEmojisAndStickers(),
st::settingsButtonNoIcon
)->toggleOn(
rpl::single(settings->showOnlyAddedEmojisAndStickers)
)->toggledValue(
) | rpl::filter(
[=](bool enabled)
{
return (enabled != settings->showOnlyAddedEmojisAndStickers);
}) | rpl::start_with_next(
[=](bool enabled)
{
AyuSettings::set_showOnlyAddedEmojisAndStickers(enabled);
AyuSettings::save();
},
container->lifetime());
std::vector checkboxes = {
NestedEntry{
tr::ayu_HideReactionsInChannels(tr::now), !settings->showChannelReactions, [=](bool enabled)
{
AyuSettings::set_hideChannelReactions(!enabled);
AyuSettings::save();
}
},
NestedEntry{
tr::ayu_HideReactionsInGroups(tr::now), !settings->showGroupReactions, [=](bool enabled)
{
AyuSettings::set_hideGroupReactions(!enabled);
AyuSettings::save();
}
}
};
AddCollapsibleToggle(container, tr::ayu_HideReactions(), checkboxes, false);
AddSkip(container);
AddDivider(container);
AddSkip(container);
}
void SetupRecentStickersLimit(not_null<Ui::VerticalLayout*> container) {
auto *settings = &AyuSettings::getInstance();
container->add(
object_ptr<Button>(container,
tr::ayu_SettingsRecentStickersCount(),
st::settingsButtonNoIcon)
)->setAttribute(Qt::WA_TransparentForMouseEvents);
auto recentStickersLimitSlider = MakeSliderWithLabel(
container,
st::autoDownloadLimitSlider,
st::settingsScaleLabel,
0,
st::settingsScaleLabel.style.font->width("8%%"));
container->add(std::move(recentStickersLimitSlider.widget), st::recentStickersLimitPadding);
const auto slider = recentStickersLimitSlider.slider;
const auto label = recentStickersLimitSlider.label;
const auto updateLabel = [=](int amount)
{
label->setText(QString::number(amount));
};
updateLabel(settings->recentStickersCount);
slider->setPseudoDiscrete(
200 + 1,
[=](int amount)
{
return amount;
},
settings->recentStickersCount,
[=](int amount)
{
updateLabel(amount);
},
[=](int amount)
{
updateLabel(amount);
AyuSettings::set_recentStickersCount(amount);
AyuSettings::save();
});
AddSkip(container);
AddDivider(container);
AddSkip(container);
}
void SetupGroupsAndChannels(not_null<Ui::VerticalLayout*> container, not_null<Window::SessionController*> controller) {
auto *settings = &AyuSettings::getInstance();
AddSubsectionTitle(container,
tr::lng_premium_double_limits_subtitle_channels()
/*rpl::single(QString("Groups and Channels"))*/);
const auto options = std::vector{
tr::ayu_ChannelBottomButtonHide(tr::now),
tr::ayu_ChannelBottomButtonMute(tr::now),
tr::ayu_ChannelBottomButtonDiscuss(tr::now),
};
AddChooseButtonWithIconAndRightText(
container,
controller,
settings->channelBottomButton,
options,
tr::ayu_ChannelBottomButton(),
tr::ayu_ChannelBottomButton(),
[=](int index)
{
AyuSettings::set_channelBottomButton(index);
AyuSettings::save();
});
AddButtonWithIcon(
container,
tr::ayu_QuickAdminShortcuts(),
st::settingsButtonNoIcon
)->toggleOn(
rpl::single(settings->quickAdminShortcuts)
)->toggledValue(
) | rpl::filter(
[=](bool enabled)
{
return (enabled != settings->quickAdminShortcuts);
}) | rpl::start_with_next(
[=](bool enabled)
{
AyuSettings::set_quickAdminShortcuts(enabled);
AyuSettings::save();
},
container->lifetime());
AddButtonWithIcon(
container,
tr::ayu_SettingsShowMessageShot(),
st::settingsButtonNoIcon
)->toggleOn(
rpl::single(settings->showMessageShot)
)->toggledValue(
) | rpl::filter(
[=](bool enabled)
{
return (enabled != settings->showMessageShot);
}) | start_with_next(
[=](bool enabled)
{
AyuSettings::set_showMessageShot(enabled);
AyuSettings::save();
},
container->lifetime());
AddSkip(container);
AddDividerText(container, tr::ayu_SettingsShowMessageShotDescription());
AddSkip(container);
}
void SetupMarks(not_null<Ui::VerticalLayout*> container) {
auto *settings = &AyuSettings::getInstance();
AddSubsectionTitle(container, tr::lng_settings_messages()/*rpl::single(QString("Messages"))*/);
AddButtonWithLabel(
container,
tr::ayu_DeletedMarkText(),
AyuSettings::get_deletedMarkReactive(),
st::settingsButtonNoIcon
)->addClickHandler(
[=]()
{
auto box = Box<EditMarkBox>(
tr::ayu_DeletedMarkText(),
settings->deletedMark,
QString("🧹"),
[=](const QString &value)
{
AyuSettings::set_deletedMark(value);
AyuSettings::save();
}
);
Ui::show(std::move(box));
});
AddButtonWithLabel(
container,
tr::ayu_EditedMarkText(),
AyuSettings::get_editedMarkReactive(),
st::settingsButtonNoIcon
)->addClickHandler(
[=]()
{
auto box = Box<EditMarkBox>(
tr::ayu_EditedMarkText(),
settings->editedMark,
tr::lng_edited(tr::now),
[=](const QString &value)
{
AyuSettings::set_editedMark(value);
AyuSettings::save();
}
);
Ui::show(std::move(box));
});
AddButtonWithIcon(
container,
tr::ayu_ReplaceMarksWithIcons(),
st::settingsButtonNoIcon
)->toggleOn(
rpl::single(settings->replaceBottomInfoWithIcons)
)->toggledValue(
) | rpl::filter(
[=](bool enabled)
{
return (enabled != settings->replaceBottomInfoWithIcons);
}) | start_with_next(
[=](bool enabled)
{
AyuSettings::set_replaceBottomInfoWithIcons(enabled);
AyuSettings::save();
},
container->lifetime());
AddSkip(container);
AddDivider(container);
AddSkip(container);
AddButtonWithIcon(
container,
tr::ayu_HideShareButton(),
st::settingsButtonNoIcon
)->toggleOn(
rpl::single(settings->hideFastShare)
)->toggledValue(
) | rpl::filter(
[=](bool enabled)
{
return (enabled != settings->hideFastShare);
}) | start_with_next(
[=](bool enabled)
{
AyuSettings::set_hideFastShare(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);
}) | rpl::start_with_next(
[=](bool enabled)
{
AyuSettings::set_simpleQuotesAndReplies(enabled);
AyuSettings::save();
},
container->lifetime());
AddSkip(container);
AddDivider(container);
AddSkip(container);
}
void SetupWideMessagesMultiplier(not_null<Ui::VerticalLayout*> container,
not_null<Window::SessionController*> controller) {
auto *settings = &AyuSettings::getInstance();
container->add(
object_ptr<Button>(container,
tr::ayu_SettingsWideMultiplier(),
st::settingsButtonNoIcon)
)->setAttribute(Qt::WA_TransparentForMouseEvents);
auto wideMultiplierSlider = MakeSliderWithLabel(
container,
st::autoDownloadLimitSlider,
st::settingsScaleLabel,
0,
st::settingsScaleLabel.style.font->width("8%%%"));
container->add(std::move(wideMultiplierSlider.widget), st::recentStickersLimitPadding);
const auto slider = wideMultiplierSlider.slider;
const auto label = wideMultiplierSlider.label;
const auto updateLabel = [=](double val)
{
label->setText(QString::number(val, 'f', 2));
};
constexpr auto kSizeAmount = 61; // (4.00 - 1.00) / 0.05 + 1
constexpr auto kMinSize = 1.00;
constexpr auto kStep = 0.05;
const auto valueToIndex = [=](double value)
{
return static_cast<int>(std::round((value - kMinSize) / kStep));
};
const auto indexToValue = [=](int index)
{
return kMinSize + index * kStep;
};
updateLabel(settings->wideMultiplier);
slider->setPseudoDiscrete(
kSizeAmount,
[=](int index) { return index; },
valueToIndex(settings->wideMultiplier),
[=](int index)
{
updateLabel(indexToValue(index));
},
[=](int index)
{
updateLabel(indexToValue(index));
AyuSettings::set_wideMultiplier(indexToValue(index));
AyuSettings::save();
// fix slider
crl::on_main([=]
{
controller->show(Ui::MakeConfirmBox({
.text = tr::lng_settings_need_restart(),
.confirmed = []
{
Core::Restart();
},
.confirmText = tr::lng_settings_restart_now(),
.cancelText = tr::lng_settings_restart_later(),
}));
});
});
AddSkip(container);
AddDividerText(container, tr::ayu_SettingsWideMultiplierDescription());
AddSkip(container);
}
void SetupContextMenuElements(not_null<Ui::VerticalLayout*> container,
not_null<Window::SessionController*> controller) {
auto *settings = &AyuSettings::getInstance();
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());
AddSkip(container);
}
void SetupMessageFieldElements(not_null<Ui::VerticalLayout*> container) {
auto *settings = &AyuSettings::getInstance();
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);
}) | rpl::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);
}) | rpl::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);
}) | rpl::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);
}) | rpl::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);
}) | rpl::start_with_next(
[=](bool enabled)
{
AyuSettings::set_showMicrophoneButtonInMessageField(enabled);
AyuSettings::save();
},
container->lifetime());
AddSkip(container);
AddDivider(container);
AddSkip(container);
}
void SetupMessageFieldPopups(not_null<Ui::VerticalLayout*> container) {
auto *settings = &AyuSettings::getInstance();
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);
}) | rpl::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);
}) | rpl::start_with_next(
[=](bool enabled)
{
AyuSettings::set_showEmojiPopup(enabled);
AyuSettings::save();
},
container->lifetime());
}
void AyuChats::setupContent(not_null<Window::SessionController*> controller) {
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
AddSkip(content);
SetupStickersAndEmojiSettings(content);
SetupRecentStickersLimit(content);
SetupGroupsAndChannels(content, controller);
SetupMarks(content);
SetupWideMessagesMultiplier(content, controller);
SetupContextMenuElements(content, controller);
SetupMessageFieldElements(content);
SetupMessageFieldPopups(content);
AddSkip(content);
ResizeFitChild(this, content);
}
} // namespace Settings

View file

@ -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 AyuChats : public Section<AyuChats>
{
public:
AyuChats(QWidget *parent, not_null<Window::SessionController*> controller);
[[nodiscard]] rpl::producer<QString> title() override;
private:
void setupContent(not_null<Window::SessionController*> controller);
};
} // namespace Settings

View file

@ -0,0 +1,329 @@
// 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_general.h"
#include "lang_auto.h"
#include "settings_ayu_utils.h"
#include "ayu/ayu_settings.h"
#include "settings/settings_common.h"
#include "styles/style_settings.h"
#include "ui/vertical_list.h"
#include "ui/boxes/single_choice_box.h"
#include "ui/widgets/buttons.h"
#include "ui/wrap/vertical_layout.h"
#include "window/window_session_controller.h"
namespace Settings {
rpl::producer<QString> AyuGeneral::title() {
return tr::ayu_CategoryGeneral();
}
AyuGeneral::AyuGeneral(
QWidget *parent,
not_null<Window::SessionController*> controller)
: Section(parent) {
setupContent(controller);
}
void SetupTranslator(not_null<Ui::VerticalLayout*> container,
not_null<Window::SessionController*> controller) {
auto *settings = &AyuSettings::getInstance();
AddSubsectionTitle(container, tr::lng_translate_settings_subtitle()/*rpl::single(QString("Translate Messages"))*/);
const auto options = std::vector{
QString("Telegram"),
QString("Google"),
QString("Yandex")
};
const auto getIndex = [=](const QString &val)
{
if (val == "telegram") {
return 0;
}
if (val == "google") {
return 1;
}
if (val == "yandex") {
return 2;
}
return 0;
};
auto currentVal = AyuSettings::get_translationProviderReactive() | rpl::map(getIndex) | rpl::map(
[=](int val)
{
return options[val];
});
const auto button = AddButtonWithLabel(
container,
tr::ayu_TranslationProvider(),
currentVal,
st::settingsButtonNoIcon);
button->addClickHandler(
[=]
{
controller->show(Box(
[=](not_null<Ui::GenericBox*> box)
{
const auto save = [=](int index)
{
const auto provider = (index == 0) ? "telegram" : (index == 1) ? "google" : "yandex";
AyuSettings::set_translationProvider(provider);
AyuSettings::save();
};
SingleChoiceBox(box,
{
.title = tr::ayu_TranslationProvider(),
.options = options,
.initialSelection = getIndex(settings->translationProvider),
.callback = save,
});
}));
});
}
void SetupShowPeerId(not_null<Ui::VerticalLayout*> container,
not_null<Window::SessionController*> 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<Ui::GenericBox*> 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 SetupQoLToggles(not_null<Ui::VerticalLayout*> container, not_null<Window::SessionController*> controller) {
auto *settings = &AyuSettings::getInstance();
SetupTranslator(container, controller);
AddSkip(container);
AddDivider(container);
AddSkip(container);
AddSubsectionTitle(container, tr::ayu_CategoryGeneral());
AddButtonWithIcon(
container,
tr::ayu_DisableStories(),
st::settingsButtonNoIcon
)->toggleOn(
rpl::single(settings->disableStories)
)->toggledValue(
) | rpl::filter(
[=](bool enabled)
{
return (enabled != settings->disableStories);
}) | rpl::start_with_next(
[=](bool enabled)
{
AyuSettings::set_disableStories(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_SettingsShowMessageSeconds(),
st::settingsButtonNoIcon
)->toggleOn(
rpl::single(settings->showMessageSeconds)
)->toggledValue(
) | rpl::filter(
[=](bool enabled)
{
return (enabled != settings->showMessageSeconds);
}) | start_with_next(
[=](bool enabled)
{
AyuSettings::set_showMessageSeconds(enabled);
AyuSettings::save();
},
container->lifetime());
SetupShowPeerId(container, controller);
AddSkip(container);
AddDivider(container);
AddSkip(container);
AddSubsectionTitle(container, rpl::single(QString("Webview")));
AddButtonWithIcon(
container,
tr::ayu_SettingsSpoofWebviewAsAndroid(),
st::settingsButtonNoIcon
)->toggleOn(
rpl::single(settings->spoofWebviewAsAndroid)
)->toggledValue(
) | rpl::filter(
[=](bool enabled)
{
return (enabled != settings->spoofWebviewAsAndroid);
}) | start_with_next(
[=](bool enabled)
{
AyuSettings::set_spoofWebviewAsAndroid(enabled);
AyuSettings::save();
},
container->lifetime());
std::vector webviewCheckboxes = {
NestedEntry{
tr::ayu_SettingsIncreaseWebviewHeight(tr::now), settings->increaseWebviewHeight, [=](bool enabled)
{
AyuSettings::set_increaseWebviewHeight(enabled);
AyuSettings::save();
}
},
NestedEntry{
tr::ayu_SettingsIncreaseWebviewWidth(tr::now), settings->increaseWebviewWidth, [=](bool enabled)
{
AyuSettings::set_increaseWebviewWidth(enabled);
AyuSettings::save();
}
}
};
AddCollapsibleToggle(container, tr::ayu_SettingsBiggerWindow(), webviewCheckboxes, false);
AddSkip(container);
AddDivider(container);
AddSkip(container);
// todo: move into a single checkbox with dropdown
AddSubsectionTitle(container, tr::ayu_ConfirmationsTitle());
AddButtonWithIcon(
container,
tr::ayu_StickerConfirmation(),
st::settingsButtonNoIcon
)->toggleOn(
rpl::single(settings->stickerConfirmation)
)->toggledValue(
) | rpl::filter(
[=](bool enabled)
{
return (enabled != settings->stickerConfirmation);
}) | start_with_next(
[=](bool enabled)
{
AyuSettings::set_stickerConfirmation(enabled);
AyuSettings::save();
},
container->lifetime());
AddButtonWithIcon(
container,
tr::ayu_GIFConfirmation(),
st::settingsButtonNoIcon
)->toggleOn(
rpl::single(settings->gifConfirmation)
)->toggledValue(
) | rpl::filter(
[=](bool enabled)
{
return (enabled != settings->gifConfirmation);
}) | start_with_next(
[=](bool enabled)
{
AyuSettings::set_gifConfirmation(enabled);
AyuSettings::save();
},
container->lifetime());
AddButtonWithIcon(
container,
tr::ayu_VoiceConfirmation(),
st::settingsButtonNoIcon
)->toggleOn(
rpl::single(settings->voiceConfirmation)
)->toggledValue(
) | rpl::filter(
[=](bool enabled)
{
return (enabled != settings->voiceConfirmation);
}) | start_with_next(
[=](bool enabled)
{
AyuSettings::set_voiceConfirmation(enabled);
AyuSettings::save();
},
container->lifetime());
}
void AyuGeneral::setupContent(not_null<Window::SessionController*> controller) {
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
AddSkip(content);
SetupQoLToggles(content, controller);
AddSkip(content);
ResizeFitChild(this, content);
}
} // namespace Settings

View file

@ -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 AyuGeneral : public Section<AyuGeneral>
{
public:
AyuGeneral(QWidget *parent, not_null<Window::SessionController*> controller);
[[nodiscard]] rpl::producer<QString> title() override;
private:
void setupContent(not_null<Window::SessionController*> controller);
};
} // namespace Settings

View file

@ -0,0 +1,224 @@
// 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_main.h"
#include <QDesktopServices>
#include <styles/style_ayu_icons.h>
#include "lang_auto.h"
#include "settings_appearance.h"
#include "settings_ayu.h"
#include "settings_chats.h"
#include "settings_general.h"
#include "settings_other.h"
#include "ayu/ayu_settings.h"
#include "ayu/ui/ayu_logo.h"
#include "core/version.h"
#include "settings/settings_common.h"
#include "styles/style_ayu_settings.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
#include "styles/style_settings.h"
#include "ui/painter.h"
#include "ui/vertical_list.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/wrap/vertical_layout.h"
#include "window/window_session_controller.h"
#include "window/window_session_controller_link_info.h"
namespace Settings {
rpl::producer<QString> AyuMain::title() {
return rpl::single(QString(""));
}
AyuMain::AyuMain(
QWidget *parent,
not_null<Window::SessionController*> controller)
: Section(parent) {
setupContent(controller);
}
void SetupAppLogo(not_null<Ui::VerticalLayout*> container) {
const auto logo = container->add(
object_ptr<Ui::CenterWrap<Ui::RpWidget>>(
container,
object_ptr<Ui::RpWidget>(container)));
const auto widget = logo->entity();
widget->resize(QSize(st::settingsCloudPasswordIconSize, st::settingsCloudPasswordIconSize));
widget->paintRequest(
) | rpl::start_with_next([=](QRect clip)
{
auto p = QPainter(widget);
const auto image = AyuAssets::currentAppLogoNoMargin(); // todo: svg renderer
if (!image.isNull()) {
const auto size = st::settingsCloudPasswordIconSize;
const auto scaled = image.scaled(
size * style::DevicePixelRatio(),
size * style::DevicePixelRatio(),
Qt::KeepAspectRatio,
Qt::SmoothTransformation);
p.drawImage(
QRect(0, 0, size, size),
scaled);
}
},
widget->lifetime());
}
void SetupCategories(
not_null<Ui::VerticalLayout*> container,
not_null<Window::SessionController*> controller,
Fn<void(Type)> showOther) {
struct CategoryInfo
{
QString name;
const style::icon *icon;
std::function<void()> handler;
};
const auto categories = std::vector<CategoryInfo>{
{QString("AyuGram"), &st::menuIconGroupReactions, [=] { showOther(AyuGhost::Id()); }},
// {QString("Filters"), &st::menuIconTagFilter, [=] { showOther(AyuFilters::Id()); }},
{tr::ayu_CategoryGeneral(tr::now), &st::menuIconShowAll, [=] { showOther(AyuGeneral::Id()); }},
{tr::ayu_CategoryAppearance(tr::now), &st::menuIconPalette, [=] { showOther(AyuAppearance::Id()); }},
{tr::ayu_CategoryChats(tr::now), &st::menuIconChatBubble, [=] { showOther(AyuChats::Id()); }},
{tr::ayu_CategoryOther(tr::now), &st::menuIconFave, [=] { showOther(AyuOther::Id()); }},
};
for (const auto &category : categories) {
AddButtonWithIcon(
container,
rpl::single(category.name),
st::settingsButton,
{category.icon}
)->setClickedCallback([=]
{
if (category.handler) {
category.handler();
}
});
}
}
void SetupLinks(
not_null<Ui::VerticalLayout*> container,
not_null<Window::SessionController*> controller) {
struct LinkInfo
{
QString name;
QString value;
const style::icon *icon;
std::function<void()> handler;
};
const auto links = std::vector<LinkInfo>{
{
tr::ayu_LinksChannel(tr::now),
QString("@ayugram"),
&st::menuIconChannel,
[=]
{
controller->showPeerByLink(Window::PeerByLinkInfo{
.usernameOrId = QString("ayugram"),
});
}
},
{
tr::ayu_LinksChats(tr::now),
QString("@ayugramchat"),
&st::menuIconChats,
[=]
{
controller->showPeerByLink(Window::PeerByLinkInfo{
.usernameOrId = QString("ayugramchat"),
});
}
},
{
tr::ayu_LinksTranslate(tr::now),
QString("Crowdin"),
&st::menuIconTranslate,
[=]
{
QDesktopServices::openUrl(QString("https://translate.ayugram.one"));
}
},
{
tr::ayu_LinksDocumentation(tr::now),
QString("docs.ayugram.one"),
&st::menuIconIpAddress,
[=]
{
QDesktopServices::openUrl(QString("https://docs.ayugram.one"));
}
},
};
for (const auto &link : links) {
AddButtonWithLabel(
container,
rpl::single(link.name),
rpl::single(link.value),
st::settingsButton,
{link.icon}
)->setClickedCallback(link.handler);
}
}
void AyuMain::setupContent(not_null<Window::SessionController*> controller) {
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
SetupAppLogo(content);
AddSkip(content);
content->add(
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
content,
object_ptr<Ui::FlatLabel>(
content,
rpl::single(QString("AyuGram Desktop v") + QString::fromLatin1(AppVersionStr)),
st::boxTitle)));
AddSkip(content);
content->add(
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
content,
object_ptr<Ui::FlatLabel>(
content,
tr::ayu_SettingsDescription(),
st::centeredBoxLabel)));
AddSkip(content);
AddSkip(content);
AddSkip(content);
AddSkip(content);
AddDivider(content);
AddSkip(content);
AddSubsectionTitle(content, tr::ayu_CategoriesHeader());
SetupCategories(content, controller, showOtherMethod());
AddSkip(content);
AddDivider(content);
AddSkip(content);
AddSubsectionTitle(content, tr::ayu_LinksHeader());
SetupLinks(content, controller);
AddSkip(content);
ResizeFitChild(this, content);
}
} // namespace Settings

View file

@ -0,0 +1,32 @@
// 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"
class BoxContent;
namespace Window {
class Controller;
class SessionController;
}
namespace Settings {
class AyuMain : public Section<AyuMain>
{
public:
AyuMain(QWidget *parent, not_null<Window::SessionController*> controller);
[[nodiscard]] rpl::producer<QString> title() override;
private:
void setupContent(not_null<Window::SessionController*> controller);
};
}

View file

@ -0,0 +1,250 @@
// 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_other.h"
#include <QDesktopServices>
#include <QGuiApplication>
#include <QSvgRenderer>
#include "lang_auto.h"
#include "ayu/ayu_settings.h"
#include "ayu/ui/boxes/donate_qr_box.h"
#include "boxes/abstract_box.h"
#include "core/application.h"
#include "lang/lang_text_entity.h"
#include "main/main_session.h"
#include "settings/settings_common.h"
#include "styles/style_menu_icons.h"
#include "styles/style_settings.h"
#include "ui/integration.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/vertical_list.h"
#include "ui/boxes/confirm_box.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/wrap/vertical_layout.h"
#include "window/window_session_controller.h"
#include "window/themes/window_theme.h"
namespace Settings {
namespace {
struct Asset
{
QString icon;
QColor background;
};
Asset getAsset(const QString &name) {
const auto isNightMode = Window::Theme::IsNightMode();
const auto normalized = name.toLower();
QString icon = QString(":/gui/icons/ayu/donates/%1.svg").arg(normalized);
QColor background = isNightMode ? QColor(0xEEEEEE) : QColor(0x242B2C);
return {
.icon = std::move(icon),
.background = std::move(background)
};
}
QImage getImage(const QString &name) {
const auto iconData = getAsset(name);
const auto factor = style::DevicePixelRatio();
const auto size = st::menuIconLink.size();
auto image = QImage(
size * factor,
QImage::Format_ARGB32_Premultiplied);
image.setDevicePixelRatio(factor);
image.fill(Qt::transparent);
{
auto p = QPainter(&image);
auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen);
p.setBrush(iconData.background);
p.drawRoundedRect(Rect(size), size.width() / 4., size.height() / 4.);
p.setBrush(Qt::transparent);
auto svgIcon = QSvgRenderer(iconData.icon);
svgIcon.render(&p, Rect(size));
}
return image;
}
[[nodiscard]] not_null<Ui::SettingsButton*> AddDonate(not_null<Ui::SettingsButton*> button, const QString &name) {
const auto btnContainer = Ui::CreateChild<Ui::RpWidget>(button);
const auto &buttonSt = button->st();
const auto fullHeight = buttonSt.height
+ rect::m::sum::v(buttonSt.padding);
const auto iconWidget = Ui::CreateChild<Ui::RpWidget>(button.get());
auto icon = getImage(name);
iconWidget->resize(icon.size() / style::DevicePixelRatio());
iconWidget->paintRequest(
) | rpl::start_with_next([=]
{
auto p = QPainter(iconWidget);
p.drawImage(0, 0, icon);
},
iconWidget->lifetime());
button->sizeValue(
) | rpl::start_with_next([=](const QSize &s)
{
iconWidget->moveToLeft(
button->st().iconLeft
+ (st::menuIconShop.width() - iconWidget->width()) / 2,
(s.height() - iconWidget->height()) / 2);
btnContainer->moveToLeft(
iconWidget->x() - (fullHeight - iconWidget->height()) / 2,
0);
},
iconWidget->lifetime());
btnContainer->resize(fullHeight, fullHeight);
return button;
}
void AddCryptoDonate(const QString &name, const QString &address, not_null<Ui::VerticalLayout*> container) {
const auto button = AddDonate(
AddButtonWithIcon(
container,
rpl::single(name),
st::settingsButton),
name);
button->setClickedCallback([=]
{
auto box = Box(
Ui::FillDonateQrBox,
address,
getAsset(name).icon);
Ui::show(std::move(box));
});
}
}
rpl::producer<QString> AyuOther::title() {
return tr::ayu_CategoryOther();
}
AyuOther::AyuOther(
QWidget *parent,
not_null<Window::SessionController*> controller)
: Section(parent) {
setupContent(controller);
}
void SetupDonations(not_null<Ui::VerticalLayout*> container, not_null<Window::SessionController*> controller) {
AddSubsectionTitle(container, tr::ayu_SupportHeader());
AddDonate(
AddButtonWithIcon(
container,
rpl::single(QString("Boosty")),
st::settingsButton),
"boosty"
)->setClickedCallback([=]
{
QDesktopServices::openUrl(QString("https://boosty.to/alexeyzavar"));
});
AddCryptoDonate("TON", QString("UQA4i8U8vP3mYUZSV3KqDQEHPwmhninEqCkkKc7BITQ652de"), container);
AddCryptoDonate("Bitcoin", QString("bc1qdk6qq4mzq5yap3fpy0qau3246w3m3uwac9f0xd"), container);
AddCryptoDonate("Ethereum", QString("0x405589857C8DFAb45B2027c68ad1e58877FDa347"), container);
AddCryptoDonate("Solana", QString("8ZHQpPxpsdRjsWoBcF1dmvRM5dB6zEhJ3jMBFZjYfyHs"), container);
AddCryptoDonate("Tron", QString("TRpbajq38qU8joThgAfKJLyEPbNjzsdPJ1"), container);
AddSkip(container);
AddDividerText(container,
tr::ayu_SupportDescription2(
lt_item,
rpl::single(
Ui::Text::Link(tr::ayu_SupportDescription1(tr::now), QString("tg://support"))
),
Ui::Text::WithEntities
)
);
}
void SetupCrashReporting(not_null<Ui::VerticalLayout*> container) {
auto *settings = &AyuSettings::getInstance();
AddSkip(container);
AddSubsectionTitle(container, tr::ayu_CategoryOther());
AddButtonWithIcon(
container,
tr::ayu_CrashReporting(),
st::settingsButton,
{&st::menuIconReport}
)->toggleOn(
rpl::single(settings->crashReporting)
)->toggledValue(
) | rpl::filter(
[=](bool enabled)
{
return (enabled != settings->crashReporting);
}) | start_with_next(
[=](bool enabled)
{
AyuSettings::set_crashReporting(enabled);
AyuSettings::save();
},
container->lifetime());
AddSkip(container);
AddDividerText(container, tr::ayu_CrashReportingDescription());
}
void SetupOtherThings(not_null<Ui::VerticalLayout*> container, not_null<Window::SessionController*> controller) {
AddSkip(container);
AddButtonWithIcon(
container,
tr::ayu_RegisterURLScheme(),
st::settingsButton,
{&st::menuIconLink}
)->setClickedCallback([=]
{
Core::Application::RegisterUrlScheme();
controller->showToast(tr::lng_box_done(tr::now));
});
AddButtonWithIcon(
container,
tr::ayu_ResetSettings(),
st::settingsButton,
{&st::menuIconRestore}
)->setClickedCallback([=]
{
controller->show(Ui::MakeConfirmBox({
.text = tr::ayu_ResetSettingsConfirmation(Ui::Text::RichLangValue),
.confirmed = [=](Fn<void()> &&close)
{
AyuSettings::reset();
close();
},
.confirmText = tr::lng_box_yes(),
}));
});
AddSkip(container);
}
void AyuOther::setupContent(not_null<Window::SessionController*> controller) {
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
AddSkip(content);
SetupDonations(content, controller);
#ifndef TDESKTOP_DISABLE_AUTOUPDATE
SetupCrashReporting(content);
#endif
SetupOtherThings(content, controller);
ResizeFitChild(this, content);
}
} // namespace Settings

View file

@ -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 AyuOther : public Section<AyuOther>
{
public:
AyuOther(QWidget *parent, not_null<Window::SessionController*> controller);
[[nodiscard]] rpl::producer<QString> title() override;
private:
void setupContent(not_null<Window::SessionController*> controller);
};
} // namespace Settings

View file

@ -12,7 +12,7 @@
constexpr auto kMaxChannelId = -1000000000000; constexpr auto kMaxChannelId = -1000000000000;
QString IDString(not_null<PeerData*> peer) { QString IDString(const not_null<PeerData*> peer) {
auto resultId = QString::number(getBareID(peer)); auto resultId = QString::number(getBareID(peer));
const auto &settings = AyuSettings::getInstance(); const auto &settings = AyuSettings::getInstance();
@ -27,10 +27,8 @@ QString IDString(not_null<PeerData*> peer) {
return resultId; return resultId;
} }
QString IDString(MsgId topic_root_id) { QString IDString(MsgId topicRootId) {
auto resultId = QString::number(topic_root_id.bare); return QString::number(topicRootId.bare);
return resultId;
} }
rpl::producer<TextWithEntities> IDValue(not_null<PeerData*> peer) { rpl::producer<TextWithEntities> IDValue(not_null<PeerData*> peer) {

View file

@ -7,7 +7,7 @@
#pragma once #pragma once
QString IDString(not_null<PeerData*> peer); QString IDString(not_null<PeerData*> peer);
QString IDString(MsgId topic_root_id); QString IDString(MsgId topicRootId);
rpl::producer<TextWithEntities> IDValue(not_null<PeerData*> peer); rpl::producer<TextWithEntities> IDValue(not_null<PeerData*> peer);
rpl::producer<TextWithEntities> IDValue(MsgId topicRootId); rpl::producer<TextWithEntities> IDValue(MsgId topicRootId);

View file

@ -51,7 +51,7 @@ void RCManager::makeRequest() {
clearSentRequest(); clearSentRequest();
const auto request = QNetworkRequest(QUrl("https://update.ayugram.one/rc/current/desktop")); const auto request = QNetworkRequest(QUrl("https://update.ayugram.one/rc/current/desktop2"));
_reply = _manager->get(request); _reply = _manager->get(request);
connect(_reply, connect(_reply,
&QNetworkReply::finished, &QNetworkReply::finished,
@ -105,12 +105,16 @@ bool RCManager::applyResponse(const QByteArray &response) {
const auto root = document.object(); const auto root = document.object();
const auto developers = root.value("developers").toArray(); const auto developers = root.value("developers").toArray();
const auto channels = root.value("channels").toArray(); const auto officialChannels = root.value("officialChannels").toArray();
const auto supporters = root.value("supporters").toArray(); const auto supporters = root.value("supporters").toArray();
const auto supporterChannels = root.value("supporterChannels").toArray();
const auto customBadges = root.value("customBadges").toArray();
_developers.clear(); _developers.clear();
_channels.clear(); _officialChannels.clear();
_supporters.clear(); _supporters.clear();
_supporterChannels.clear();
_customBadges.clear();
for (const auto &developer : developers) { for (const auto &developer : developers) {
if (const auto id = developer.toVariant().toLongLong()) { if (const auto id = developer.toVariant().toLongLong()) {
@ -118,9 +122,9 @@ bool RCManager::applyResponse(const QByteArray &response) {
} }
} }
for (const auto &channel : channels) { for (const auto &channel : officialChannels) {
if (const auto id = channel.toVariant().toLongLong()) { if (const auto id = channel.toVariant().toLongLong()) {
_channels.insert(id); _officialChannels.insert(id);
} }
} }
@ -130,10 +134,47 @@ bool RCManager::applyResponse(const QByteArray &response) {
} }
} }
for (const auto &channel : supporterChannels) {
if (const auto id = channel.toVariant().toLongLong()) {
_supporterChannels.insert(id);
}
}
for (const auto &badge : customBadges) {
if (!badge.isObject()) {
continue;
}
const auto obj = badge.toObject();
const auto id = obj.value("id").toVariant().toLongLong();
if (!id) {
continue;
}
const auto badgeObj = obj.value("badge");
if (!badgeObj.isObject()) {
continue;
}
const auto badgeData = badgeObj.toObject();
CustomBadge customBadge;
if (const auto emojiStatusId = badgeData.value("documentId").toVariant().toLongLong()) {
customBadge.emojiStatusId = EmojiStatusId(emojiStatusId);
} else {
continue;
}
if (const auto text = badgeData.value("text").toString(); !text.isEmpty()) {
customBadge.text = text;
}
_customBadges[id] = customBadge;
}
_donateUsername = root.value("donateUsername").toString();
_donateAmountUsd = root.value("donateAmountUsd").toString();
_donateAmountTon = root.value("donateAmountTon").toString();
_donateAmountRub = root.value("donateAmountRub").toString();
initialized = true; initialized = true;
LOG(("RCManager: Loaded %1 developers, %2 channels" LOG(("RCManager: Loaded %1 developers, %2 official channels"
).arg(_developers.size()).arg(_channels.size())); ).arg(_developers.size()).arg(_officialChannels.size()));
return true; return true;
} }

View file

@ -13,6 +13,12 @@
extern std::unordered_set<ID> default_developers; extern std::unordered_set<ID> default_developers;
extern std::unordered_set<ID> default_channels; extern std::unordered_set<ID> default_channels;
struct CustomBadge
{
EmojiStatusId emojiStatusId;
QString text;
};
class RCManager final : public QObject class RCManager final : public QObject
{ {
Q_OBJECT Q_OBJECT
@ -40,13 +46,37 @@ public:
if (!initialized) { if (!initialized) {
return default_channels; return default_channels;
} }
return _channels; return _officialChannels;
} }
[[nodiscard]] const std::unordered_set<ID> &supporters() const { [[nodiscard]] const std::unordered_set<ID> &supporters() const {
return _supporters; return _supporters;
} }
[[nodiscard]] const std::unordered_set<ID> &supporterChannels() const {
return _supporterChannels;
}
[[nodiscard]] const std::unordered_map<ID, CustomBadge> &supporterCustomBadges() const {
return _customBadges;
}
[[nodiscard]] QString donateUsername() const {
return _donateUsername;
}
[[nodiscard]] QString donateAmountUsd() const {
return _donateAmountUsd;
}
[[nodiscard]] QString donateAmountTon() const {
return _donateAmountTon;
}
[[nodiscard]] QString donateAmountRub() const {
return _donateAmountRub;
}
private: private:
RCManager() = default; RCManager() = default;
~RCManager(); ~RCManager();
@ -62,8 +92,15 @@ private:
bool initialized = false; bool initialized = false;
std::unordered_set<ID> _developers = {}; std::unordered_set<ID> _developers = {};
std::unordered_set<ID> _channels = {}; std::unordered_set<ID> _officialChannels = {};
std::unordered_set<ID> _supporters = {}; std::unordered_set<ID> _supporters = {};
std::unordered_set<ID> _supporterChannels = {};
std::unordered_map<ID, CustomBadge> _customBadges = {};
QString _donateUsername = QString("@ayugramOwner");
QString _donateAmountUsd = QString("4.00");
QString _donateAmountTon = QString("1.30");
QString _donateAmountRub = QString("300");
QTimer* _timer = nullptr; QTimer* _timer = nullptr;

View file

@ -92,14 +92,35 @@ bool isExteraPeer(ID peerId) {
} }
bool isSupporterPeer(ID peerId) { bool isSupporterPeer(ID peerId) {
return RCManager::getInstance().supporters().contains(peerId); return RCManager::getInstance().supporters().contains(peerId) || RCManager::getInstance().supporterChannels().contains(peerId);
}
bool isCustomBadgePeer(ID peerId) {
return RCManager::getInstance().supporterCustomBadges().contains(peerId);
}
CustomBadge getCustomBadge(ID peerId) {
const auto &badges = RCManager::getInstance().supporterCustomBadges();
if (const auto it = badges.find(peerId); it != badges.end()) {
return it->second;
}
return {};
} }
rpl::producer<Info::Profile::Badge::Content> ExteraBadgeTypeFromPeer(not_null<PeerData*> peer) { rpl::producer<Info::Profile::Badge::Content> ExteraBadgeTypeFromPeer(not_null<PeerData*> peer) {
if (isExteraPeer(getBareID(peer))) { if (isCustomBadgePeer(getBareID(peer))) {
return rpl::single(Info::Profile::Badge::Content{Info::Profile::BadgeType::Extera }); return rpl::single(Info::Profile::Badge::Content{
.badge = Info::Profile::BadgeType::ExteraCustom,
.emojiStatusId = getCustomBadge(getBareID(peer)).emojiStatusId
});
} else if (isExteraPeer(getBareID(peer))) {
return rpl::single(Info::Profile::Badge::Content{
.badge = Info::Profile::BadgeType::Extera
});
} else if (isSupporterPeer(getBareID(peer))) { } else if (isSupporterPeer(getBareID(peer))) {
return rpl::single(Info::Profile::Badge::Content{Info::Profile::BadgeType::ExteraSupporter }); return rpl::single(Info::Profile::Badge::Content{
.badge = Info::Profile::BadgeType::ExteraSupporter
});
} }
return rpl::single(Info::Profile::Badge::Content{Info::Profile::BadgeType::None}); return rpl::single(Info::Profile::Badge::Content{Info::Profile::BadgeType::None});
} }

View file

@ -6,6 +6,7 @@
// Copyright @Radolyn, 2025 // Copyright @Radolyn, 2025
#pragma once #pragma once
#include "rc_manager.h"
#include "ayu/data/entities.h" #include "ayu/data/entities.h"
#include "core/application.h" #include "core/application.h"
@ -25,6 +26,8 @@ ID getBareID(not_null<PeerData*> peer);
bool isExteraPeer(ID peerId); bool isExteraPeer(ID peerId);
bool isSupporterPeer(ID peerId); bool isSupporterPeer(ID peerId);
bool isCustomBadgePeer(ID peerId);
CustomBadge getCustomBadge(ID peerId);
rpl::producer<Info::Profile::Badge::Content> ExteraBadgeTypeFromPeer(not_null<PeerData*> peer); rpl::producer<Info::Profile::Badge::Content> ExteraBadgeTypeFromPeer(not_null<PeerData*> peer);

View file

@ -51,6 +51,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <QtGui/QGuiApplication> #include <QtGui/QGuiApplication>
#include <QtGui/QClipboard> #include <QtGui/QClipboard>
// AyuGram includes
#include "ayu/ayu_settings.h"
namespace { namespace {
using Language = Lang::Language; using Language = Lang::Language;
@ -1196,7 +1200,15 @@ void LanguageBox::setupTop(not_null<Ui::VerticalLayout*> container) {
}, translateEnabled->lifetime()); }, translateEnabled->lifetime());
using namespace rpl::mappers; using namespace rpl::mappers;
auto premium = Data::AmPremiumValue(&_controller->session()); auto premium = Data::AmPremiumValue(&_controller->session()) | rpl::map([=](bool val)
{
// const auto &settings = AyuSettings::getInstance();
// if (settings.translationProvider != "telegram") {
// return true;
// }
// return val;
return true;
});
const auto translateChat = container->add(object_ptr<Ui::SettingsButton>( const auto translateChat = container->add(object_ptr<Ui::SettingsButton>(
container, container,
tr::lng_translate_settings_chat(), tr::lng_translate_settings_chat(),

View file

@ -38,6 +38,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <QLocale> #include <QLocale>
// AyuGram includes
#include "ayu/features/translator/ayu_translator.h"
namespace Ui { namespace Ui {
namespace { namespace {
@ -226,7 +230,8 @@ void TranslateBox(
const auto send = [=](LanguageId to) { const auto send = [=](LanguageId to) {
loading->show(anim::type::instant); loading->show(anim::type::instant);
translated->hide(anim::type::instant); translated->hide(anim::type::instant);
state->api.request(MTPmessages_TranslateText( Ayu::Translator::TranslateManager::currentInstance()->request(
peer->session(),
MTP_flags(flags), MTP_flags(flags),
msgId ? peer->input : MTP_inputPeerEmpty(), msgId ? peer->input : MTP_inputPeerEmpty(),
(msgId (msgId
@ -241,7 +246,7 @@ void TranslateBox(
text.entities, text.entities,
Api::ConvertOption::SkipLocal)))), Api::ConvertOption::SkipLocal)))),
MTP_string(to.twoLetterCode()) MTP_string(to.twoLetterCode())
)).done([=](const MTPmessages_TranslatedText &result) { ).done([=](const MTPmessages_TranslatedText &result) {
const auto &data = result.data(); const auto &data = result.data();
const auto &list = data.vresult().v; const auto &list = data.vresult().v;
if (list.isEmpty()) { if (list.isEmpty()) {

View file

@ -23,6 +23,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <QtCore/QStandardPaths> #include <QtCore/QStandardPaths>
#include <QtCore/QTimer> #include <QtCore/QTimer>
// AyuGram includes
#include "ayu/ayu_settings.h"
namespace { namespace {
constexpr auto kDefaultProxyPort = 80; constexpr auto kDefaultProxyPort = 80;
@ -273,11 +277,11 @@ LastCrashedWindow::LastCrashedWindow(
excludeReportUsername(); excludeReportUsername();
#ifndef TDESKTOP_DISABLE_AUTOUPDATE #ifndef TDESKTOP_DISABLE_AUTOUPDATE
if (false) { const auto &settings = AyuSettings::getInstance();
if (!settings.crashReporting) {
#else #else
if (true) { if (true) {
#endif #endif
// Currently accept crash reports only from testers.
_sendingState = SendingNoReport; _sendingState = SendingNoReport;
} else if (Core::OpenGLLastCheckFailed()) { } else if (Core::OpenGLLastCheckFailed()) {
// Nothing we can do right now with graphics driver crashes in GL. // Nothing we can do right now with graphics driver crashes in GL.

View file

@ -1643,9 +1643,13 @@ const std::vector<LocalUrlHandler> &LocalUrlHandlers() {
AyuUrlHandlers::ResolveUser AyuUrlHandlers::ResolveUser
}, },
{ {
u"^ayu/?(.+)?(#|$)"_q, u"^ayu(/?.+)?(#|$)"_q,
AyuUrlHandlers::HandleAyu AyuUrlHandlers::HandleAyu
}, },
{
u"^support$"_q,
AyuUrlHandlers::HandleSupport
},
{ {
u"^([^\\?]+)(\\?|#|$)"_q, u"^([^\\?]+)(\\?|#|$)"_q,
HandleUnknown HandleUnknown

View file

@ -53,6 +53,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_dialogs.h" #include "styles/style_dialogs.h"
// AyuGram includes // AyuGram includes
#include "ayu/ayu_settings.h"
#include "ayu/features/messageshot/message_shot.h" #include "ayu/features/messageshot/message_shot.h"
#include "styles/style_ayu_icons.h" #include "styles/style_ayu_icons.h"
@ -4042,6 +4043,11 @@ std::optional<QSize> Message::rightActionSize() const {
} }
bool Message::displayFastShare() const { bool Message::displayFastShare() const {
const auto &settings = AyuSettings::getInstance();
if (settings.hideFastShare) {
return false;
}
const auto item = data(); const auto item = data();
const auto peer = item->history()->peer; const auto peer = item->history()->peer;
if (!item->allowsForward()) { if (!item->allowsForward()) {

View file

@ -1270,6 +1270,10 @@ void TopBarWidget::updateControlsVisibility() {
const auto showRecentActions = [&] const auto showRecentActions = [&]
{ {
const auto &settings = AyuSettings::getInstance();
if (!settings.quickAdminShortcuts) {
return false;
}
if (_activeChat.section == Section::ChatsList) { if (_activeChat.section == Section::ChatsList) {
return false; return false;
} }
@ -1284,6 +1288,10 @@ void TopBarWidget::updateControlsVisibility() {
_recentActions->setVisible(showRecentActions); _recentActions->setVisible(showRecentActions);
const auto showAdmins = [&] const auto showAdmins = [&]
{ {
const auto &settings = AyuSettings::getInstance();
if (!settings.quickAdminShortcuts) {
return false;
}
if (_activeChat.section == Section::ChatsList) { if (_activeChat.section == Section::ChatsList) {
return false; return false;
} }

View file

@ -23,6 +23,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_session.h" #include "main/main_session.h"
#include "spellcheck/platform/platform_language.h" #include "spellcheck/platform/platform_language.h"
// AyuGram includes
#include "ayu/features/translator/ayu_translator.h"
namespace HistoryView { namespace HistoryView {
namespace { namespace {
@ -240,7 +244,7 @@ void TranslateTracker::cancelSentRequest() {
item->translationShowRequiresRequest({}); item->translationShowRequiresRequest({});
} }
} }
_history->session().api().request(base::take(_requestId)).cancel(); Ayu::Translator::TranslateManager::currentInstance()->cancel(_requestId);
} }
} }
@ -277,13 +281,14 @@ void TranslateTracker::requestSome() {
} }
} }
using Flag = MTPmessages_TranslateText::Flag; using Flag = MTPmessages_TranslateText::Flag;
_requestId = session->api().request(MTPmessages_TranslateText( _requestId = Ayu::Translator::TranslateManager::currentInstance()->request(
peer->session(),
MTP_flags(Flag::f_peer | Flag::f_id), MTP_flags(Flag::f_peer | Flag::f_id),
peer->input, peer->input,
MTP_vector<MTPint>(list), MTP_vector<MTPint>(list),
MTPVector<MTPTextWithEntities>(), MTPVector<MTPTextWithEntities>(),
MTP_string(to.twoLetterCode()) MTP_string(to.twoLetterCode())
)).done([=](const MTPmessages_TranslatedText &result) { ).done([=](const MTPmessages_TranslatedText &result) {
requestDone(to, result.data().vresult().v); requestDone(to, result.data().vresult().v);
}).fail([=] { }).fail([=] {
requestDone(to, {}); requestDone(to, {});

View file

@ -53,9 +53,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_menu_icons.h" #include "styles/style_menu_icons.h"
#include "styles/style_layers.h" #include "styles/style_layers.h"
// AyuGram includes
#include "ayu/ui/settings/settings_ayu.h"
namespace Info { namespace Info {
namespace { namespace {

View file

@ -32,7 +32,8 @@ namespace {
return content.badge == BadgeType::Premium return content.badge == BadgeType::Premium
|| (content.badge == BadgeType::Verified && content.emojiStatusId) || (content.badge == BadgeType::Verified && content.emojiStatusId)
|| (content.badge == BadgeType::Extera) || (content.badge == BadgeType::Extera)
|| (content.badge == BadgeType::ExteraSupporter); || (content.badge == BadgeType::ExteraSupporter)
|| (content.badge == BadgeType::ExteraCustom);
} }
} // namespace } // namespace
@ -88,6 +89,7 @@ void Badge::setContent(Content content) {
_view.create(_parent); _view.create(_parent);
_view->show(); _view->show();
switch (_content.badge) { switch (_content.badge) {
case BadgeType::ExteraCustom:
case BadgeType::Verified: case BadgeType::Verified:
case BadgeType::BotVerified: case BadgeType::BotVerified:
case BadgeType::Premium: { case BadgeType::Premium: {

View file

@ -35,7 +35,7 @@ namespace Info::Profile {
class EmojiStatusPanel; class EmojiStatusPanel;
enum class BadgeType : uchar { enum class BadgeType : ushort {
None = 0x00, None = 0x00,
Verified = 0x01, Verified = 0x01,
BotVerified = 0x02, BotVerified = 0x02,
@ -44,7 +44,8 @@ enum class BadgeType : uchar {
Fake = 0x10, Fake = 0x10,
Direct = 0x20, Direct = 0x20,
Extera = 0x40, Extera = 0x40,
ExteraSupporter = 0x80, // todo: remove `uchar` if they add more badges ExteraSupporter = 0x80,
ExteraCustom = 0x100,
}; };
inline constexpr bool is_flag_type(BadgeType) { return true; } inline constexpr bool is_flag_type(BadgeType) { return true; }

View file

@ -685,17 +685,43 @@ Cover::Cover(
::Settings::ShowEmojiStatusPremium(_controller, _peer); ::Settings::ShowEmojiStatusPremium(_controller, _peer);
} }
}); });
if (_peer->isUser() && (isExteraPeer(getBareID(_peer)) || isSupporterPeer(getBareID(_peer)))) {
const auto isCustomBadge = isCustomBadgePeer(getBareID(_peer));
const auto isExtera = isExteraPeer(getBareID(_peer));
const auto isSupporter = isSupporterPeer(getBareID(_peer));
if (isExtera || isSupporter || isCustomBadge) {
_exteraBadge->setPremiumClickCallback([=] _exteraBadge->setPremiumClickCallback([=]
{ {
TextWithEntities text; TextWithEntities text;
if (isExteraPeer(getBareID(_peer))) { if (isCustomBadge) {
text = tr::ayu_DeveloperPopup( const auto custom = getCustomBadge(getBareID(_peer));
text = custom.text.isEmpty()
? (isExtera
? tr::ayu_DeveloperPopup(
tr::now,
lt_item,
TextWithEntities{_peer->name()},
Ui::Text::RichLangValue)
: tr::ayu_SupporterPopup(
tr::now,
lt_item,
TextWithEntities{_peer->name()},
Ui::Text::RichLangValue))
: Ui::Text::RichLangValue(custom.text);
} else if (isExtera) {
text = _peer->isUser()
? tr::ayu_DeveloperPopup(
tr::now,
lt_item,
TextWithEntities{_peer->name()},
Ui::Text::RichLangValue)
: tr::ayu_OfficialResourcePopup(
tr::now, tr::now,
lt_item, lt_item,
TextWithEntities{_peer->name()}, TextWithEntities{_peer->name()},
Ui::Text::RichLangValue); Ui::Text::RichLangValue);
} else if (isSupporterPeer(getBareID(_peer))) { } else if (isSupporter) {
text = tr::ayu_SupporterPopup( text = tr::ayu_SupporterPopup(
tr::now, tr::now,
lt_item, lt_item,

View file

@ -131,7 +131,7 @@ struct LinkWithUrl {
[[nodiscard]] rpl::producer<bool> CanViewParticipantsValue( [[nodiscard]] rpl::producer<bool> CanViewParticipantsValue(
not_null<ChannelData*> megagroup); not_null<ChannelData*> megagroup);
enum class BadgeType : uchar; enum class BadgeType : ushort;
[[nodiscard]] rpl::producer<BadgeType> BadgeValue(not_null<PeerData*> peer); [[nodiscard]] rpl::producer<BadgeType> BadgeValue(not_null<PeerData*> peer);
[[nodiscard]] rpl::producer<EmojiStatusId> EmojiStatusIdValue( [[nodiscard]] rpl::producer<EmojiStatusId> EmojiStatusIdValue(
not_null<PeerData*> peer); not_null<PeerData*> peer);

View file

@ -12,6 +12,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "settings/settings_information.h" #include "settings/settings_information.h"
#include "ui/ui_utility.h" #include "ui/ui_utility.h"
// AyuGram includes
#include "ayu/ui/settings/settings_main.h"
namespace Info { namespace Info {
namespace Settings { namespace Settings {
@ -227,7 +231,8 @@ const Ui::RoundRect *Widget::bottomSkipRounding() const {
rpl::producer<bool> Widget::desiredShadowVisibility() const { rpl::producer<bool> Widget::desiredShadowVisibility() const {
return (_type == ::Settings::Main::Id() return (_type == ::Settings::Main::Id()
|| _type == ::Settings::Information::Id()) || _type == ::Settings::Information::Id()
|| _type == ::Settings::AyuMain::Id())
? ContentWidget::desiredShadowVisibility() ? ContentWidget::desiredShadowVisibility()
: rpl::single(true); : rpl::single(true);
} }

View file

@ -83,7 +83,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_dialogs.h" #include "styles/style_dialogs.h"
// AyuGram includes // AyuGram includes
#include "ayu/ui/settings/settings_ayu.h"
#include "ayu/features/messageshot/message_shot.h" #include "ayu/features/messageshot/message_shot.h"
#include "window/themes/window_theme_preview.h" #include "window/themes/window_theme_preview.h"

View file

@ -11,17 +11,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "settings/settings_chat.h" #include "settings/settings_chat.h"
#include "settings/settings_main.h" #include "settings/settings_main.h"
// AyuGram includes
#include "ayu/ui/settings/settings_ayu.h"
namespace Settings { namespace Settings {
bool HasMenu(Type type) { bool HasMenu(Type type) {
return (type == ::Settings::CloudPasswordEmailConfirmId()) return (type == ::Settings::CloudPasswordEmailConfirmId())
|| (type == Main::Id()) || (type == Main::Id())
|| (type == Chat::Id()) || (type == Chat::Id());
|| (type == Ayu::Id());
} }
} // namespace Settings } // namespace Settings

View file

@ -84,7 +84,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <QtGui/QWindow> #include <QtGui/QWindow>
// AyuGram includes // AyuGram includes
#include "ayu/ui/settings/settings_ayu.h" #include "ayu/ui/settings/settings_main.h"
#include "ayu/ui/utils/ayu_profile_values.h" #include "ayu/ui/utils/ayu_profile_values.h"
#include "ayu/utils/telegram_helpers.h" #include "ayu/utils/telegram_helpers.h"
@ -657,7 +657,7 @@ void SetupSections(
Ui::AddSkip(container); Ui::AddSkip(container);
addSection( addSection(
tr::ayu_AyuPreferences(), tr::ayu_AyuPreferences(),
Ayu::Id(), AyuMain::Id(),
{ .icon = &st::menuIconPremium }); { .icon = &st::menuIconPremium });
Ui::AddSkip(container); Ui::AddSkip(container);
Ui::AddDivider(container); Ui::AddDivider(container);

View file

@ -17,7 +17,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
// AyuGram includes // AyuGram includes
#include "ayu/ayu_settings.h" #include "ayu/ayu_settings.h"
#include "ayu/ui/settings/settings_ayu.h"
#include "ayu/features/streamer_mode/streamer_mode.h" #include "ayu/features/streamer_mode/streamer_mode.h"
#include "lang_auto.h" #include "lang_auto.h"

View file

@ -12,6 +12,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/chat/chat_style.h" #include "ui/chat/chat_style.h"
#include "styles/style_chat.h" #include "styles/style_chat.h"
// AyuGram includes
#include "ayu/ayu_settings.h"
namespace Ui { namespace Ui {
namespace { namespace {
@ -32,8 +36,8 @@ void PaintBubbleGeneric(
const auto topLeft = args.rounding.topLeft; const auto topLeft = args.rounding.topLeft;
const auto topRight = args.rounding.topRight; const auto topRight = args.rounding.topRight;
const auto bottomWithTailLeft = args.rounding.bottomLeft; auto bottomWithTailLeft = args.rounding.bottomLeft;
const auto bottomWithTailRight = args.rounding.bottomRight; auto bottomWithTailRight = args.rounding.bottomRight;
if (topLeft == Corner::None if (topLeft == Corner::None
&& topRight == Corner::None && topRight == Corner::None
&& bottomWithTailLeft == Corner::None && bottomWithTailLeft == Corner::None
@ -41,6 +45,17 @@ void PaintBubbleGeneric(
fillBg(args.geometry); fillBg(args.geometry);
return; return;
} }
const auto &settings = AyuSettings::getInstance();
if (settings.removeMessageTail) {
if (bottomWithTailLeft == Corner::Tail) {
bottomWithTailLeft = Corner::Large;
}
if (bottomWithTailRight == Corner::Tail) {
bottomWithTailRight = Corner::Large;
}
}
const auto bottomLeft = (bottomWithTailLeft == Corner::Tail) const auto bottomLeft = (bottomWithTailLeft == Corner::Tail)
? Corner::None ? Corner::None
: bottomWithTailLeft; : bottomWithTailLeft;

View file

@ -86,8 +86,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ayu/features/streamer_mode/streamer_mode.h" #include "ayu/features/streamer_mode/streamer_mode.h"
#include "styles/style_ayu_icons.h" #include "styles/style_ayu_icons.h"
#include "lang_auto.h" #include "lang_auto.h"
#include "ayu/ui/settings/settings_ayu.h" #include "ayu/ui/settings/settings_main.h"
namespace Window { namespace Window {
namespace { namespace {
@ -624,7 +623,7 @@ void MainMenu::setupAccountsToggle() {
void MainMenu::setupSetEmojiStatus() { void MainMenu::setupSetEmojiStatus() {
_setEmojiStatus->overrideLinkClickHandler([=] { _setEmojiStatus->overrideLinkClickHandler([=] {
_controller->showSettings(Settings::Ayu::Id()); _controller->showSettings(Settings::AyuMain::Id());
}); });
} }
@ -652,6 +651,7 @@ void MainMenu::setupMenu() {
std::move(descriptor)); std::move(descriptor));
}; };
if (!_controller->session().supportMode()) { if (!_controller->session().supportMode()) {
if (settings.showMyProfileInDrawer)
_menu->add( _menu->add(
CreateButtonWithIcon( CreateButtonWithIcon(
_menu, _menu,
@ -663,12 +663,15 @@ void MainMenu::setupMenu() {
Info::Stories::Make(controller->session().user())); Info::Stories::Make(controller->session().user()));
}); });
if (settings.showBotsInDrawer)
SetupMenuBots(_menu, controller); SetupMenuBots(_menu, controller);
if (settings.showMyProfileInDrawer || settings.showBotsInDrawer)
_menu->add( _menu->add(
object_ptr<Ui::PlainShadow>(_menu), object_ptr<Ui::PlainShadow>(_menu),
{ 0, st::mainMenuSkip, 0, st::mainMenuSkip }); { 0, st::mainMenuSkip, 0, st::mainMenuSkip });
if (settings.showNewGroupInDrawer)
AddMyChannelsBox(addAction( AddMyChannelsBox(addAction(
tr::lng_create_group_title(), tr::lng_create_group_title(),
{ &st::menuIconGroups } { &st::menuIconGroups }
@ -678,6 +681,7 @@ void MainMenu::setupMenu() {
} }
}); });
if (settings.showNewChannelInDrawer)
AddMyChannelsBox(addAction( AddMyChannelsBox(addAction(
tr::lng_create_channel_title(), tr::lng_create_channel_title(),
{ &st::menuIconChannel } { &st::menuIconChannel }
@ -687,18 +691,21 @@ void MainMenu::setupMenu() {
} }
}); });
if (settings.showContactsInDrawer)
addAction( addAction(
tr::lng_menu_contacts(), tr::lng_menu_contacts(),
{ &st::menuIconUserShow } { &st::menuIconUserShow }
)->setClickedCallback([=] { )->setClickedCallback([=] {
controller->show(PrepareContactsBox(controller)); controller->show(PrepareContactsBox(controller));
}); });
if (settings.showCallsInDrawer)
addAction( addAction(
tr::lng_menu_calls(), tr::lng_menu_calls(),
{ &st::menuIconPhone } { &st::menuIconPhone }
)->setClickedCallback([=] { )->setClickedCallback([=] {
::Calls::ShowCallsBox(controller); ::Calls::ShowCallsBox(controller);
}); });
if (settings.showSavedMessagesInDrawer)
addAction( addAction(
tr::lng_saved_messages(), tr::lng_saved_messages(),
{ &st::menuIconSavedMessages } { &st::menuIconSavedMessages }
@ -706,18 +713,16 @@ void MainMenu::setupMenu() {
controller->showPeerHistory(controller->session().user()); controller->showPeerHistory(controller->session().user());
}); });
const auto &settings = AyuSettings::getInstance();
if (settings.showLReadToggleInDrawer) { if (settings.showLReadToggleInDrawer) {
addAction( addAction(
tr::ayu_LReadMessages(), tr::ayu_LReadMessages(),
{&st::ayuLReadMenuIcon} {&st::ayuLReadMenuIcon}
)->setClickedCallback([=] )->setClickedCallback([=]
{ {
auto prev = settings.sendReadMessages; const auto prev = settings.sendReadMessages;
AyuSettings::set_sendReadMessages(false); AyuSettings::set_sendReadMessages(false);
auto chats = controller->session().data().chatsList(); const auto chats = controller->session().data().chatsList();
MarkAsReadChatList(chats); MarkAsReadChatList(chats);
AyuSettings::set_sendReadMessages(prev); AyuSettings::set_sendReadMessages(prev);
@ -784,6 +789,8 @@ void MainMenu::setupMenu() {
controller->showSettings(); controller->showSettings();
}); });
if (settings.showNightModeToggleInDrawer) {
_nightThemeToggle = addAction( _nightThemeToggle = addAction(
tr::lng_menu_night_mode(), tr::lng_menu_night_mode(),
{ &st::menuIconNightMode } { &st::menuIconNightMode }
@ -813,30 +820,40 @@ void MainMenu::setupMenu() {
&_controller->window(), &_controller->window(),
toggle); toggle);
}, _nightThemeToggle->lifetime()); }, _nightThemeToggle->lifetime());
Core::App().settings().systemDarkModeValue(
) | rpl::start_with_next([=](std::optional<bool> darkMode) {
const auto darkModeEnabled
= Core::App().settings().systemDarkModeEnabled();
if (darkModeEnabled && darkMode.has_value()) {
_nightThemeSwitches.fire_copy(*darkMode);
}
}, _nightThemeToggle->lifetime());
}
if (settings.showGhostToggleInDrawer) { if (settings.showGhostToggleInDrawer) {
_ghostModeToggle = addAction( const auto ghostModeToggle = addAction(
tr::ayu_GhostModeToggle(), tr::ayu_GhostModeToggle(),
{&st::ayuGhostIcon} {&st::ayuGhostIcon}
)->toggleOn(AyuSettings::get_ghostModeEnabledReactive()); )->toggleOn(AyuSettings::get_ghostModeEnabledReactive());
_ghostModeToggle->toggledChanges( ghostModeToggle->toggledChanges(
) | rpl::start_with_next( ) | rpl::start_with_next(
[=](bool ghostMode) [=](bool ghostMode)
{ {
AyuSettings::set_ghostModeEnabled(ghostMode); AyuSettings::set_ghostModeEnabled(ghostMode);
AyuSettings::save(); AyuSettings::save();
}, },
_ghostModeToggle->lifetime()); ghostModeToggle->lifetime());
} }
if (settings.showStreamerToggleInDrawer) { if (settings.showStreamerToggleInDrawer) {
_streamerModeToggle = addAction( const auto streamerModeToggle = addAction(
tr::ayu_StreamerModeToggle(), tr::ayu_StreamerModeToggle(),
{&st::ayuStreamerModeMenuIcon} {&st::ayuStreamerModeMenuIcon}
)->toggleOn(rpl::single(AyuFeatures::StreamerMode::isEnabled())); )->toggleOn(rpl::single(AyuFeatures::StreamerMode::isEnabled()));
_streamerModeToggle->toggledChanges( streamerModeToggle->toggledChanges(
) | rpl::start_with_next( ) | rpl::start_with_next(
[=](bool enabled) [=](bool enabled)
{ {
@ -846,17 +863,8 @@ void MainMenu::setupMenu() {
AyuFeatures::StreamerMode::disable(); AyuFeatures::StreamerMode::disable();
} }
}, },
_streamerModeToggle->lifetime()); streamerModeToggle->lifetime());
} }
Core::App().settings().systemDarkModeValue(
) | rpl::start_with_next([=](std::optional<bool> darkMode) {
const auto darkModeEnabled
= Core::App().settings().systemDarkModeEnabled();
if (darkModeEnabled && darkMode.has_value()) {
_nightThemeSwitches.fire_copy(*darkMode);
}
}, _nightThemeToggle->lifetime());
} }
void MainMenu::resizeEvent(QResizeEvent *e) { void MainMenu::resizeEvent(QResizeEvent *e) {

View file

@ -104,8 +104,6 @@ private:
not_null<Ui::FlatLabel*> _telegram; not_null<Ui::FlatLabel*> _telegram;
not_null<Ui::FlatLabel*> _version; not_null<Ui::FlatLabel*> _version;
QPointer<Ui::SettingsButton> _nightThemeToggle; QPointer<Ui::SettingsButton> _nightThemeToggle;
QPointer<Ui::SettingsButton> _ghostModeToggle;
QPointer<Ui::SettingsButton> _streamerModeToggle;
rpl::event_stream<bool> _nightThemeSwitches; rpl::event_stream<bool> _nightThemeSwitches;
base::Timer _nightThemeSwitch; base::Timer _nightThemeSwitch;
base::unique_qptr<Ui::PopupMenu> _contextMenu; base::unique_qptr<Ui::PopupMenu> _contextMenu;

View file

@ -11,9 +11,14 @@ add_library(tdesktop::td_ui ALIAS td_ui)
include(lib_ui/cmake/generate_styles.cmake) include(lib_ui/cmake/generate_styles.cmake)
include(cmake/generate_numbers.cmake) include(cmake/generate_numbers.cmake)
set(style_files set(ayugram_style_files
ayu/ui/ayu_icons.style ayu/ui/ayu_icons.style
ayu/ui/ayu_styles.style ayu/ui/ayu_styles.style
ayu/ui/settings/ayu_settings.style
)
set(style_files
${ayugram_style_files}
ui/td_common.style ui/td_common.style
ui/filter_icons.style ui/filter_icons.style

@ -1 +1 @@
Subproject commit eed6fe4254c3670e48b680a7e0b61f642d3a40e8 Subproject commit 85f397c73f9595a7548ca84cc002dc3fb09aae11