From c2e2dfb577196b74d85d9055f24914ffdfc53b45 Mon Sep 17 00:00:00 2001 From: fourtf Date: Thu, 2 Aug 2018 14:23:27 +0200 Subject: [PATCH] this commit is too big --- chatterino.pro | 51 +- .../_generate_resources.cpython-36.pyc | Bin 0 -> 622 bytes resources/_generate_resources.py | 38 ++ .../button_ban.png => buttons/ban.png} | Bin .../buttons/ban.png => buttons/banRed.png} | Bin resources/{images => buttons}/emote.svg | 0 .../emote_dark.svg => buttons/emoteDark.svg} | 0 .../menu_black.png => buttons/menuDark.png} | Bin .../menu_white.png => buttons/menuLight.png} | Bin resources/{images => }/buttons/mod.png | Bin .../modModeDisabled.png} | Bin .../modModeDisabled2.png} | Bin .../modModeEnabled.png} | Bin .../modModeEnabled2.png} | Bin .../timeout.png} | Bin resources/{images => }/buttons/unban.png | Bin resources/{images => }/buttons/unmod.png | Bin .../update.png} | Bin .../updateError.png} | Bin resources/{images => }/chatterino2.icns | Bin resources/error.png | Bin 0 -> 685 bytes resources/generate_resources.py | 61 +++ resources/{images => }/icon.png | Bin resources/images/AppearanceEditorPart_16x.png | Bin 347 -> 0 bytes resources/images/BrowserLink_16x.png | Bin 680 -> 0 bytes .../images/CopyLongTextToClipboard_16x.png | Bin 403 -> 0 bytes resources/images/CustomActionEditor_16x.png | Bin 368 -> 0 bytes resources/images/Emoji_Color_1F60A_19 old.png | Bin 1932 -> 0 bytes resources/images/Emoji_Color_1F60A_19.png | Bin 660 -> 0 bytes resources/images/Filter_16x.png | Bin 280 -> 0 bytes resources/images/Message_16xLG.png | Bin 179 -> 0 bytes .../StatusAnnotations_Blocked_16xLG_color.png | Bin 1618 -> 0 bytes resources/images/UserProfile_22x.png | Bin 286 -> 0 bytes resources/images/VSO_Link_blue_16x.png | Bin 480 -> 0 bytes resources/images/cheer100.png | Bin 360 -> 0 bytes resources/images/cheer1000.png | Bin 351 -> 0 bytes resources/images/cheer10000.png | Bin 330 -> 0 bytes resources/images/cheer100000.png | Bin 285 -> 0 bytes resources/images/cheer5000.png | Bin 362 -> 0 bytes resources/images/collapse.png | Bin 236 -> 0 bytes resources/images/format_Bold_16xLG.png | Bin 354 -> 0 bytes resources/images/settings.png | Bin 274 -> 0 bytes resources/images/tool_moreCollapser_off16.png | Bin 279 -> 0 bytes resources/{images => }/pajaDank.png | Bin resources/resources.qrc | 83 +-- resources/resources_autogenerated.qrc | 64 +++ resources/{images => settings}/about.svg | 0 resources/{images => settings}/aboutlogo.png | Bin resources/{images => settings}/accounts.svg | 0 resources/{images => settings}/behave.svg | 0 resources/{images => settings}/commands.svg | 0 .../{images => settings}/notifications.svg | 0 resources/{images => settings}/theme.svg | 0 .../split/splitdown.png => split/down.png} | Bin .../split/splitleft.png => split/left.png} | Bin .../split/splitmove.png => split/move.png} | Bin .../split/splitright.png => split/right.png} | Bin .../split/splitup.png => split/up.png} | Bin .../{images/admin_bg.png => twitch/admin.png} | Bin .../broadcaster.png} | Bin resources/{images => twitch}/cheer1.png | Bin .../globalmod_bg.png => twitch/globalmod.png} | Bin .../moderator_bg.png => twitch/moderator.png} | Bin .../twitchprime_bg.png => twitch/prime.png} | Bin .../{images/staff_bg.png => twitch/staff.png} | Bin resources/{images => twitch}/subscriber.png | Bin .../{images/turbo_bg.png => twitch/turbo.png} | Bin resources/{images => twitch}/verified.png | Bin src/Application.cpp | 128 ++--- src/Application.hpp | 70 +-- src/BrowserExtension.cpp | 88 +++ src/BrowserExtension.hpp | 10 + src/RunGui.cpp | 130 +++++ src/RunGui.hpp | 10 + src/autogenerated/ResourcesAutogen.cpp | 44 ++ src/autogenerated/ResourcesAutogen.hpp | 57 ++ src/common/Aliases.hpp | 34 ++ src/common/Channel.cpp | 15 +- src/common/Channel.hpp | 3 +- src/common/Common.hpp | 24 +- src/common/CompletionModel.cpp | 64 ++- src/common/CompletionModel.hpp | 2 + src/common/Emotemap.cpp | 64 +-- src/common/Emotemap.hpp | 26 +- src/common/NetworkCommon.hpp | 4 +- src/common/NetworkRequest.cpp | 10 +- src/common/NetworkRequest.hpp | 4 +- src/common/NetworkResult.cpp | 2 +- src/common/NetworkResult.hpp | 7 +- src/common/NullablePtr.hpp | 15 +- src/common/Outcome.hpp | 51 ++ src/common/Singleton.hpp | 10 +- src/common/UniqueAccess.hpp | 31 +- .../accounts/AccountController.cpp | 2 +- .../accounts/AccountController.hpp | 7 +- .../commands/CommandController.cpp | 10 +- .../commands/CommandController.hpp | 9 +- .../highlights/HighlightController.cpp | 2 +- .../highlights/HighlightController.hpp | 7 +- src/controllers/ignores/IgnoreController.cpp | 2 +- src/controllers/ignores/IgnoreController.hpp | 7 +- .../moderationactions/ModerationAction.cpp | 7 +- .../moderationactions/ModerationAction.hpp | 8 +- .../moderationactions/ModerationActions.cpp | 2 +- .../moderationactions/ModerationActions.hpp | 5 +- .../taggedusers/TaggedUsersController.hpp | 2 +- src/debug/Log.hpp | 7 + src/main.cpp | 221 +------- src/messages/Emote.cpp | 75 +++ src/messages/Emote.hpp | 65 +++ src/messages/EmoteCache.hpp | 93 ++++ src/messages/EmoteMap.cpp | 44 ++ src/messages/EmoteMap.hpp | 20 + src/messages/Image.cpp | 449 ++++++++------- src/messages/Image.hpp | 103 ++-- src/messages/ImageSet.cpp | 95 ++++ src/messages/ImageSet.hpp | 35 ++ src/messages/MessageElement.cpp | 56 +- src/messages/MessageElement.hpp | 13 +- src/messages/layouts/MessageLayoutElement.cpp | 34 +- src/messages/layouts/MessageLayoutElement.hpp | 6 +- src/providers/bttv/BttvEmotes.cpp | 155 +++--- src/providers/bttv/BttvEmotes.hpp | 29 +- src/providers/bttv/LoadBttvChannelEmote.cpp | 76 +++ src/providers/bttv/LoadBttvChannelEmote.hpp | 14 + src/providers/chatterino/ChatterinoBadges.cpp | 50 ++ src/providers/chatterino/ChatterinoBadges.hpp | 25 + src/providers/emoji/Emojis.cpp | 24 +- src/providers/emoji/Emojis.hpp | 9 +- src/providers/ffz/FfzEmotes.cpp | 206 ++++--- src/providers/ffz/FfzEmotes.hpp | 35 +- src/providers/irc/AbstractIrcServer.cpp | 7 +- src/providers/twitch/IrcMessageHandler.cpp | 2 - src/providers/twitch/PartialTwitchUser.cpp | 13 +- src/providers/twitch/TwitchAccount.cpp | 191 ++++++- src/providers/twitch/TwitchAccount.hpp | 33 +- src/providers/twitch/TwitchApi.cpp | 12 +- src/providers/twitch/TwitchBadges.cpp | 58 ++ src/providers/twitch/TwitchBadges.hpp | 26 + src/providers/twitch/TwitchChannel.cpp | 224 ++++++-- src/providers/twitch/TwitchChannel.hpp | 41 +- src/providers/twitch/TwitchEmotes.cpp | 236 ++------ src/providers/twitch/TwitchEmotes.hpp | 61 +-- src/providers/twitch/TwitchMessageBuilder.cpp | 512 +++++++++--------- src/providers/twitch/TwitchMessageBuilder.hpp | 12 +- .../twitch/TwitchParseCheerEmotes.cpp | 264 +++++++++ .../twitch/TwitchParseCheerEmotes.hpp | 36 ++ src/providers/twitch/TwitchServer.cpp | 14 +- src/providers/twitch/TwitchServer.hpp | 9 +- src/singletons/Badges.cpp | 9 + src/singletons/Badges.hpp | 15 + src/singletons/Emotes.cpp | 15 +- src/singletons/Emotes.hpp | 11 +- src/singletons/Fonts.cpp | 14 +- src/singletons/Fonts.hpp | 5 +- src/singletons/Logging.cpp | 2 +- src/singletons/Logging.hpp | 2 +- src/singletons/NativeMessaging.cpp | 109 ++-- src/singletons/NativeMessaging.hpp | 29 +- src/singletons/Paths.cpp | 18 +- src/singletons/Paths.hpp | 10 +- src/singletons/Resources.cpp | 480 ---------------- src/singletons/Resources.hpp | 159 +----- src/singletons/Settings.cpp | 25 +- src/singletons/Settings.hpp | 11 +- src/singletons/WindowManager.cpp | 24 +- src/singletons/WindowManager.hpp | 7 +- src/util/JsonQuery.cpp | 9 + src/util/JsonQuery.hpp | 12 + src/widgets/Notebook.cpp | 5 +- src/widgets/Window.cpp | 4 +- src/widgets/dialogs/EmotePopup.cpp | 38 +- src/widgets/dialogs/LogsPopup.cpp | 14 +- src/widgets/dialogs/SelectChannelDialog.cpp | 2 +- src/widgets/dialogs/UserInfoPopup.cpp | 6 +- src/widgets/helper/ChannelView.cpp | 125 ++--- src/widgets/helper/SearchPopup.cpp | 2 +- src/widgets/settingspages/AboutPage.cpp | 2 +- src/widgets/settingspages/LookPage.cpp | 24 +- src/widgets/splits/Split.cpp | 67 +-- src/widgets/splits/Split.hpp | 18 +- src/widgets/splits/SplitContainer.cpp | 14 +- src/widgets/splits/SplitHeader.cpp | 20 +- src/widgets/splits/SplitInput.cpp | 4 +- src/widgets/splits/SplitOverlay.cpp | 11 +- weakOf | 0 186 files changed, 3626 insertions(+), 2656 deletions(-) create mode 100644 resources/__pycache__/_generate_resources.cpython-36.pyc create mode 100644 resources/_generate_resources.py rename resources/{images/button_ban.png => buttons/ban.png} (100%) rename resources/{images/buttons/ban.png => buttons/banRed.png} (100%) rename resources/{images => buttons}/emote.svg (100%) rename resources/{images/emote_dark.svg => buttons/emoteDark.svg} (100%) rename resources/{images/menu_black.png => buttons/menuDark.png} (100%) rename resources/{images/menu_white.png => buttons/menuLight.png} (100%) rename resources/{images => }/buttons/mod.png (100%) rename resources/{images/moderatormode_disabled.png => buttons/modModeDisabled.png} (100%) rename resources/{images/moderatormode_disabled2.png => buttons/modModeDisabled2.png} (100%) rename resources/{images/moderatormode_enabled.png => buttons/modModeEnabled.png} (100%) rename resources/{images/moderatormode_enabled2.png => buttons/modModeEnabled2.png} (100%) rename resources/{images/button_timeout.png => buttons/timeout.png} (100%) rename resources/{images => }/buttons/unban.png (100%) rename resources/{images => }/buttons/unmod.png (100%) rename resources/{images/download_update.png => buttons/update.png} (100%) rename resources/{images/download_update_error.png => buttons/updateError.png} (100%) rename resources/{images => }/chatterino2.icns (100%) create mode 100644 resources/error.png create mode 100755 resources/generate_resources.py rename resources/{images => }/icon.png (100%) delete mode 100644 resources/images/AppearanceEditorPart_16x.png delete mode 100644 resources/images/BrowserLink_16x.png delete mode 100644 resources/images/CopyLongTextToClipboard_16x.png delete mode 100644 resources/images/CustomActionEditor_16x.png delete mode 100644 resources/images/Emoji_Color_1F60A_19 old.png delete mode 100644 resources/images/Emoji_Color_1F60A_19.png delete mode 100644 resources/images/Filter_16x.png delete mode 100644 resources/images/Message_16xLG.png delete mode 100644 resources/images/StatusAnnotations_Blocked_16xLG_color.png delete mode 100644 resources/images/UserProfile_22x.png delete mode 100644 resources/images/VSO_Link_blue_16x.png delete mode 100644 resources/images/cheer100.png delete mode 100644 resources/images/cheer1000.png delete mode 100644 resources/images/cheer10000.png delete mode 100644 resources/images/cheer100000.png delete mode 100644 resources/images/cheer5000.png delete mode 100644 resources/images/collapse.png delete mode 100644 resources/images/format_Bold_16xLG.png delete mode 100644 resources/images/settings.png delete mode 100644 resources/images/tool_moreCollapser_off16.png rename resources/{images => }/pajaDank.png (100%) create mode 100644 resources/resources_autogenerated.qrc rename resources/{images => settings}/about.svg (100%) rename resources/{images => settings}/aboutlogo.png (100%) rename resources/{images => settings}/accounts.svg (100%) rename resources/{images => settings}/behave.svg (100%) rename resources/{images => settings}/commands.svg (100%) rename resources/{images => settings}/notifications.svg (100%) rename resources/{images => settings}/theme.svg (100%) rename resources/{images/split/splitdown.png => split/down.png} (100%) rename resources/{images/split/splitleft.png => split/left.png} (100%) rename resources/{images/split/splitmove.png => split/move.png} (100%) rename resources/{images/split/splitright.png => split/right.png} (100%) rename resources/{images/split/splitup.png => split/up.png} (100%) rename resources/{images/admin_bg.png => twitch/admin.png} (100%) rename resources/{images/broadcaster_bg.png => twitch/broadcaster.png} (100%) rename resources/{images => twitch}/cheer1.png (100%) rename resources/{images/globalmod_bg.png => twitch/globalmod.png} (100%) rename resources/{images/moderator_bg.png => twitch/moderator.png} (100%) rename resources/{images/twitchprime_bg.png => twitch/prime.png} (100%) rename resources/{images/staff_bg.png => twitch/staff.png} (100%) rename resources/{images => twitch}/subscriber.png (100%) rename resources/{images/turbo_bg.png => twitch/turbo.png} (100%) rename resources/{images => twitch}/verified.png (100%) create mode 100644 src/BrowserExtension.cpp create mode 100644 src/BrowserExtension.hpp create mode 100644 src/RunGui.cpp create mode 100644 src/RunGui.hpp create mode 100644 src/autogenerated/ResourcesAutogen.cpp create mode 100644 src/autogenerated/ResourcesAutogen.hpp create mode 100644 src/common/Aliases.hpp create mode 100644 src/common/Outcome.hpp create mode 100644 src/messages/Emote.cpp create mode 100644 src/messages/Emote.hpp create mode 100644 src/messages/EmoteCache.hpp create mode 100644 src/messages/EmoteMap.cpp create mode 100644 src/messages/EmoteMap.hpp create mode 100644 src/messages/ImageSet.cpp create mode 100644 src/messages/ImageSet.hpp create mode 100644 src/providers/bttv/LoadBttvChannelEmote.cpp create mode 100644 src/providers/bttv/LoadBttvChannelEmote.hpp create mode 100644 src/providers/chatterino/ChatterinoBadges.cpp create mode 100644 src/providers/chatterino/ChatterinoBadges.hpp create mode 100644 src/providers/twitch/TwitchBadges.cpp create mode 100644 src/providers/twitch/TwitchBadges.hpp create mode 100644 src/providers/twitch/TwitchParseCheerEmotes.cpp create mode 100644 src/providers/twitch/TwitchParseCheerEmotes.hpp create mode 100644 src/singletons/Badges.cpp create mode 100644 src/singletons/Badges.hpp create mode 100644 src/util/JsonQuery.cpp create mode 100644 src/util/JsonQuery.hpp create mode 100644 weakOf diff --git a/chatterino.pro b/chatterino.pro index 4a1d00adc..8d477fc61 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -9,7 +9,6 @@ message(----) QT += widgets core gui network multimedia svg CONFIG += communi COMMUNI += core model util -CONFIG += c++14 INCLUDEPATH += src/ TARGET = chatterino TEMPLATE = app @@ -17,6 +16,16 @@ DEFINES += QT_DEPRECATED_WARNINGS PRECOMPILED_HEADER = src/PrecompiledHeader.hpp CONFIG += precompile_header +win32-msvc* { + QMAKE_CXXFLAGS = /std=c++17 +} else { + QMAKE_CXXFLAGS = -std=c++17 +} + +debug { + DEFINES += QT_DEBUG +} + useBreakpad { LIBS += -L$$PWD/lib/qBreakpad/handler/build include(lib/qBreakpad/qBreakpad.pri) @@ -132,9 +141,7 @@ SOURCES += \ src/messages/MessageBuilder.cpp \ src/messages/MessageColor.cpp \ src/messages/MessageElement.cpp \ - src/providers/bttv/BttvEmotes.cpp \ src/providers/emoji/Emojis.cpp \ - src/providers/ffz/FfzEmotes.cpp \ src/providers/irc/AbstractIrcServer.cpp \ src/providers/irc/IrcAccount.cpp \ src/providers/irc/IrcChannel2.cpp \ @@ -232,7 +239,21 @@ SOURCES += \ src/widgets/dialogs/UpdateDialog.cpp \ src/widgets/settingspages/IgnoresPage.cpp \ src/providers/twitch/PubsubClient.cpp \ - src/providers/twitch/TwitchApi.cpp + src/providers/twitch/TwitchApi.cpp \ + src/messages/Emote.cpp \ + src/messages/EmoteMap.cpp \ + src/messages/ImageSet.cpp \ + src/providers/bttv/BttvEmotes.cpp \ + src/providers/ffz/FfzEmotes.cpp \ + src/autogenerated/ResourcesAutogen.cpp \ + src/singletons/Badges.cpp \ + src/providers/twitch/TwitchBadges.cpp \ + src/providers/chatterino/ChatterinoBadges.cpp \ + src/providers/twitch/TwitchParseCheerEmotes.cpp \ + src/providers/bttv/LoadBttvChannelEmote.cpp \ + src/util/JsonQuery.cpp \ + src/RunGui.cpp \ + src/BrowserExtension.cpp HEADERS += \ src/Application.hpp \ @@ -292,9 +313,7 @@ HEADERS += \ src/messages/MessageParseArgs.hpp \ src/messages/Selection.hpp \ src/PrecompiledHeader.hpp \ - src/providers/bttv/BttvEmotes.hpp \ src/providers/emoji/Emojis.hpp \ - src/providers/ffz/FfzEmotes.hpp \ src/providers/irc/AbstractIrcServer.hpp \ src/providers/irc/IrcAccount.hpp \ src/providers/irc/IrcChannel2.hpp \ @@ -413,10 +432,28 @@ HEADERS += \ src/widgets/dialogs/UpdateDialog.hpp \ src/widgets/settingspages/IgnoresPage.hpp \ src/providers/twitch/PubsubClient.hpp \ - src/providers/twitch/TwitchApi.hpp + src/providers/twitch/TwitchApi.hpp \ + src/messages/Emote.hpp \ + src/messages/EmoteMap.hpp \ + src/messages/EmoteCache.hpp \ + src/messages/ImageSet.hpp \ + src/common/Outcome.hpp \ + src/providers/bttv/BttvEmotes.hpp \ + src/providers/ffz/FfzEmotes.hpp \ + src/autogenerated/ResourcesAutogen.hpp \ + src/singletons/Badges.hpp \ + src/providers/twitch/TwitchBadges.hpp \ + src/providers/chatterino/ChatterinoBadges.hpp \ + src/common/Aliases.hpp \ + src/providers/twitch/TwitchParseCheerEmotes.hpp \ + src/providers/bttv/LoadBttvChannelEmote.hpp \ + src/util/JsonQuery.hpp \ + src/RunGui.hpp \ + src/BrowserExtension.hpp RESOURCES += \ resources/resources.qrc \ + resources/resources_autogenerated.qrc DISTFILES += diff --git a/resources/__pycache__/_generate_resources.cpython-36.pyc b/resources/__pycache__/_generate_resources.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dfaf6af51e47da0c049a7c2115f8105a43375046 GIT binary patch literal 622 zcmah{yH3L}6m{FA1&c~7Y*1NYX(UujWh)_~Dl>wXDGZf~ohBlQO&(H7)lc9<_y@kE zD?h-@gi9K?0|HAv^1bKC=U!i*x7+aSojiQj0QdkqOUB?z?(|p!1rDG!r$*~eoi>~X z)f|oLj!v5sV77{mH6D#@h>-Q1u$1SC%aBNz7tAjWY}mz-WU!)aXyLWKcs&hbH_RzR z*4=F|kBG3LN*OMXA|B&O5YIxE@wn#;VL%v@h^2x^2iGTA#*!fBXaS)ck~Br*jeXij z10?dtFmRE&LCVV1K$ePjv|aN8!lJX3xI_qJ^qWa>zpYK#4D*c7*nhPtd4Dk2p1USo zz~W^2m+6h!TpcD=Q&XQ2%97oT$GLoiRl8&=K^0bPITOWh!jgRhNlT9S2fl{b=Mlr6 zoMGnSYnDD`T;T0X_pvGlPi5mQAsL&hTu;2L_N<80JY;rb{d=GUP16oEUEA06ZwVd9 AfdBvi literal 0 HcmV?d00001 diff --git a/resources/_generate_resources.py b/resources/_generate_resources.py new file mode 100644 index 000000000..2ce917fbd --- /dev/null +++ b/resources/_generate_resources.py @@ -0,0 +1,38 @@ +resources_header = \ +''' + ''' + +resources_footer = \ +''' +''' + +header_header = \ +'''#include +#include "common/Singleton.hpp" + +namespace chatterino { + +class Resources2 : public Singleton { +public: + Resources2(); + +''' + +header_footer = \ +'''}; + +} // namespace chatterino''' + +source_header = \ +'''#include "ResourcesAutogen.hpp" + +namespace chatterino { + +Resources2::Resources2() +{ +''' + +source_footer = \ +'''} + +} // namespace chatterino''' diff --git a/resources/images/button_ban.png b/resources/buttons/ban.png similarity index 100% rename from resources/images/button_ban.png rename to resources/buttons/ban.png diff --git a/resources/images/buttons/ban.png b/resources/buttons/banRed.png similarity index 100% rename from resources/images/buttons/ban.png rename to resources/buttons/banRed.png diff --git a/resources/images/emote.svg b/resources/buttons/emote.svg similarity index 100% rename from resources/images/emote.svg rename to resources/buttons/emote.svg diff --git a/resources/images/emote_dark.svg b/resources/buttons/emoteDark.svg similarity index 100% rename from resources/images/emote_dark.svg rename to resources/buttons/emoteDark.svg diff --git a/resources/images/menu_black.png b/resources/buttons/menuDark.png similarity index 100% rename from resources/images/menu_black.png rename to resources/buttons/menuDark.png diff --git a/resources/images/menu_white.png b/resources/buttons/menuLight.png similarity index 100% rename from resources/images/menu_white.png rename to resources/buttons/menuLight.png diff --git a/resources/images/buttons/mod.png b/resources/buttons/mod.png similarity index 100% rename from resources/images/buttons/mod.png rename to resources/buttons/mod.png diff --git a/resources/images/moderatormode_disabled.png b/resources/buttons/modModeDisabled.png similarity index 100% rename from resources/images/moderatormode_disabled.png rename to resources/buttons/modModeDisabled.png diff --git a/resources/images/moderatormode_disabled2.png b/resources/buttons/modModeDisabled2.png similarity index 100% rename from resources/images/moderatormode_disabled2.png rename to resources/buttons/modModeDisabled2.png diff --git a/resources/images/moderatormode_enabled.png b/resources/buttons/modModeEnabled.png similarity index 100% rename from resources/images/moderatormode_enabled.png rename to resources/buttons/modModeEnabled.png diff --git a/resources/images/moderatormode_enabled2.png b/resources/buttons/modModeEnabled2.png similarity index 100% rename from resources/images/moderatormode_enabled2.png rename to resources/buttons/modModeEnabled2.png diff --git a/resources/images/button_timeout.png b/resources/buttons/timeout.png similarity index 100% rename from resources/images/button_timeout.png rename to resources/buttons/timeout.png diff --git a/resources/images/buttons/unban.png b/resources/buttons/unban.png similarity index 100% rename from resources/images/buttons/unban.png rename to resources/buttons/unban.png diff --git a/resources/images/buttons/unmod.png b/resources/buttons/unmod.png similarity index 100% rename from resources/images/buttons/unmod.png rename to resources/buttons/unmod.png diff --git a/resources/images/download_update.png b/resources/buttons/update.png similarity index 100% rename from resources/images/download_update.png rename to resources/buttons/update.png diff --git a/resources/images/download_update_error.png b/resources/buttons/updateError.png similarity index 100% rename from resources/images/download_update_error.png rename to resources/buttons/updateError.png diff --git a/resources/images/chatterino2.icns b/resources/chatterino2.icns similarity index 100% rename from resources/images/chatterino2.icns rename to resources/chatterino2.icns diff --git a/resources/error.png b/resources/error.png new file mode 100644 index 0000000000000000000000000000000000000000..07fba9f7cdf56d924bd46eb0a9ba1632e92f2293 GIT binary patch literal 685 zcmV;e0#f~nP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02y>eSaefwW^{L9 za%BK;VQFr3E^cLXAT%y8E;2FkAZe8V00JFJL_t(|Ugea*ZiFxlMET_Y|DhB|>qu+{ za2(rl=;5L5Y-r|<)NcE>{T}RJ{Orf`RghEbS-xn(5APY_LY}xAfLMQo8+qbs0Ak${ z9^?th0K|GD2;>Rz!Gm>1=x{ooeJ?bZWuw#O_@sL=B?^b~82JThPQ>Sv+_I7lkMm1jC# z-X+g;IMHGB#W(B7Gr|!h1FR>{2u64sU|o6UVT8K@)|Y2)Mz|W_J@O13VcY)zSH)J= TiRV#)00000NkvXXu0mjfZe|>+ literal 0 HcmV?d00001 diff --git a/resources/generate_resources.py b/resources/generate_resources.py new file mode 100755 index 000000000..4b047219c --- /dev/null +++ b/resources/generate_resources.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +from pathlib import Path + +from _generate_resources import * + +ignored_files = ['qt.conf', 'resources.qrc', 'resources_autogenerated.qrc', 'windows.rc', + 'generate_resources.py', '_generate_resources.py'] +ignored_directories = ['__pycache__'] + +def isNotIgnored(file): + return str(file) not in ignored_files + +all_files = list(filter(isNotIgnored, \ + filter(Path.is_file, Path('.').glob('**/*')))) +image_files = list(filter(isNotIgnored, \ + filter(Path.is_file, Path('.').glob('**/*.png')))) + +with open('./resources_autogenerated.qrc', 'w') as out: + out.write(resources_header) + for file in all_files: + out.write(f" {str(file)}\n") + out.write(resources_footer) + +with open('../src/autogenerated/ResourcesAutogen.cpp', 'w') as out: + out.write(source_header) + for file in sorted(image_files): + var_name = str(file.with_suffix("")).replace("/",".") + out.write(f' this->{var_name}') + out.write(f' = QPixmap(":/{file}");\n') + out.write(source_footer) + +def writeHeader(out, name, element, indent): + if isinstance(element, dict): + if name != "": + out.write(f"{indent}struct {{\n") + for (key, value) in element.items(): + writeHeader(out, key, value, indent + ' ') + if name != "": + out.write(f"{indent}}} {name};\n"); + else: + out.write(f"{indent}QPixmap {element};\n") + +with open('../src/autogenerated/ResourcesAutogen.hpp', 'w') as out: + out.write(header_header) + + elements = {} + for file in sorted(image_files): + elements_ref = elements + directories = str(file).split('/')[:-1] + filename = file.stem + for directory in directories: + if directory not in elements_ref: + if directory not in ignored_directories: + elements_ref[directory] = {} + elements_ref = elements_ref[directory] + elements_ref[filename] = filename + + writeHeader(out, "", elements, '') + + out.write(header_footer) + diff --git a/resources/images/icon.png b/resources/icon.png similarity index 100% rename from resources/images/icon.png rename to resources/icon.png diff --git a/resources/images/AppearanceEditorPart_16x.png b/resources/images/AppearanceEditorPart_16x.png deleted file mode 100644 index 86c59f7ff0455b522519a95a7814c96608c0a84c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 347 zcmV-h0i^zkP)!Po0^`Cv+&V#FaXY2*zEZ+KvV zXTk1n2p*3YlarIsYP6VtG>^Htd+>U_B1RnIl16MmHS-KBsZhd5R^(KMxzmAkgry&K^XiQ z@a&~)^RLmo$=27`+2Z0Nvsf&&9y5o-!IqbwkT32A*H)>7DsRYr+=Bi6S2#QSibyz$ z>FH_2_hQiLbQC(F4IU&C37Rcrm`o-ZjYg!?Y4MFlqi{N$LVR)Y4drqfE|-g_87hI9 z&1RO%P)z@Q!Y)7o_C)WxNr z!=*`M>ms!pNKn&A!UyQk598oMn6$_duZNcw^hs`d`MvMMdq;|*0EG|&MxN)V0B!(u zX$im@z_p?%@2Eku5)QN8Znp!^^HkS$mCa`30eCAL6A17Id^%M+pH8Qk-_EgE++jAG z(P?J@c4hqpH9A7D!m_L%ML;4zmSwPQ8&MQNRn?Eu1SNzU@L+Wqy}l2>=Y#FZaU50` z4LC2)uV~`3d(qs0ay97V5D&{0L^^FBiX=($cLT#PFdB^z1OehWhH07@kH^2>jC9N( zxL>w~i$;Sm4EJG5nx^y!zjDOjarMMB+Yk--{cD<5DO0G0uIrdgCjYyIug@KZ!{NS( xEX(yh5N6QyP}uTVBlapq1HPCR*!0SV@(rXni`{+Y%fSEu002ovPDHLkV1j%tuvY*8 diff --git a/resources/images/CustomActionEditor_16x.png b/resources/images/CustomActionEditor_16x.png deleted file mode 100644 index b7b68a21eb6c80e613b18977843f44834284e5e1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 368 zcmV-$0gwKPP)kKCawJa|+ zL2Wb`LVf@&Wzi7JwLleXf<8gC;&+Y>;+Vi&@ij&#=?qmuzfc{Mp*vRV;bJYEna9gY zvn){t0R*ASV$ongRG6q0PvA1>J-$rfSRzDivEt?Nf>?vI9;=jOh#9qd4M$QGM$=l# zV5Dhk6{aI89YLZWt)UEzk!ExzY~sR8voMRydl{FlYN8mjthirQLkvMgA`xvwua#sU zL7C0wIEYTCK@l3IO;p*aMpTO85J?OhR5)1(se&Y8aZI*J3aeHeM}BMqJRnbDi^@c^ zP}_)r;Pb1pgx)AENlee+xv7j$SPmri!gB-+z#tS=1;tY{;!)?O5@wW$vY{uyQLxkb z^zdMC2E3x$ThW;&`tzR!6i-_S5-zAnVVQ&WRFUJ9CX;MI6}}00?+8pmHNsG6Ro$IOm;m*l~B5J z8)eqns|;p4sWVMWw@F+$-l@rS{{N-d$pRuS8+b05I5RLyKxX^`lv$bc3mhA8hnt!q zrzU(!-c+!@q?I%SLMe*D>Sc)!bLyL(DmPXq1_TjWh&Zc|J3*+VHJNK6o>IobbByu~ zWuLXwzu+th8Zp&^qDpexw1c_frkMvzY#hT9Bw|=D{-s!Pg>8j=lJb)-;dy6{`ke%i z_#uLRI=`lZ70IhcSQ(c45K$&2r9u{XAH*u%G`3=mAo5Z~DW17Y&d-WIh{TMDi&C7` zI5B-TlgGCZPc$ewylH}g@{(&vh>!vjQxb@bDG`MtDkdX|+)UIMk-TPxs@Ez2%tews zH$MgJ?plZzF|OL>1R%ByfVbZTVC*q^-UJ{_0`U7r0GOQsyeNIR;Y1YxDLWmua!>Sf z@5lW$spYxv4R*eexo}Q{J-4rB@uiWUGjd%A9e<@!4?wJL@5RfXf6|?IzI|ROqd)7w z<&`Og3-jim?{$t2A6vIAC@sX{o_UCk`9`0ClwLzP%rffL6qh|PA zEU%}sEv4&WVP4(Qb7QWZIX8?yt=s?AOV_G2sXun>n)@#G^yI*<^n`RB&`%|Cqy|w)8w}iT4 z_0X67v(LQ>&i=8W);tK@*9)5eP9NI0YP^JBL3bQl+%@O>l3Md;V_S>ek3O9x-*ap~ zc6)v2nsIOLai#5kMNProjI@(6?8x1ZI_91lY~R;d{K^CROx^ukJ*FR)jQzUBwW;Wr zH_8gt#i!fbZH;>>I=zPNk2^{uXTVKV-cm4s*N_YnV diff --git a/resources/images/Emoji_Color_1F60A_19.png b/resources/images/Emoji_Color_1F60A_19.png deleted file mode 100644 index 15e15bd4ac351cab80279e7b7c3094b98c4d15e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 660 zcmV;F0&D$=P)004R= z004l4008;_004mL004C`008P>0026e000+nl3&F}00009a7bBm000XT000XT0n*)m z`~Uy|8FWQhbW?9;ba!ELWdLwtX>N2bZe?^JG%heMF*(%MvSa`N0rg2lK~y+Tom9I@ z13?tsY$h)^4>b~F+?b#!5no{N0TD@&N)*xx7AYj9_(VI?*ohVvqK#Izf_{KMAou|m z7J`Lf8!;;5oL%oSYc@o^a9Hl0bI;y6v$G24+z}#}KtW(xpz{693os`>;1n#K@b39M zu39|G-i!HGzPZ%N=Vv0kE#&?{9qZtkbTgd9GQHj2r@~~2KU@uSew(es{pFA$oa*vE zf^W9fi)C8;>f7PE?kMsTM_WlA3A(H3L$fT#Fwt*?2xq$~n$Ymkh}@WVjI9yr?qbl^ z^b_(rwhRwRjKNg$sI(A8m0#}nSq(B!$2xe(;h`2#t0Y!Pba+0A3?fh|Cu|}F)QLki zoU+JurL&!_;P`3b=48OY!}W+&lYu&MsD`Zyv7(aAw4Cph;rkvapyuP|7$UHZj=hhtSFC9iEwoT1p(a%a}zR1v7&8C^vG1 z?b__~Kz*&+%(=69^`fqergykJS@^5}{fp$OVbAZC-c6mQR^t>X67c^_>~xirOb^y) zN-fFk68LiAwCvoYju$6{&wE}Qd!56PfA5`(jfWkhr#(GWweQuJob<`c2j)Cl>9?NK zV0p!=<{1ptUCVztvT(aRS@mhnDy|e6_fxs`wpSQ-8=f}eGux36CM b{d4o%p$=SnbeZ}DE>gTe~DWM4f1jcTo diff --git a/resources/images/Message_16xLG.png b/resources/images/Message_16xLG.png deleted file mode 100644 index 7d06b1995d0f7c0610effdaee04a93782ae5c4b6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 179 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9F5M?jcysy3fAP%zcg z#WAEJ?(HN)t_A}h*Wj05@>OGtXWr9g|ES^jQo@>p&2dqg@ck)IZ8`p=q?tG^^UFSa zkG*Wh&y6}tCKn~{bNp$1mdKI;Vst06l6v;s5{u diff --git a/resources/images/StatusAnnotations_Blocked_16xLG_color.png b/resources/images/StatusAnnotations_Blocked_16xLG_color.png deleted file mode 100644 index b58166e3064edc117c8b97942e585dc84d36a27b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1618 zcmbVMPi)&%7!QmNQE6HaYzMeJxsw2?j_v%h8_VuU>y$>C)2NBGawzJH{hGYhvCZ>K zlXgf86B2MiLP7#@=m`l92yNnsrd~K8AwWnwY=>QtI8GhA0f+H9PNIaNYRs}@zxVyV z-}k=n`{nzvT6yEl>`Sw$RO(E5&DwzXT>LzD3V!e3`wcc`d|Kb+oAxE$3EFAw1`U$# zwL`F{QpUwzh@D-+QG+z8ZwklX9SMlKrf@lL%XYXzT6FDzk=j9}?i}no1y{JZG;8#9 zAZQbgQLpX!k=`?f0k00<@wOzO0fg_GLNN}AHti}}2^c~7bXIg^MM0{XR&se&RTfZ2 zRx*+dw<;<*J+JB+4Gk}0X%-pG-O)F!)nPDLnL>;6P?w}`x0~)}(*bKrN}*7QNirD` z5Ms3NbKDdCXg;P$I4lx5jE0;BK8iVUBj|8b5WtRxol$qhYxItZkBN~StU!4E&&ErS>SdY{!V?h zhSq(L`oxf8Re^aRsAM(rHKY^57_Os7PZ0J=L;Z`+7|2LpqY)3-{zPIm^7eFMgM%Y9 z5Rs0Z_@8155#A$iBKeVtK5fsT-U)fPMS%KbeT~6Uu*18Ukzx~sG8!q%jJi#NR%$AG z^)mI{pc}n5eM+Z={U(87CdET3R<$5a_GXm6Aw5>0q6T_qVFQznFS(H3ZACw{3jhpnyBgH!8K%Hu~l0 z`0&=sGqAH&wu<%MTcwYFe4#JT&Ak7`%pVJP`gh)aWp{4<(0l!B{L|Kr_imiqJpA>C zoyw`d_s@P+f4;wW?(Ws_+Y3LRzIKc8+YfKbJ9j^L^I4@OoWJ?8_-SS%rD%& ZV)P$s#V`L_d>I0z%B70+&GPp3e*mip`8EIm diff --git a/resources/images/UserProfile_22x.png b/resources/images/UserProfile_22x.png deleted file mode 100644 index c47f61243ff1731e5b9d01a5bb04a7de5e2efddb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 286 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H1|*Mc$*~4foCO|{#S9F5M?jcysy3fAQ1F7M zi(^Pd+})`gxeghKxaO;Rzr1(z+}Ge;D-!;2Sl?dCx9f_%VbiWJ>aTL@7ge8(s60H| zh-H$<5$49zvV8d!o{KVP8L&4sU9(zY+xOU`AW($W>B2VM4%TM}_53EWO#jS!{&+WE z>Z?CTuWodjxBfuDvUA~Qf);yUzM@o?BEe=~c)wm}?&2Lg&2*A9OE)McB+9s7SuVx4 zE-6aa?XKvQU5yip_;*fDFuT3=_7RrOd$GrFrYB8yjx6HQJ~wUVn&mOjZw#KUelF{r5}E+X)^#`l diff --git a/resources/images/VSO_Link_blue_16x.png b/resources/images/VSO_Link_blue_16x.png deleted file mode 100644 index eb3882929648613f91bccc6c7ac9758abf566c63..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 480 zcmV<60U!Q}P))+3xEnKP~`I&NJwkN!5li+b=IKAIc8_rGBZ%J zC6y}{`scSv9o-;|+&d83qnxHw?Ch3oW51~`5TTXLG%-Fou<&UyDK4m<2vfsUMA-Nx zDk85xM``^aL;mkaVt)5EPE14&B-}GGvitu~UNcD34f>hPP>I!@8xbA)b|_}=Qj%Uy zhKWhButEUE5HTRD=~&y(f*%L|ZWo3-T!?w!!dY&?MYf3DQ~@@t3MR*z0YuwhIhyA- zF3}m>!mp2R^fbP}_>U7zubo3xfOKAm$@@1>FbR1d%m#XE;AyRbW)S|K=CJH(g0IVk zGK&I6+(9MVR`0-cUoDs*``|NvVB_>}uGm2^(mN2FDFlFp8MFbgo3pg;8WPXoXSxGN W(LnN591B8F2MazHx%6|XKfBz{VLMb6ZPB(2S zAVDi4M8bOarfS!UTC~P|{5>sNyLIn+P@yd(N63EvEh9$Be*ZZtRWc?_rD@itY1V2- zj!iXeGAB)ALx(vlRboPgw{z-}UcQoDzP57dX-AKAw1aN|005XtL_t(|+DuRf62mYE zV>Ej--FyB2lR-4b!RG_Zk`X4d5L-)lA!3OW#J-t@S0r8&a07{Dwt_!z!?M(%@9p>^ z$^xQ&p;}DQ7_4!lsv{2R>88?BwQBI}AE|p)(E2)bcW{6{B~8$7OGkEX_>}Vltiy@3 z4;swGbufwBWx*ib@6s*yDKb1ISHL8BlpuIx`$=5P0t^5mcLQqA{Jq8i0000QaG}WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8gaDrqSGyaV?Qd+6GBlPpG?6wmw!6Mr+Q`Ig-DJBPTddEn zGF>~_bj?JS=n$2-Fy-iAyQ`bDDl%=aZZumrMb6e%!OKlKDp_W_n97{BwWmV<>|DXE6!ux{c zbXGBz1%@sMcwRU6nLktfb9CzM_j1u|c2@1`%y=b`>{&No!=T=tAu`#5Yh~;62fqaK z!_C@S^uCu)vkXw!eMC9I#7uWxXwa!w`iY8R(x)qISI)@jcK&;Njb~I%+pps(PC~!h vq8h#%6}g^E-n{CCqKbP0l+XkKR0M%i diff --git a/resources/images/cheer10000.png b/resources/images/cheer10000.png deleted file mode 100644 index 3e3eb4284a7da6b09effd73a91d672c06de74418..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 330 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+3?vf;>QaG}WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8*Z`jp*Z=Br|J7vUCD`I5*y1JG|EmMp>{}FscPI+)R22TC zDxDz7K2?r)lcLZ@MWHjwVp|o2awR#Y$noY%aZHutJFO(1EyfCXKYrxi zdp<;edwU(@znDA91zFrdJDj-%9PWw;FBdGhqP5`J-XAAp!`4rm+99vQaG}WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!fozW|>Q*Z-%5TR0fjda%7-EpRZC^UE&5Yu&v4l1zuPxGuNy zeAy}Zb&t@iRRZh0*;_andc_#~B$%#s@vik^yVl9uE6#W{k87G-;{JvN(24H;+K^6+2hRK04aV`t|c?8bP0l+XkKg9K)+ diff --git a/resources/images/cheer5000.png b/resources/images/cheer5000.png deleted file mode 100644 index 89a7a4015e3b125791e8b4a05f623703c9ca4d72..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 362 zcmV-w0hRuVP)-U04z@r zNp}D*Q%I@(LZI+CklHzr+Crf5D|oppdAcllx*T7Z9AB0IE>Z?LU=B%l1vp?Tc)A8S zU`MF^8()?NIbleu{v2SJL7(t9kJ;KCq$mIY0HaAnK~#9!L{J9~12GK3_;&Z+{r@v- z>^n7zB0~tlh7qK}42VsOnu(w@Zy0rGqA+U1ii6;Ozsfk>1zQ(QqzrIxy;i!%cPyy4nNmMAzK}>9F*kit$8F|SWPEROJ5ABv z+#&Jl+|Q$>CRBMd93BJz9p(s@swR>SR`bYQMTNrB2lxpv0B?f>pwtt;%K!iX07*qo IM6N<$f>;2N-2eap diff --git a/resources/images/collapse.png b/resources/images/collapse.png deleted file mode 100644 index b181ccbf931e797f8cc0703dc9d0a79300c6ce40..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 236 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qucLCF%=h?3y^w370~qEv>0#LT=By}Z;C1rt33 zJwwYY*EWF^#dx|nhFJI?J!8mqz<|g3qRd_M@{=E24Y^!OUVLyU%vt;H-w{4xuF$K} z@1waymbh|lX81E-W5;9jZ)&XNPd4=LVE8Y(q5m}#h`Z%u{h|qSPKvfIvTAQW#I0xl YW`FtkGFEq#y`LMqSqkPh2>u3>cPW zvG5-Z-S!!_Z6nNw9bjId?|YCtp)AXgzH1xEh@yxb$3gn396@E8rYX`d@sww!LDMuK z6-7aw=MlgobzKMXMXLkXbt%ttxx_np)E;4)CQf;o2!qxVmt~>0ZDl<#@_J-&b0pvQ zaR(o|Bk%#?IanMpx_Zt z7srr_xV6E%c@HZHeASrQI!`vpK+HCINhkNJ(;PVumN0ltYX73s6BDs3!s?~nd%5!S z8_vY9{W`C>Zc5Otj1vpp?nFL*%PwNJWXdcg&#L-MgKi$F6A3JB2q+2REoY z=7?1u^lRPgg&ebxsLQ0Hr@|D*ylh diff --git a/resources/images/tool_moreCollapser_off16.png b/resources/images/tool_moreCollapser_off16.png deleted file mode 100644 index 717fc75e2b9191726c30875cc8f75d33527bea67..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 279 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSK$uZf!>a)(C|TkfQ4*Y=R#Ki=l*&+$n3-3imzP?iV4`QBXK1jNi|YVT(G*V? z#}EtuqZ2RkHX8^yJ5HU^F1abp?5vRN!yVI)1xYzc{aC5memGrviB7$%*UQjjhIe*M z`^dd6ztUjypE->8Cf}c3_WZVGj#9_+itjo`3P&V(lP-iuZCYJ3Ux+VGz?oyZYDVw! z|KfXvo}J#%aA1*cqy1qfZl2NvE1fN>+YdR#7Fp}8coG<)q562kx>uUJAKBzuG8Ayu VbXBTb{RFy?!PC{xWt~$(697pmVmbf- diff --git a/resources/images/pajaDank.png b/resources/pajaDank.png similarity index 100% rename from resources/images/pajaDank.png rename to resources/pajaDank.png diff --git a/resources/resources.qrc b/resources/resources.qrc index 129713804..8568b5032 100644 --- a/resources/resources.qrc +++ b/resources/resources.qrc @@ -1,82 +1,5 @@ - - images/AppearanceEditorPart_16x.png - images/BrowserLink_16x.png - images/cheer1.png - images/cheer100.png - images/cheer1000.png - images/cheer10000.png - images/cheer100000.png - images/cheer5000.png - images/verified.png - images/CopyLongTextToClipboard_16x.png - images/CustomActionEditor_16x.png - images/Emoji_Color_1F60A_19.png - images/Filter_16x.png - images/format_Bold_16xLG.png - images/Message_16xLG.png - images/settings.png - images/tool_moreCollapser_off16.png - images/twitchprime_bg.png - qss/settings.qss - images/admin_bg.png - images/broadcaster_bg.png - images/globalmod_bg.png - images/moderator_bg.png - images/staff_bg.png - images/turbo_bg.png - emojidata.txt - images/button_ban.png - images/button_timeout.png - images/StatusAnnotations_Blocked_16xLG_color.png - images/UserProfile_22x.png - images/VSO_Link_blue_16x.png - sounds/ping2.wav - images/subscriber.png - images/collapse.png - images/emote.svg - images/notifications.svg - images/behave.svg - images/theme.svg - images/accounts.svg - images/chatterino2.icns - images/icon.png - images/commands.svg - images/aboutlogo.png - images/about.svg - images/moderatormode_disabled.png - images/moderatormode_enabled.png - images/split/splitdown.png - images/split/splitleft.png - images/split/splitright.png - images/split/splitup.png - images/split/splitmove.png - licenses/boost_boost.txt - licenses/fmt_bsd2.txt - licenses/libcommuni_BSD3.txt - licenses/openssl.txt - licenses/pajlada_settings.txt - licenses/pajlada_signals.txt - licenses/qt_lgpl-3.0.txt - licenses/rapidjson.txt - licenses/websocketpp.txt - emoji.json - images/buttons/ban.png - images/buttons/mod.png - images/buttons/unban.png - images/buttons/unmod.png - images/emote_dark.svg - tlds.txt - images/menu_black.png - images/menu_white.png - contributors.txt - avatars/fourtf.png - avatars/pajlada.png - images/download_update.png - images/download_update_error.png - images/pajaDank.png - - - qt.conf - + + qt.conf + diff --git a/resources/resources_autogenerated.qrc b/resources/resources_autogenerated.qrc new file mode 100644 index 000000000..6fc9e6f68 --- /dev/null +++ b/resources/resources_autogenerated.qrc @@ -0,0 +1,64 @@ + + pajaDank.png + icon.png + emojidata.txt + contributors.txt + error.png + emoji.json + icon.ico + tlds.txt + chatterino2.icns + qss/settings.qss + __pycache__/_generate_resources.cpython-36.pyc + licenses/fmt_bsd2.txt + licenses/openssl.txt + licenses/pajlada_settings.txt + licenses/qt_lgpl-3.0.txt + licenses/pajlada_signals.txt + licenses/rapidjson.txt + licenses/websocketpp.txt + licenses/boost_boost.txt + licenses/libcommuni_BSD3.txt + settings/aboutlogo.png + settings/behave.svg + settings/accounts.svg + settings/about.svg + settings/notifications.svg + settings/commands.svg + settings/theme.svg + split/up.png + split/left.png + split/move.png + split/right.png + split/down.png + buttons/unban.png + buttons/menuDark.png + buttons/mod.png + buttons/emote.svg + buttons/modModeEnabled2.png + buttons/ban.png + buttons/unmod.png + buttons/emoteDark.svg + buttons/updateError.png + buttons/modModeDisabled.png + buttons/modModeDisabled2.png + buttons/modModeEnabled.png + buttons/menuLight.png + buttons/update.png + buttons/timeout.png + buttons/banRed.png + sounds/ping2.wav + twitch/prime.png + twitch/verified.png + twitch/admin.png + twitch/subscriber.png + twitch/turbo.png + twitch/moderator.png + twitch/globalmod.png + twitch/cheer1.png + twitch/broadcaster.png + twitch/staff.png + avatars/fourtf.png + avatars/pajlada.png + + \ No newline at end of file diff --git a/resources/images/about.svg b/resources/settings/about.svg similarity index 100% rename from resources/images/about.svg rename to resources/settings/about.svg diff --git a/resources/images/aboutlogo.png b/resources/settings/aboutlogo.png similarity index 100% rename from resources/images/aboutlogo.png rename to resources/settings/aboutlogo.png diff --git a/resources/images/accounts.svg b/resources/settings/accounts.svg similarity index 100% rename from resources/images/accounts.svg rename to resources/settings/accounts.svg diff --git a/resources/images/behave.svg b/resources/settings/behave.svg similarity index 100% rename from resources/images/behave.svg rename to resources/settings/behave.svg diff --git a/resources/images/commands.svg b/resources/settings/commands.svg similarity index 100% rename from resources/images/commands.svg rename to resources/settings/commands.svg diff --git a/resources/images/notifications.svg b/resources/settings/notifications.svg similarity index 100% rename from resources/images/notifications.svg rename to resources/settings/notifications.svg diff --git a/resources/images/theme.svg b/resources/settings/theme.svg similarity index 100% rename from resources/images/theme.svg rename to resources/settings/theme.svg diff --git a/resources/images/split/splitdown.png b/resources/split/down.png similarity index 100% rename from resources/images/split/splitdown.png rename to resources/split/down.png diff --git a/resources/images/split/splitleft.png b/resources/split/left.png similarity index 100% rename from resources/images/split/splitleft.png rename to resources/split/left.png diff --git a/resources/images/split/splitmove.png b/resources/split/move.png similarity index 100% rename from resources/images/split/splitmove.png rename to resources/split/move.png diff --git a/resources/images/split/splitright.png b/resources/split/right.png similarity index 100% rename from resources/images/split/splitright.png rename to resources/split/right.png diff --git a/resources/images/split/splitup.png b/resources/split/up.png similarity index 100% rename from resources/images/split/splitup.png rename to resources/split/up.png diff --git a/resources/images/admin_bg.png b/resources/twitch/admin.png similarity index 100% rename from resources/images/admin_bg.png rename to resources/twitch/admin.png diff --git a/resources/images/broadcaster_bg.png b/resources/twitch/broadcaster.png similarity index 100% rename from resources/images/broadcaster_bg.png rename to resources/twitch/broadcaster.png diff --git a/resources/images/cheer1.png b/resources/twitch/cheer1.png similarity index 100% rename from resources/images/cheer1.png rename to resources/twitch/cheer1.png diff --git a/resources/images/globalmod_bg.png b/resources/twitch/globalmod.png similarity index 100% rename from resources/images/globalmod_bg.png rename to resources/twitch/globalmod.png diff --git a/resources/images/moderator_bg.png b/resources/twitch/moderator.png similarity index 100% rename from resources/images/moderator_bg.png rename to resources/twitch/moderator.png diff --git a/resources/images/twitchprime_bg.png b/resources/twitch/prime.png similarity index 100% rename from resources/images/twitchprime_bg.png rename to resources/twitch/prime.png diff --git a/resources/images/staff_bg.png b/resources/twitch/staff.png similarity index 100% rename from resources/images/staff_bg.png rename to resources/twitch/staff.png diff --git a/resources/images/subscriber.png b/resources/twitch/subscriber.png similarity index 100% rename from resources/images/subscriber.png rename to resources/twitch/subscriber.png diff --git a/resources/images/turbo_bg.png b/resources/twitch/turbo.png similarity index 100% rename from resources/images/turbo_bg.png rename to resources/twitch/turbo.png diff --git a/resources/images/verified.png b/resources/twitch/verified.png similarity index 100% rename from resources/images/verified.png rename to resources/twitch/verified.png diff --git a/src/Application.cpp b/src/Application.cpp index 9068bf9a8..b7d651aa4 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -6,9 +6,10 @@ #include "controllers/ignores/IgnoreController.hpp" #include "controllers/moderationactions/ModerationActions.hpp" #include "controllers/taggedusers/TaggedUsersController.hpp" +#include "providers/bttv/BttvEmotes.hpp" +#include "providers/ffz/FfzEmotes.hpp" #include "providers/twitch/PubsubClient.hpp" #include "providers/twitch/TwitchServer.hpp" -#include "singletons/Emotes.hpp" #include "singletons/Fonts.hpp" #include "singletons/Logging.hpp" #include "singletons/NativeMessaging.hpp" @@ -24,69 +25,75 @@ namespace chatterino { -static std::atomic isAppConstructed{false}; static std::atomic isAppInitialized{false}; -static Application *staticApp = nullptr; +Application *Application::instance = nullptr; // this class is responsible for handling the workflow of Chatterino // It will create the instances of the major classes, and connect their signals to each other -Application::Application(int _argc, char **_argv) - : argc_(_argc) - , argv_(_argv) +Application::Application(Settings &_settings, Paths &_paths) + : settings(&_settings) + , paths(&_paths) + , resources(&this->emplace()) + + , themes(&this->emplace()) + , fonts(&this->emplace()) + , emotes(&this->emplace()) + , windows(&this->emplace()) + + , accounts(&this->emplace()) + , commands(&this->emplace()) + , highlights(&this->emplace()) + , ignores(&this->emplace()) + , taggedUsers(&this->emplace()) + , moderationActions(&this->emplace()) + , twitch2(&this->emplace()) + , logging(&this->emplace()) { - getSettings()->initialize(); - getSettings()->load(); -} + this->instance = this; -void Application::construct() -{ - assert(isAppConstructed == false); - isAppConstructed = true; + this->fonts->fontChanged.connect([this]() { this->windows->layoutChannelViews(); }); - // 1. Instantiate all classes - this->settings = getSettings(); - this->paths = getPaths(); - - this->addSingleton(this->themes = new Theme); - this->addSingleton(this->windows = new WindowManager); - this->addSingleton(this->logging = new Logging); - this->addSingleton(this->commands = new CommandController); - this->addSingleton(this->highlights = new HighlightController); - this->addSingleton(this->ignores = new IgnoreController); - this->addSingleton(this->taggedUsers = new TaggedUsersController); - this->addSingleton(this->accounts = new AccountController); - this->addSingleton(this->emotes = new Emotes); - this->addSingleton(this->fonts = new Fonts); - this->addSingleton(this->resources = new Resources); - this->addSingleton(this->moderationActions = new ModerationActions); - - this->addSingleton(this->twitch2 = new TwitchServer); this->twitch.server = this->twitch2; this->twitch.pubsub = this->twitch2->pubsub; } -void Application::instantiate(int argc, char **argv) -{ - assert(staticApp == nullptr); - - staticApp = new Application(argc, argv); -} - -void Application::initialize() +void Application::initialize(Settings &settings, Paths &paths) { assert(isAppInitialized == false); isAppInitialized = true; - // 2. Initialize/load classes - for (Singleton *singleton : this->singletons_) { - singleton->initialize(*this); + for (auto &singleton : this->singletons_) { + singleton->initialize(settings, paths); } - // XXX this->windows->updateWordTypeMask(); + this->initNm(); + this->initPubsub(); +} + +int Application::run(QApplication &qtApp) +{ + assert(isAppInitialized); + + this->twitch.server->connect(); + + this->windows->getMainWindow().show(); + + return qtApp.exec(); +} + +void Application::save() +{ + for (auto &singleton : this->singletons_) { + singleton->save(); + } +} + +void Application::initNm() +{ #ifdef Q_OS_WIN #ifdef QT_DEBUG #ifdef C_DEBUG_NM @@ -98,7 +105,10 @@ void Application::initialize() this->nativeMessaging->openGuiMessageQueue(); #endif #endif +} +void Application::initPubsub() +{ this->twitch.pubsub->signals_.whisper.sent.connect([](const auto &msg) { Log("WHISPER SENT LOL"); // }); @@ -197,39 +207,11 @@ void Application::initialize() RequestModerationActions(); } -int Application::run(QApplication &qtApp) -{ - // Start connecting to the IRC Servers (Twitch only for now) - this->twitch.server->connect(); - - // Show main window - this->windows->getMainWindow().show(); - - return qtApp.exec(); -} - -void Application::save() -{ - for (Singleton *singleton : this->singletons_) { - singleton->save(); - } -} - -void Application::addSingleton(Singleton *singleton) -{ - this->singletons_.push_back(singleton); -} - Application *getApp() { - assert(staticApp != nullptr); + assert(Application::instance != nullptr); - return staticApp; -} - -bool appInitialized() -{ - return isAppInitialized; + return Application::instance; } } // namespace chatterino diff --git a/src/Application.hpp b/src/Application.hpp index 558ae7b73..c378715f5 100644 --- a/src/Application.hpp +++ b/src/Application.hpp @@ -1,13 +1,13 @@ #pragma once +#include "common/Singleton.hpp" #include "singletons/Resources.hpp" #include +#include namespace chatterino { -class Singleton; - class TwitchServer; class PubSub; @@ -24,45 +24,47 @@ class Logging; class Paths; class AccountManager; class Emotes; -class NativeMessaging; class Settings; class Fonts; class Resources; class Application { - Application(int _argc, char **_argv); + std::vector> singletons_; + int argc_; + char **argv_; public: - static void instantiate(int argc_, char **argv_); + static Application *instance; - ~Application() = delete; + Application(Settings &settings, Paths &paths); - void construct(); - void initialize(); + void initialize(Settings &settings, Paths &paths); void load(); + void save(); int run(QApplication &qtApp); friend void test(); - Settings *settings = nullptr; - Paths *paths = nullptr; + Settings *const settings = nullptr; + Paths *const paths = nullptr; + Resources2 *const resources; - Theme *themes = nullptr; - WindowManager *windows = nullptr; - Logging *logging = nullptr; - CommandController *commands = nullptr; - HighlightController *highlights = nullptr; - IgnoreController *ignores = nullptr; - TaggedUsersController *taggedUsers = nullptr; - AccountController *accounts = nullptr; - Emotes *emotes = nullptr; - NativeMessaging *nativeMessaging = nullptr; - Fonts *fonts = nullptr; - Resources *resources = nullptr; - ModerationActions *moderationActions = nullptr; - TwitchServer *twitch2 = nullptr; + Theme *const themes = nullptr; + Fonts *const fonts = nullptr; + Emotes *const emotes = nullptr; + WindowManager *const windows = nullptr; + + AccountController *const accounts = nullptr; + CommandController *const commands = nullptr; + HighlightController *const highlights = nullptr; + IgnoreController *const ignores = nullptr; + TaggedUsersController *const taggedUsers = nullptr; + ModerationActions *const moderationActions = nullptr; + TwitchServer *const twitch2 = nullptr; + + [[deprecated]] Logging *const logging = nullptr; /// Provider-specific struct { @@ -70,22 +72,20 @@ public: [[deprecated("use twitch2->pubsub instead")]] PubSub *pubsub = nullptr; } twitch; - void save(); - - // Special application mode that only initializes the native messaging host - static void runNativeMessagingHost(); - private: void addSingleton(Singleton *singleton); + void initPubsub(); + void initNm(); - int argc_; - char **argv_; - - std::vector singletons_; + template ::value>> + T &emplace() + { + auto t = new T; + this->singletons_.push_back(std::unique_ptr(t)); + return *t; + } }; Application *getApp(); -bool appInitialized(); - } // namespace chatterino diff --git a/src/BrowserExtension.cpp b/src/BrowserExtension.cpp new file mode 100644 index 000000000..3440170ab --- /dev/null +++ b/src/BrowserExtension.cpp @@ -0,0 +1,88 @@ +#include "BrowserExtension.hpp" + +#include "singletons/NativeMessaging.hpp" + +#include +#include +#include +#include +#include + +#ifdef Q_OS_WIN +#include +#include +#include +#endif + +namespace chatterino { + +namespace { +void initFileMode() +{ +#ifdef Q_OS_WIN + _setmode(_fileno(stdin), _O_BINARY); + _setmode(_fileno(stdout), _O_BINARY); +#endif +} + +void runLoop(NativeMessagingClient &client) +{ + while (true) { + char size_c[4]; + std::cin.read(size_c, 4); + + if (std::cin.eof()) { + break; + } + + uint32_t size = *reinterpret_cast(size_c); + +#if 0 + bool bigEndian = isBigEndian(); + // To avoid breaking strict-aliasing rules and potentially inducing undefined behaviour, the following code can be run instead + uint32_t size = 0; + if (bigEndian) { + size = size_c[3] | static_cast(size_c[2]) << 8 | + static_cast(size_c[1]) << 16 | static_cast(size_c[0]) << 24; + } else { + size = size_c[0] | static_cast(size_c[1]) << 8 | + static_cast(size_c[2]) << 16 | static_cast(size_c[3]) << 24; + } +#endif + + std::unique_ptr b(new char[size + 1]); + std::cin.read(b.get(), size); + *(b.get() + size) = '\0'; + + client.sendMessage(QByteArray::fromRawData(b.get(), static_cast(size))); + } +} +} // namespace + +bool shouldRunBrowserExtensionHost(const QStringList &args) +{ + return args.size() > 0 && + (args[0].startsWith("chrome-extension://") || args[0].endsWith(".json")); +} + +void runBrowserExtensionHost() +{ + initFileMode(); + + std::atomic ping(false); + + QTimer timer; + QObject::connect(&timer, &QTimer::timeout, [&ping] { + if (!ping.exchange(false)) { + _Exit(0); + } + }); + timer.setInterval(11000); + timer.start(); + + NativeMessagingClient client; + + runLoop(client); +} + +} // namespace chatterino diff --git a/src/BrowserExtension.hpp b/src/BrowserExtension.hpp new file mode 100644 index 000000000..0232ac2b8 --- /dev/null +++ b/src/BrowserExtension.hpp @@ -0,0 +1,10 @@ +#pragma once + +class QStringList; + +namespace chatterino { + +bool shouldRunBrowserExtensionHost(const QStringList &args); +void runBrowserExtensionHost(); + +} // namespace chatterino diff --git a/src/RunGui.cpp b/src/RunGui.cpp new file mode 100644 index 000000000..a15bce6bc --- /dev/null +++ b/src/RunGui.cpp @@ -0,0 +1,130 @@ +#include "RunGui.hpp" + +#include +#include +#include +#include + +#include "Application.hpp" +#include "common/NetworkManager.hpp" +#include "singletons/Paths.hpp" +#include "singletons/Updates.hpp" +#include "widgets/dialogs/LastRunCrashDialog.hpp" + +#ifdef C_USE_BREAKPAD +#include +#endif + +// void initQt(); +// void installCustomPalette(); +// void showLastCrashDialog(); +// void createRunningFile(const QString &path); +// void removeRunningFile(const QString &path); + +namespace chatterino { +namespace { +void installCustomPalette() +{ + // borrowed from + // https://stackoverflow.com/questions/15035767/is-the-qt-5-dark-fusion-theme-available-for-windows + QPalette darkPalette = qApp->palette(); + + darkPalette.setColor(QPalette::Window, QColor(22, 22, 22)); + darkPalette.setColor(QPalette::WindowText, Qt::white); + darkPalette.setColor(QPalette::Text, Qt::white); + darkPalette.setColor(QPalette::Disabled, QPalette::WindowText, QColor(127, 127, 127)); + darkPalette.setColor(QPalette::Base, QColor("#333")); + darkPalette.setColor(QPalette::AlternateBase, QColor("#444")); + darkPalette.setColor(QPalette::ToolTipBase, Qt::white); + darkPalette.setColor(QPalette::ToolTipText, Qt::white); + darkPalette.setColor(QPalette::Disabled, QPalette::Text, QColor(127, 127, 127)); + darkPalette.setColor(QPalette::Dark, QColor(35, 35, 35)); + darkPalette.setColor(QPalette::Shadow, QColor(20, 20, 20)); + darkPalette.setColor(QPalette::Button, QColor(70, 70, 70)); + darkPalette.setColor(QPalette::ButtonText, Qt::white); + darkPalette.setColor(QPalette::Disabled, QPalette::ButtonText, QColor(127, 127, 127)); + darkPalette.setColor(QPalette::BrightText, Qt::red); + darkPalette.setColor(QPalette::Link, QColor(42, 130, 218)); + darkPalette.setColor(QPalette::Highlight, QColor(42, 130, 218)); + darkPalette.setColor(QPalette::Disabled, QPalette::Highlight, QColor(80, 80, 80)); + darkPalette.setColor(QPalette::HighlightedText, Qt::white); + darkPalette.setColor(QPalette::Disabled, QPalette::HighlightedText, QColor(127, 127, 127)); + + qApp->setPalette(darkPalette); +} + +void initQt() +{ + // set up the QApplication flags + QApplication::setAttribute(Qt::AA_Use96Dpi, true); +#ifdef Q_OS_WIN32 + QApplication::setAttribute(Qt::AA_DisableHighDpiScaling, true); +#endif + + QApplication::setStyle(QStyleFactory::create("Fusion")); + + installCustomPalette(); +} + +void showLastCrashDialog() +{ +#ifndef C_DISABLE_CRASH_DIALOG + LastRunCrashDialog dialog; + + switch (dialog.exec()) { + case QDialog::Accepted: { + }; break; + default: { + _exit(0); + } + } +#endif +} + +void createRunningFile(const QString &path) +{ + QFile runningFile(path); + + runningFile.open(QIODevice::WriteOnly | QIODevice::Truncate); + runningFile.flush(); + runningFile.close(); +} + +void removeRunningFile(const QString &path) +{ + QFile::remove(path); +} +} // namespace + +void runGui(QApplication &a, Paths &paths, Settings &settings) +{ + chatterino::NetworkManager::init(); + chatterino::Updates::getInstance().checkForUpdates(); + +#ifdef C_USE_BREAKPAD + QBreakpadInstance.setDumpPath(app->paths->settingsFolderPath + "/Crashes"); +#endif + + // Running file + auto runningPath = paths.miscDirectory + "/running_" + paths.applicationFilePathHash; + + if (QFile::exists(runningPath)) { + showLastCrashDialog(); + } else { + createRunningFile(runningPath); + } + + Application app(settings, paths); + app.initialize(settings, paths); + app.run(a); + app.save(); + + removeRunningFile(runningPath); + + pajlada::Settings::SettingManager::gSave(); + + chatterino::NetworkManager::deinit(); + + _exit(0); +} +} // namespace chatterino diff --git a/src/RunGui.hpp b/src/RunGui.hpp new file mode 100644 index 000000000..338164404 --- /dev/null +++ b/src/RunGui.hpp @@ -0,0 +1,10 @@ +#pragma once + +class QApplication; + +namespace chatterino { +class Paths; +class Settings; + +void runGui(QApplication &a, Paths &paths, Settings &settings); +} // namespace chatterino diff --git a/src/autogenerated/ResourcesAutogen.cpp b/src/autogenerated/ResourcesAutogen.cpp new file mode 100644 index 000000000..c5035f2e6 --- /dev/null +++ b/src/autogenerated/ResourcesAutogen.cpp @@ -0,0 +1,44 @@ +#include "ResourcesAutogen.hpp" + +namespace chatterino { + +Resources2::Resources2() +{ + this->avatars.fourtf = QPixmap(":/avatars/fourtf.png"); + this->avatars.pajlada = QPixmap(":/avatars/pajlada.png"); + this->buttons.ban = QPixmap(":/buttons/ban.png"); + this->buttons.banRed = QPixmap(":/buttons/banRed.png"); + this->buttons.menuDark = QPixmap(":/buttons/menuDark.png"); + this->buttons.menuLight = QPixmap(":/buttons/menuLight.png"); + this->buttons.mod = QPixmap(":/buttons/mod.png"); + this->buttons.modModeDisabled = QPixmap(":/buttons/modModeDisabled.png"); + this->buttons.modModeDisabled2 = QPixmap(":/buttons/modModeDisabled2.png"); + this->buttons.modModeEnabled = QPixmap(":/buttons/modModeEnabled.png"); + this->buttons.modModeEnabled2 = QPixmap(":/buttons/modModeEnabled2.png"); + this->buttons.timeout = QPixmap(":/buttons/timeout.png"); + this->buttons.unban = QPixmap(":/buttons/unban.png"); + this->buttons.unmod = QPixmap(":/buttons/unmod.png"); + this->buttons.update = QPixmap(":/buttons/update.png"); + this->buttons.updateError = QPixmap(":/buttons/updateError.png"); + this->error = QPixmap(":/error.png"); + this->icon = QPixmap(":/icon.png"); + this->pajaDank = QPixmap(":/pajaDank.png"); + this->settings.aboutlogo = QPixmap(":/settings/aboutlogo.png"); + this->split.down = QPixmap(":/split/down.png"); + this->split.left = QPixmap(":/split/left.png"); + this->split.move = QPixmap(":/split/move.png"); + this->split.right = QPixmap(":/split/right.png"); + this->split.up = QPixmap(":/split/up.png"); + this->twitch.admin = QPixmap(":/twitch/admin.png"); + this->twitch.broadcaster = QPixmap(":/twitch/broadcaster.png"); + this->twitch.cheer1 = QPixmap(":/twitch/cheer1.png"); + this->twitch.globalmod = QPixmap(":/twitch/globalmod.png"); + this->twitch.moderator = QPixmap(":/twitch/moderator.png"); + this->twitch.prime = QPixmap(":/twitch/prime.png"); + this->twitch.staff = QPixmap(":/twitch/staff.png"); + this->twitch.subscriber = QPixmap(":/twitch/subscriber.png"); + this->twitch.turbo = QPixmap(":/twitch/turbo.png"); + this->twitch.verified = QPixmap(":/twitch/verified.png"); +} + +} // namespace chatterino \ No newline at end of file diff --git a/src/autogenerated/ResourcesAutogen.hpp b/src/autogenerated/ResourcesAutogen.hpp new file mode 100644 index 000000000..048a1a94b --- /dev/null +++ b/src/autogenerated/ResourcesAutogen.hpp @@ -0,0 +1,57 @@ +#include +#include "common/Singleton.hpp" + +namespace chatterino { + +class Resources2 : public Singleton { +public: + Resources2(); + + struct { + QPixmap fourtf; + QPixmap pajlada; + } avatars; + struct { + QPixmap ban; + QPixmap banRed; + QPixmap menuDark; + QPixmap menuLight; + QPixmap mod; + QPixmap modModeDisabled; + QPixmap modModeDisabled2; + QPixmap modModeEnabled; + QPixmap modModeEnabled2; + QPixmap timeout; + QPixmap unban; + QPixmap unmod; + QPixmap update; + QPixmap updateError; + } buttons; + QPixmap error; + QPixmap icon; + QPixmap pajaDank; + struct { + QPixmap aboutlogo; + } settings; + struct { + QPixmap down; + QPixmap left; + QPixmap move; + QPixmap right; + QPixmap up; + } split; + struct { + QPixmap admin; + QPixmap broadcaster; + QPixmap cheer1; + QPixmap globalmod; + QPixmap moderator; + QPixmap prime; + QPixmap staff; + QPixmap subscriber; + QPixmap turbo; + QPixmap verified; + } twitch; +}; + +} // namespace chatterino \ No newline at end of file diff --git a/src/common/Aliases.hpp b/src/common/Aliases.hpp new file mode 100644 index 000000000..8a61a9639 --- /dev/null +++ b/src/common/Aliases.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include + +#define QStringAlias(name) \ + namespace chatterino { \ + struct name { \ + QString string; \ + bool operator==(const name &other) const \ + { \ + return this->string == other.string; \ + } \ + bool operator!=(const name &other) const \ + { \ + return this->string != other.string; \ + } \ + }; \ + } /* namespace chatterino */ \ + namespace std { \ + template <> \ + struct hash { \ + size_t operator()(const chatterino::name &s) const \ + { \ + return qHash(s.string); \ + } \ + }; \ + } /* namespace std */ + +QStringAlias(UserName); +QStringAlias(UserId); +QStringAlias(Url); +QStringAlias(Tooltip); diff --git a/src/common/Channel.cpp b/src/common/Channel.cpp index a0ffc511f..c03019c43 100644 --- a/src/common/Channel.cpp +++ b/src/common/Channel.cpp @@ -17,9 +17,9 @@ namespace chatterino { -Channel::Channel(const QString &_name, Type type) - : name(_name) - , completionModel(this->name) +Channel::Channel(const QString &name, Type type) + : completionModel(name) + , name_(name) , type_(type) { QObject::connect(&this->clearCompletionModelTimer_, &QTimer::timeout, [this]() { @@ -38,6 +38,11 @@ Channel::Type Channel::getType() const return this->type_; } +const QString &Channel::getName() const +{ + return this->name_; +} + bool Channel::isTwitchChannel() const { return this->type_ >= Type::Twitch && this->type_ < Type::TwitchEnd; @@ -45,7 +50,7 @@ bool Channel::isTwitchChannel() const bool Channel::isEmpty() const { - return this->name.isEmpty(); + return this->name_.isEmpty(); } LimitedQueueSnapshot Channel::getMessageSnapshot() @@ -66,7 +71,7 @@ void Channel::addMessage(MessagePtr message) // FOURTF: change this when adding more providers if (this->isTwitchChannel()) { - app->logging->addMessage(this->name, message); + app->logging->addMessage(this->name_, message); } if (this->messages_.pushBack(message, deleted)) { diff --git a/src/common/Channel.hpp b/src/common/Channel.hpp index 99172c108..e153914c1 100644 --- a/src/common/Channel.hpp +++ b/src/common/Channel.hpp @@ -41,6 +41,7 @@ public: pajlada::Signals::NoArgSignal destroyed; Type getType() const; + const QString &getName() const; bool isTwitchChannel() const; virtual bool isEmpty() const; LimitedQueueSnapshot getMessageSnapshot(); @@ -52,7 +53,6 @@ public: void replaceMessage(MessagePtr message, MessagePtr replacement); virtual void addRecentChatter(const std::shared_ptr &message); - QString name; QStringList modList; virtual bool canSendMessage() const; @@ -72,6 +72,7 @@ protected: virtual void onConnected(); private: + const QString name_; LimitedQueue messages_; Type type_; QTimer clearCompletionModelTimer_; diff --git a/src/common/Common.hpp b/src/common/Common.hpp index 3a573163d..18548cb3b 100644 --- a/src/common/Common.hpp +++ b/src/common/Common.hpp @@ -1,9 +1,13 @@ #pragma once +#include "common/Aliases.hpp" +#include "common/Outcome.hpp" +#include "common/ProviderId.hpp" #include "debug/Log.hpp" #include #include +#include #include #include @@ -27,14 +31,18 @@ const Qt::KeyboardModifiers showResizeHandlesModifiers = Qt::ControlModifier; static const char *ANONYMOUS_USERNAME_LABEL ATTR_UNUSED = " - anonymous - "; -#define return_if(condition) \ - if ((condition)) { \ - return; \ - } +template +std::weak_ptr weakOf(T *element) +{ + return element->shared_from_this(); +} -#define return_unless(condition) \ - if (!(condition)) { \ - return; \ - } +template +struct overloaded : Ts... { + using Ts::operator()...; +}; + +template +overloaded(Ts...)->overloaded; } // namespace chatterino diff --git a/src/common/CompletionModel.cpp b/src/common/CompletionModel.cpp index de5bafce8..f56425d27 100644 --- a/src/common/CompletionModel.cpp +++ b/src/common/CompletionModel.cpp @@ -2,8 +2,10 @@ #include "Application.hpp" #include "common/Common.hpp" +#include "controllers/accounts/AccountController.hpp" #include "controllers/commands/CommandController.hpp" #include "debug/Log.hpp" +#include "providers/twitch/TwitchServer.hpp" #include "singletons/Emotes.hpp" #include @@ -107,41 +109,45 @@ void CompletionModel::refresh() auto app = getApp(); // User-specific: Twitch Emotes - // TODO: Fix this so it properly updates with the proper api. oauth token needs proper scope - for (const auto &m : app->emotes->twitch.emotes) { - for (const auto &emoteName : m.second.emoteCodes) { + if (auto account = app->accounts->twitch.getCurrent()) { + for (const auto &emote : account->accessEmotes()->allEmoteNames) { // XXX: No way to discern between a twitch global emote and sub emote right now - this->addString(emoteName, TaggedString::Type::TwitchGlobalEmote); + this->addString(emote.string, TaggedString::Type::TwitchGlobalEmote); } } - // Global: BTTV Global Emotes - std::vector &bttvGlobalEmoteCodes = app->emotes->bttv.globalEmoteCodes; - for (const auto &m : bttvGlobalEmoteCodes) { - this->addString(m, TaggedString::Type::BTTVGlobalEmote); + // // Global: BTTV Global Emotes + // std::vector &bttvGlobalEmoteCodes = app->emotes->bttv.globalEmoteNames_; + // for (const auto &m : bttvGlobalEmoteCodes) { + // this->addString(m, TaggedString::Type::BTTVGlobalEmote); + // } + + // // Global: FFZ Global Emotes + // std::vector &ffzGlobalEmoteCodes = app->emotes->ffz.globalEmoteCodes; + // for (const auto &m : ffzGlobalEmoteCodes) { + // this->addString(m, TaggedString::Type::FFZGlobalEmote); + // } + + // Channel emotes + if (auto channel = dynamic_cast( + getApp()->twitch2->getChannelOrEmptyByID(this->channelName_).get())) { + auto bttv = channel->accessBttvEmotes(); + // auto it = bttv->begin(); + // for (const auto &emote : *bttv) { + // } + // std::vector &bttvChannelEmoteCodes = + // app->emotes->bttv.channelEmoteName_[this->channelName_]; + // for (const auto &m : bttvChannelEmoteCodes) { + // this->addString(m, TaggedString::Type::BTTVChannelEmote); + // } + + // Channel-specific: FFZ Channel Emotes + for (const auto &emote : *channel->accessFfzEmotes()) { + this->addString(emote.second->name.string, TaggedString::Type::FFZChannelEmote); + } } - // Global: FFZ Global Emotes - std::vector &ffzGlobalEmoteCodes = app->emotes->ffz.globalEmoteCodes; - for (const auto &m : ffzGlobalEmoteCodes) { - this->addString(m, TaggedString::Type::FFZGlobalEmote); - } - - // Channel-specific: BTTV Channel Emotes - std::vector &bttvChannelEmoteCodes = - app->emotes->bttv.channelEmoteCodes[this->channelName_]; - for (const auto &m : bttvChannelEmoteCodes) { - this->addString(m, TaggedString::Type::BTTVChannelEmote); - } - - // Channel-specific: FFZ Channel Emotes - std::vector &ffzChannelEmoteCodes = - app->emotes->ffz.channelEmoteCodes[this->channelName_]; - for (const auto &m : ffzChannelEmoteCodes) { - this->addString(m, TaggedString::Type::FFZChannelEmote); - } - - // Global: Emojis + // Emojis const auto &emojiShortCodes = app->emotes->emojis.shortCodes; for (const auto &m : emojiShortCodes) { this->addString(":" + m + ":", TaggedString::Type::Emoji); diff --git a/src/common/CompletionModel.hpp b/src/common/CompletionModel.hpp index 5bae63be0..d9baac7b3 100644 --- a/src/common/CompletionModel.hpp +++ b/src/common/CompletionModel.hpp @@ -10,6 +10,8 @@ namespace chatterino { +class TwitchChannel; + class CompletionModel : public QAbstractListModel { struct TaggedString { diff --git a/src/common/Emotemap.cpp b/src/common/Emotemap.cpp index b8903afd9..9bfae628b 100644 --- a/src/common/Emotemap.cpp +++ b/src/common/Emotemap.cpp @@ -5,42 +5,42 @@ namespace chatterino { -EmoteData::EmoteData(Image *image) - : image1x(image) -{ -} +// EmoteData::EmoteData(Image *image) +// : image1x(image) +//{ +//} -// Emotes must have a 1x image to be valid -bool EmoteData::isValid() const -{ - return this->image1x != nullptr; -} +//// Emotes must have a 1x image to be valid +// bool EmoteData::isValid() const +//{ +// return this->image1x != nullptr; +//} -Image *EmoteData::getImage(float scale) const -{ - int quality = getApp()->settings->preferredEmoteQuality; +// Image *EmoteData::getImage(float scale) const +//{ +// int quality = getApp()->settings->preferredEmoteQuality; - if (quality == 0) { - scale *= getApp()->settings->emoteScale.getValue(); - quality = [&] { - if (scale <= 1) - return 1; - if (scale <= 2) - return 2; - return 3; - }(); - } +// if (quality == 0) { +// scale *= getApp()->settings->emoteScale.getValue(); +// quality = [&] { +// if (scale <= 1) +// return 1; +// if (scale <= 2) +// return 2; +// return 3; +// }(); +// } - Image *_image; - if (quality == 3 && this->image3x != nullptr) { - _image = this->image3x; - } else if (quality >= 2 && this->image2x != nullptr) { - _image = this->image2x; - } else { - _image = this->image1x; - } +// Image *_image; +// if (quality == 3 && this->image3x != nullptr) { +// _image = this->image3x; +// } else if (quality >= 2 && this->image2x != nullptr) { +// _image = this->image2x; +// } else { +// _image = this->image1x; +// } - return _image; -} +// return _image; +//} } // namespace chatterino diff --git a/src/common/Emotemap.hpp b/src/common/Emotemap.hpp index d4010048d..c57f08e91 100644 --- a/src/common/Emotemap.hpp +++ b/src/common/Emotemap.hpp @@ -5,23 +5,23 @@ namespace chatterino { -struct EmoteData { - EmoteData() = default; +// struct EmoteData { +// EmoteData() = default; - EmoteData(Image *image); +// EmoteData(Image *image); - // Emotes must have a 1x image to be valid - bool isValid() const; - Image *getImage(float scale) const; +// // Emotes must have a 1x image to be valid +// bool isValid() const; +// Image *getImage(float scale) const; - // Link to the emote page i.e. https://www.frankerfacez.com/emoticon/144722-pajaCringe - QString pageLink; +// // Link to the emote page i.e. https://www.frankerfacez.com/emoticon/144722-pajaCringe +// QString pageLink; - Image *image1x = nullptr; - Image *image2x = nullptr; - Image *image3x = nullptr; -}; +// Image *image1x = nullptr; +// Image *image2x = nullptr; +// Image *image3x = nullptr; +//}; -using EmoteMap = ConcurrentMap; +// using EmoteMap = ConcurrentMap; } // namespace chatterino diff --git a/src/common/NetworkCommon.hpp b/src/common/NetworkCommon.hpp index 743eeee04..30d1e9267 100644 --- a/src/common/NetworkCommon.hpp +++ b/src/common/NetworkCommon.hpp @@ -2,13 +2,15 @@ #include +#include "Common.hpp" + class QNetworkReply; namespace chatterino { class NetworkResult; -using NetworkSuccessCallback = std::function; +using NetworkSuccessCallback = std::function; using NetworkErrorCallback = std::function; using NetworkReplyCreatedCallback = std::function; diff --git a/src/common/NetworkRequest.cpp b/src/common/NetworkRequest.cpp index 69c636128..976c0fa14 100644 --- a/src/common/NetworkRequest.cpp +++ b/src/common/NetworkRequest.cpp @@ -129,7 +129,7 @@ void NetworkRequest::execute() } } -bool NetworkRequest::tryLoadCachedFile() +Outcome NetworkRequest::tryLoadCachedFile() { auto app = getApp(); @@ -137,24 +137,24 @@ bool NetworkRequest::tryLoadCachedFile() if (!cachedFile.exists()) { // File didn't exist - return false; + return Failure; } if (!cachedFile.open(QIODevice::ReadOnly)) { // File could not be opened - return false; + return Failure; } QByteArray bytes = cachedFile.readAll(); NetworkResult result(bytes); - bool success = this->data->onSuccess_(result); + auto outcome = this->data->onSuccess_(result); cachedFile.close(); // XXX: If success is false, we should invalidate the cache file somehow/somewhere - return success; + return outcome; } void NetworkRequest::doRequest() diff --git a/src/common/NetworkRequest.hpp b/src/common/NetworkRequest.hpp index e2fd2e7e4..049c2cba1 100644 --- a/src/common/NetworkRequest.hpp +++ b/src/common/NetworkRequest.hpp @@ -33,7 +33,7 @@ public: explicit NetworkRequest(const std::string &url, NetworkRequestType requestType = NetworkRequestType::Get); - NetworkRequest(QUrl url, NetworkRequestType requestType = NetworkRequestType::Get); + explicit NetworkRequest(QUrl url, NetworkRequestType requestType = NetworkRequestType::Get); ~NetworkRequest(); @@ -58,7 +58,7 @@ private: // Returns true if the file was successfully loaded from cache // Returns false if the cache file either didn't exist, or it contained "invalid" data // "invalid" is specified by the onSuccess callback - bool tryLoadCachedFile(); + Outcome tryLoadCachedFile(); void doRequest(); diff --git a/src/common/NetworkResult.cpp b/src/common/NetworkResult.cpp index 6d8b70067..0620bf3c2 100644 --- a/src/common/NetworkResult.cpp +++ b/src/common/NetworkResult.cpp @@ -38,7 +38,7 @@ rapidjson::Document NetworkResult::parseRapidJson() const return ret; } -QByteArray NetworkResult::getData() const +const QByteArray &NetworkResult::getData() const { return this->data_; } diff --git a/src/common/NetworkResult.hpp b/src/common/NetworkResult.hpp index 5631e1fe4..36a23de22 100644 --- a/src/common/NetworkResult.hpp +++ b/src/common/NetworkResult.hpp @@ -7,14 +7,15 @@ namespace chatterino { class NetworkResult { - QByteArray data_; - public: NetworkResult(const QByteArray &data); QJsonObject parseJson() const; rapidjson::Document parseRapidJson() const; - QByteArray getData() const; + const QByteArray &getData() const; + +private: + QByteArray data_; }; } // namespace chatterino diff --git a/src/common/NullablePtr.hpp b/src/common/NullablePtr.hpp index f38ecfa5d..ba08bb7f9 100644 --- a/src/common/NullablePtr.hpp +++ b/src/common/NullablePtr.hpp @@ -1,5 +1,7 @@ #pragma once +#include + namespace chatterino { template @@ -23,7 +25,7 @@ public: return element_; } - T &operator*() const + typename std::add_lvalue_reference::type &operator*() const { assert(this->hasElement()); @@ -52,6 +54,17 @@ public: return this->hasElement(); } + bool operator!() const + { + return !this->hasElement(); + } + + template ::value>> + operator NullablePtr() const + { + return NullablePtr(this->element_); + } + private: T *element_; }; diff --git a/src/common/Outcome.hpp b/src/common/Outcome.hpp new file mode 100644 index 000000000..01be69fd8 --- /dev/null +++ b/src/common/Outcome.hpp @@ -0,0 +1,51 @@ +#pragma once + +namespace chatterino { + +struct SuccessTag { +}; + +struct FailureTag { +}; + +const SuccessTag Success{}; +const FailureTag Failure{}; + +class Outcome +{ +public: + Outcome(SuccessTag) + : success_(true) + { + } + + Outcome(FailureTag) + : success_(false) + { + } + + explicit operator bool() const + { + return this->success_; + } + + bool operator!() const + { + return !this->success_; + } + + bool operator==(const Outcome &other) const + { + return this->success_ == other.success_; + } + + bool operator!=(const Outcome &other) const + { + return !this->operator==(other); + } + +private: + bool success_; +}; + +} // namespace chatterino diff --git a/src/common/Singleton.hpp b/src/common/Singleton.hpp index 474c31f0f..a98dedac2 100644 --- a/src/common/Singleton.hpp +++ b/src/common/Singleton.hpp @@ -4,14 +4,18 @@ namespace chatterino { -class Application; +class Settings; +class Paths; class Singleton : boost::noncopyable { public: - virtual void initialize(Application &app) + virtual ~Singleton() = default; + + virtual void initialize(Settings &settings, Paths &paths) { - (void)(app); + (void)(settings); + (void)(paths); } virtual void save() diff --git a/src/common/UniqueAccess.hpp b/src/common/UniqueAccess.hpp index c4013af3e..ef7f0165f 100644 --- a/src/common/UniqueAccess.hpp +++ b/src/common/UniqueAccess.hpp @@ -1,12 +1,13 @@ #pragma once +#include #include #include namespace chatterino { template -class AccessGuard +class AccessGuard : boost::noncopyable { public: AccessGuard(T &element, std::mutex &mutex) @@ -21,31 +22,16 @@ public: this->mutex_.unlock(); } - const T *operator->() const + T *operator->() const { return &this->element_; } - T *operator->() - { - return &this->element_; - } - - const T &operator*() const + T &operator*() const { return this->element_; } - T &operator*() - { - return this->element_; - } - - T clone() const - { - return T(this->element_); - } - private: T &element_; std::mutex &mutex_; @@ -55,7 +41,7 @@ template class UniqueAccess { public: - template +// template UniqueAccess() : element_(T()) { @@ -83,14 +69,15 @@ public: return *this; } - AccessGuard access() + AccessGuard access() const { return AccessGuard(this->element_, this->mutex_); } - const AccessGuard access() const + template >> + AccessGuard accessConst() const { - return AccessGuard(this->element_, this->mutex_); + return AccessGuard(this->element_, this->mutex_); } private: diff --git a/src/controllers/accounts/AccountController.cpp b/src/controllers/accounts/AccountController.cpp index 0341a06a6..888c21152 100644 --- a/src/controllers/accounts/AccountController.cpp +++ b/src/controllers/accounts/AccountController.cpp @@ -33,7 +33,7 @@ AccountController::AccountController() }); } -void AccountController::initialize(Application &app) +void AccountController::initialize(Settings &settings, Paths &paths) { this->twitch.load(); } diff --git a/src/controllers/accounts/AccountController.hpp b/src/controllers/accounts/AccountController.hpp index 69fc7f092..25f0fee5c 100644 --- a/src/controllers/accounts/AccountController.hpp +++ b/src/controllers/accounts/AccountController.hpp @@ -11,16 +11,19 @@ namespace chatterino { +class Settings; +class Paths; + class AccountModel; -class AccountController : public Singleton +class AccountController final : public Singleton { public: AccountController(); AccountModel *createModel(QObject *parent); - virtual void initialize(Application &app) override; + virtual void initialize(Settings &settings, Paths &paths) override; TwitchAccountManager twitch; diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index 47c4e25db..1d85052e1 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -11,6 +11,7 @@ #include "providers/twitch/TwitchServer.hpp" #include "singletons/Paths.hpp" #include "singletons/Settings.hpp" +#include "util/CombinePath.hpp" #include "widgets/dialogs/LogsPopup.hpp" #include @@ -44,15 +45,14 @@ CommandController::CommandController() this->items.itemRemoved.connect(addFirstMatchToMap); } -void CommandController::initialize(Application &app) +void CommandController::initialize(Settings &, Paths &paths) { - this->load(); + this->load(paths); } -void CommandController::load() +void CommandController::load(Paths &paths) { - auto app = getApp(); - this->filePath_ = app->paths->settingsDirectory + "/commands.txt"; + this->filePath_ = combinePath(paths.settingsDirectory, "commands.txt"); QFile textFile(this->filePath_); if (!textFile.open(QIODevice::ReadOnly)) { diff --git a/src/controllers/commands/CommandController.hpp b/src/controllers/commands/CommandController.hpp index c1cbfd855..45aba5bcc 100644 --- a/src/controllers/commands/CommandController.hpp +++ b/src/controllers/commands/CommandController.hpp @@ -10,11 +10,14 @@ #include "controllers/commands/Command.hpp" namespace chatterino { + +class Settings; +class Paths; class Channel; class CommandModel; -class CommandController : public Singleton +class CommandController final : public Singleton { public: CommandController(); @@ -22,7 +25,7 @@ public: QString execCommand(const QString &text, std::shared_ptr channel, bool dryRun); QStringList getDefaultTwitchCommandList(); - virtual void initialize(Application &app) override; + virtual void initialize(Settings &settings, Paths &paths) override; virtual void save() override; CommandModel *createModel(QObject *parent); @@ -30,7 +33,7 @@ public: UnsortedSignalVector items; private: - void load(); + void load(Paths &paths); QMap commandsMap_; diff --git a/src/controllers/highlights/HighlightController.cpp b/src/controllers/highlights/HighlightController.cpp index 02c420c79..69cb3d681 100644 --- a/src/controllers/highlights/HighlightController.cpp +++ b/src/controllers/highlights/HighlightController.cpp @@ -12,7 +12,7 @@ HighlightController::HighlightController() { } -void HighlightController::initialize(Application &app) +void HighlightController::initialize(Settings &settings, Paths &paths) { assert(!this->initialized_); this->initialized_ = true; diff --git a/src/controllers/highlights/HighlightController.hpp b/src/controllers/highlights/HighlightController.hpp index a881499cf..8afac7947 100644 --- a/src/controllers/highlights/HighlightController.hpp +++ b/src/controllers/highlights/HighlightController.hpp @@ -10,16 +10,19 @@ namespace chatterino { +class Settings; +class Paths; + class UserHighlightModel; class HighlightModel; class HighlightBlacklistModel; -class HighlightController : public Singleton +class HighlightController final : public Singleton { public: HighlightController(); - virtual void initialize(Application &app) override; + virtual void initialize(Settings &settings, Paths &paths) override; UnsortedSignalVector phrases; UnsortedSignalVector blacklistedUsers; diff --git a/src/controllers/ignores/IgnoreController.cpp b/src/controllers/ignores/IgnoreController.cpp index 5aa271cb9..819779eff 100644 --- a/src/controllers/ignores/IgnoreController.cpp +++ b/src/controllers/ignores/IgnoreController.cpp @@ -7,7 +7,7 @@ namespace chatterino { -void IgnoreController::initialize(Application &) +void IgnoreController::initialize(Settings &, Paths &) { assert(!this->initialized_); this->initialized_ = true; diff --git a/src/controllers/ignores/IgnoreController.hpp b/src/controllers/ignores/IgnoreController.hpp index f03460131..09c109186 100644 --- a/src/controllers/ignores/IgnoreController.hpp +++ b/src/controllers/ignores/IgnoreController.hpp @@ -8,12 +8,15 @@ namespace chatterino { +class Settings; +class Paths; + class IgnoreModel; -class IgnoreController : public Singleton +class IgnoreController final : public Singleton { public: - virtual void initialize(Application &app) override; + virtual void initialize(Settings &settings, Paths &paths) override; UnsortedSignalVector phrases; diff --git a/src/controllers/moderationactions/ModerationAction.cpp b/src/controllers/moderationactions/ModerationAction.cpp index eb477602f..d3f07b172 100644 --- a/src/controllers/moderationactions/ModerationAction.cpp +++ b/src/controllers/moderationactions/ModerationAction.cpp @@ -1,5 +1,6 @@ #include "ModerationAction.hpp" +#include #include "Application.hpp" #include "singletons/Resources.hpp" @@ -57,7 +58,7 @@ ModerationAction::ModerationAction(const QString &action) // this->_moderationActions.emplace_back(app->resources->buttonTimeout, str); // } } else if (action.startsWith("/ban ")) { - this->image_ = getApp()->resources->buttonBan; + this->image_ = Image::fromNonOwningPixmap(&getApp()->resources->buttons.ban); } else { QString xD = action; @@ -75,10 +76,10 @@ bool ModerationAction::operator==(const ModerationAction &other) const bool ModerationAction::isImage() const { - return this->image_ != nullptr; + return bool(this->image_); } -Image *ModerationAction::getImage() const +const boost::optional &ModerationAction::getImage() const { return this->image_; } diff --git a/src/controllers/moderationactions/ModerationAction.hpp b/src/controllers/moderationactions/ModerationAction.hpp index bde1040c9..f509f79c5 100644 --- a/src/controllers/moderationactions/ModerationAction.hpp +++ b/src/controllers/moderationactions/ModerationAction.hpp @@ -1,14 +1,14 @@ #pragma once #include +#include #include +#include "messages/Image.hpp" #include "util/RapidjsonHelpers.hpp" namespace chatterino { -class Image; - class ModerationAction { public: @@ -17,13 +17,13 @@ public: bool operator==(const ModerationAction &other) const; bool isImage() const; - Image *getImage() const; + const boost::optional &getImage() const; const QString &getLine1() const; const QString &getLine2() const; const QString &getAction() const; private: - Image *image_ = nullptr; + boost::optional image_; QString line1_; QString line2_; QString action_; diff --git a/src/controllers/moderationactions/ModerationActions.cpp b/src/controllers/moderationactions/ModerationActions.cpp index c130e65ad..68070f6b2 100644 --- a/src/controllers/moderationactions/ModerationActions.cpp +++ b/src/controllers/moderationactions/ModerationActions.cpp @@ -12,7 +12,7 @@ ModerationActions::ModerationActions() { } -void ModerationActions::initialize(Application &app) +void ModerationActions::initialize(Settings &settings, Paths &paths) { assert(!this->initialized_); this->initialized_ = true; diff --git a/src/controllers/moderationactions/ModerationActions.hpp b/src/controllers/moderationactions/ModerationActions.hpp index 934e2f95e..6ecc3281d 100644 --- a/src/controllers/moderationactions/ModerationActions.hpp +++ b/src/controllers/moderationactions/ModerationActions.hpp @@ -8,6 +8,9 @@ namespace chatterino { +class Settings; +class Paths; + class ModerationActionModel; class ModerationActions final : public Singleton @@ -15,7 +18,7 @@ class ModerationActions final : public Singleton public: ModerationActions(); - virtual void initialize(Application &app) override; + virtual void initialize(Settings &settings, Paths &paths) override; UnsortedSignalVector items; diff --git a/src/controllers/taggedusers/TaggedUsersController.hpp b/src/controllers/taggedusers/TaggedUsersController.hpp index cfd98b620..263ff391c 100644 --- a/src/controllers/taggedusers/TaggedUsersController.hpp +++ b/src/controllers/taggedusers/TaggedUsersController.hpp @@ -9,7 +9,7 @@ namespace chatterino { class TaggedUsersModel; -class TaggedUsersController : public Singleton +class TaggedUsersController final : public Singleton { public: TaggedUsersController(); diff --git a/src/debug/Log.hpp b/src/debug/Log.hpp index 6889139ee..51ef0f053 100644 --- a/src/debug/Log.hpp +++ b/src/debug/Log.hpp @@ -14,4 +14,11 @@ inline void Log(const std::string &formatString, Args &&... args) << fS(formatString, std::forward(args)...).c_str(); } +template +inline void Warn(const std::string &formatString, Args &&... args) +{ + qWarning() << QTime::currentTime().toString("hh:mm:ss.zzz") + << fS(formatString, std::forward(args)...).c_str(); +} + } // namespace chatterino diff --git a/src/main.cpp b/src/main.cpp index 9be014123..4ebb846a3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,221 +1,28 @@ -#include "Application.hpp" -#include "common/NetworkManager.hpp" -#include "singletons/NativeMessaging.hpp" +#include "BrowserExtension.hpp" +#include "RunGui.hpp" #include "singletons/Paths.hpp" -#include "singletons/Updates.hpp" -#include "util/DebugCount.hpp" -#include "widgets/dialogs/LastRunCrashDialog.hpp" +#include "singletons/Settings.hpp" -#include #include -#include -#include #include -#include -#include -#include -#include +using namespace chatterino; -#ifdef Q_OS_WIN -#include -#include -#include -#endif - -#ifdef C_USE_BREAKPAD -#include -#endif - -int runGui(QApplication &a, int argc, char *argv[]); -void runNativeMessagingHost(); -void installCustomPalette(); - -// -// Main entry point of the application. -// Decides if it should run in gui mode, daemon mode, ... -// Sets up the QApplication -// -int main(int argc, char *argv[]) +int main(int argc, char **argv) { - // set up the QApplication flags - QApplication::setAttribute(Qt::AA_Use96Dpi, true); -#ifdef Q_OS_WIN32 - QApplication::setAttribute(Qt::AA_DisableHighDpiScaling, true); -#endif - // QApplication::setAttribute(Qt::AA_UseSoftwareOpenGL, true); - - // instanciate the QApplication QApplication a(argc, argv); - // FOURTF: might get arguments from the commandline passed in the future - chatterino::Paths::initInstance(); + // convert char[][] to QStringList + auto args = QStringList(); + std::transform(argv + 1, argv + argc, std::back_inserter(args), [&](auto s) { return s; }); - // read args - QStringList args; - - for (int i = 1; i < argc; i++) { - args << argv[i]; - } - - // run native messaging host for the browser extension - if (args.size() > 0 && - (args[0].startsWith("chrome-extension://") || args[0].endsWith(".json"))) { - runNativeMessagingHost(); - return 0; - } - - // run gui - return runGui(a, argc, argv); -} - -int runGui(QApplication &a, int argc, char *argv[]) -{ - QApplication::setStyle(QStyleFactory::create("Fusion")); - - installCustomPalette(); - - // Initialize NetworkManager - chatterino::NetworkManager::init(); - - // Check for upates - chatterino::Updates::getInstance().checkForUpdates(); - - // Initialize application - chatterino::Application::instantiate(argc, argv); - auto app = chatterino::getApp(); - - app->construct(); - -#ifdef C_USE_BREAKPAD - QBreakpadInstance.setDumpPath(app->paths->settingsFolderPath + "/Crashes"); -#endif - - auto &pathMan = *app->paths; - // Running file - auto runningPath = pathMan.miscDirectory + "/running_" + pathMan.applicationFilePathHash; - - if (QFile::exists(runningPath)) { -#ifndef C_DISABLE_CRASH_DIALOG - chatterino::LastRunCrashDialog dialog; - - switch (dialog.exec()) { - case QDialog::Accepted: { - }; break; - default: { - _exit(0); - } - } -#endif + // run in gui mode or browser extension host mode + if (shouldRunBrowserExtensionHost(args)) { + runBrowserExtensionHost(); } else { - QFile runningFile(runningPath); + Paths paths; + Settings settings(paths); - runningFile.open(QIODevice::WriteOnly | QIODevice::Truncate); - runningFile.flush(); - runningFile.close(); - } - - app->initialize(); - - // Start the application - // This is a blocking call - app->run(a); - - // We have finished our application, make sure we save stuff - app->save(); - - // Running file - QFile::remove(runningPath); - - // Save settings - pajlada::Settings::SettingManager::gSave(); - - // Deinitialize NetworkManager (stop thread and wait for finish, should be instant) - chatterino::NetworkManager::deinit(); - - // None of the singletons has a proper destructor - _exit(0); -} - -void runNativeMessagingHost() -{ - auto *nm = new chatterino::NativeMessaging; - -#ifdef Q_OS_WIN - _setmode(_fileno(stdin), _O_BINARY); - _setmode(_fileno(stdout), _O_BINARY); -#endif - -#if 0 - bool bigEndian = isBigEndian(); -#endif - - std::atomic ping(false); - - QTimer timer; - QObject::connect(&timer, &QTimer::timeout, [&ping] { - if (!ping.exchange(false)) { - _exit(0); - } - }); - timer.setInterval(11000); - timer.start(); - - while (true) { - char size_c[4]; - std::cin.read(size_c, 4); - - if (std::cin.eof()) { - break; - } - - uint32_t size = *reinterpret_cast(size_c); -#if 0 - // To avoid breaking strict-aliasing rules and potentially inducing undefined behaviour, the following code can be run instead - uint32_t size = 0; - if (bigEndian) { - size = size_c[3] | static_cast(size_c[2]) << 8 | - static_cast(size_c[1]) << 16 | static_cast(size_c[0]) << 24; - } else { - size = size_c[0] | static_cast(size_c[1]) << 8 | - static_cast(size_c[2]) << 16 | static_cast(size_c[3]) << 24; - } -#endif - - std::unique_ptr b(new char[size + 1]); - std::cin.read(b.get(), size); - *(b.get() + size) = '\0'; - - nm->sendToGuiProcess(QByteArray::fromRawData(b.get(), static_cast(size))); + runGui(a, paths, settings); } } - -void installCustomPalette() -{ - // borrowed from - // https://stackoverflow.com/questions/15035767/is-the-qt-5-dark-fusion-theme-available-for-windows - QPalette darkPalette = qApp->palette(); - - darkPalette.setColor(QPalette::Window, QColor(22, 22, 22)); - darkPalette.setColor(QPalette::WindowText, Qt::white); - darkPalette.setColor(QPalette::Text, Qt::white); - darkPalette.setColor(QPalette::Disabled, QPalette::WindowText, QColor(127, 127, 127)); - darkPalette.setColor(QPalette::Base, QColor("#333")); - darkPalette.setColor(QPalette::AlternateBase, QColor("#444")); - darkPalette.setColor(QPalette::ToolTipBase, Qt::white); - darkPalette.setColor(QPalette::ToolTipText, Qt::white); - darkPalette.setColor(QPalette::Disabled, QPalette::Text, QColor(127, 127, 127)); - darkPalette.setColor(QPalette::Dark, QColor(35, 35, 35)); - darkPalette.setColor(QPalette::Shadow, QColor(20, 20, 20)); - darkPalette.setColor(QPalette::Button, QColor(70, 70, 70)); - darkPalette.setColor(QPalette::ButtonText, Qt::white); - darkPalette.setColor(QPalette::Disabled, QPalette::ButtonText, QColor(127, 127, 127)); - darkPalette.setColor(QPalette::BrightText, Qt::red); - darkPalette.setColor(QPalette::Link, QColor(42, 130, 218)); - darkPalette.setColor(QPalette::Highlight, QColor(42, 130, 218)); - darkPalette.setColor(QPalette::Disabled, QPalette::Highlight, QColor(80, 80, 80)); - darkPalette.setColor(QPalette::HighlightedText, Qt::white); - darkPalette.setColor(QPalette::Disabled, QPalette::HighlightedText, QColor(127, 127, 127)); - - qApp->setPalette(darkPalette); -} diff --git a/src/messages/Emote.cpp b/src/messages/Emote.cpp new file mode 100644 index 000000000..369725426 --- /dev/null +++ b/src/messages/Emote.cpp @@ -0,0 +1,75 @@ +#include "Emote.hpp" + +#include + +namespace chatterino { + +bool operator==(const Emote &a, const Emote &b) +{ + return std::tie(a.homePage, a.name, a.tooltip, a.images) == + std::tie(b.homePage, b.name, b.tooltip, b.images); +} + +bool operator!=(const Emote &a, const Emote &b) +{ + return !(a == b); +} + +// EmotePtr Emote::create(const EmoteData2 &data) +//{ +//} + +// EmotePtr Emote::create(EmoteData2 &&data) +//{ +//} + +// Emote::Emote(EmoteData2 &&data) +// : data_(data) +//{ +//} +// +// Emote::Emote(const EmoteData2 &data) +// : data_(data) +//{ +//} +// +// const Url &Emote::getHomePage() const +//{ +// return this->data_.homePage; +//} +// +// const EmoteName &Emote::getName() const +//{ +// return this->data_.name; +//} +// +// const Tooltip &Emote::getTooltip() const +//{ +// return this->data_.tooltip; +//} +// +// const ImageSet &Emote::getImages() const +//{ +// return this->data_.images; +//} +// +// const QString &Emote::getCopyString() const +//{ +// return this->data_.name.string; +//} +// +// bool Emote::operator==(const Emote &other) const +//{ +// auto &a = this->data_; +// auto &b = other.data_; +// +// return std::tie(a.homePage, a.name, a.tooltip, a.images) == +// std::tie(b.homePage, b.name, b.tooltip, b.images); +//} +// +// bool Emote::operator!=(const Emote &other) const +//{ +// return !this->operator==(other); +//} + +} // namespace chatterino diff --git a/src/messages/Emote.hpp b/src/messages/Emote.hpp new file mode 100644 index 000000000..c5817752e --- /dev/null +++ b/src/messages/Emote.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include "messages/Image.hpp" +#include "messages/ImageSet.hpp" + +#include +#include +#include + +QStringAlias(EmoteId); +QStringAlias(EmoteName); + +namespace chatterino { + +struct Emote { + EmoteName name; + ImageSet images; + Tooltip tooltip; + Url homePage; + + // FOURTF: no solution yet, to be refactored later + const QString &getCopyString() const + { + return name.string; + } +}; + +bool operator==(const Emote &a, const Emote &b); +bool operator!=(const Emote &a, const Emote &b); + +using EmotePtr = std::shared_ptr; + +class EmoteMap : public std::unordered_map +{ +}; +using EmoteIdMap = std::unordered_map; +using WeakEmoteMap = std::unordered_map>; +using WeakEmoteIdMap = std::unordered_map>; + +// struct EmoteData2 { +// EmoteName name; +// ImageSet images; +// Tooltip tooltip; +// Url homePage; +//}; +// +// class Emote +//{ +// public: +// Emote(EmoteData2 &&data); +// Emote(const EmoteData2 &data); +// +// const Url &getHomePage() const; +// const EmoteName &getName() const; +// const Tooltip &getTooltip() const; +// const ImageSet &getImages() const; +// const QString &getCopyString() const; +// bool operator==(const Emote &other) const; +// bool operator!=(const Emote &other) const; +// +// private: +// EmoteData2 data_; +//}; + +} // namespace chatterino diff --git a/src/messages/EmoteCache.hpp b/src/messages/EmoteCache.hpp new file mode 100644 index 000000000..1bbb01fd4 --- /dev/null +++ b/src/messages/EmoteCache.hpp @@ -0,0 +1,93 @@ +#pragma once + +#include +#include +#include +#include + +#include "common/UniqueAccess.hpp" +#include "messages/Emote.hpp" + +namespace chatterino { + +template +class MapReplacement +{ +public: + MapReplacement(std::unordered_map &items) + : oldItems_(items) + { + } + + void add(const TKey &key, const Emote &data) + { + this->add(key, Emote(data)); + } + + void add(const TKey &key, Emote &&data) + { + auto it = this->oldItems_.find(key); + if (it != this->oldItems_.end() && *it->second == data) { + this->newItems_[key] = it->second; + } else { + this->newItems_[key] = std::make_shared(std::move(data)); + } + } + + void apply() + { + this->oldItems_ = std::move(this->newItems_); + } + +private: + std::unordered_map &oldItems_; + std::unordered_map newItems_; +}; + +template +class EmoteCache +{ +public: + using Iterator = typename std::unordered_map::iterator; + using ConstIterator = typename std::unordered_map::iterator; + + Iterator begin() + { + return this->items_.begin(); + } + + ConstIterator begin() const + { + return this->items_.begin(); + } + + Iterator end() + { + return this->items_.end(); + } + + ConstIterator end() const + { + return this->items_.end(); + } + + boost::optional get(const TKey &key) const + { + auto it = this->items_.find(key); + + if (it == this->items_.end()) + return boost::none; + else + return it->second; + } + + MapReplacement makeReplacment() + { + return MapReplacement(this->items_); + } + +private: + std::unordered_map items_; +}; + +} // namespace chatterino diff --git a/src/messages/EmoteMap.cpp b/src/messages/EmoteMap.cpp new file mode 100644 index 000000000..f940de852 --- /dev/null +++ b/src/messages/EmoteMap.cpp @@ -0,0 +1,44 @@ +#include "EmoteMap.hpp" + +#include "Application.hpp" +#include "singletons/Settings.hpp" + +namespace chatterino { + +// EmoteData::EmoteData(Image *image) +// : image1x(image) +//{ +//} + +//// Emotes must have a 1x image to be valid +// bool EmoteData::isValid() const +//{ +// return this->image1x != nullptr; +//} + +// Image *EmoteData::getImage(float scale) const +//{ +// int quality = getApp()->settings->preferredEmoteQuality; + +// if (quality == 0) { +// scale *= getApp()->settings->emoteScale.getValue(); +// quality = [&] { +// if (scale <= 1) return 1; +// if (scale <= 2) return 2; +// return 3; +// }(); +// } + +// Image *_image; +// if (quality == 3 && this->image3x != nullptr) { +// _image = this->image3x; +// } else if (quality >= 2 && this->image2x != nullptr) { +// _image = this->image2x; +// } else { +// _image = this->image1x; +// } + +// return _image; +//} + +} // namespace chatterino diff --git a/src/messages/EmoteMap.hpp b/src/messages/EmoteMap.hpp new file mode 100644 index 000000000..30a0dfdd1 --- /dev/null +++ b/src/messages/EmoteMap.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "boost/optional.hpp" +#include "messages/Emote.hpp" + +namespace chatterino { + +// class EmoteMap +//{ +// public: +// void add(Emote emote); +// void remove(const Emote &emote); +// void remove(const QString &name); + +// private: +//}; + +// using EmoteMap = ConcurrentMap; + +} // namespace chatterino diff --git a/src/messages/Image.cpp b/src/messages/Image.cpp index 11cd4525c..b1fcf8787 100644 --- a/src/messages/Image.cpp +++ b/src/messages/Image.cpp @@ -2,6 +2,7 @@ #include "Application.hpp" #include "common/NetworkRequest.hpp" +#include "debug/AssertInGuiThread.hpp" #include "debug/Log.hpp" #include "singletons/Emotes.hpp" #include "singletons/WindowManager.hpp" @@ -19,258 +20,308 @@ namespace chatterino { -bool Image::loadedEventQueued = false; +// IMAGE2 +std::atomic Image::loadedEventQueued{false}; -Image::Image(const QString &url, qreal scale, const QString &name, const QString &tooltip, - const QMargins &margin, bool isHat) - : url(url) - , name(name) - , tooltip(tooltip) - , margin(margin) - , ishat(isHat) - , scale(scale) +ImagePtr Image::fromUrl(const Url &url, qreal scale) { - DebugCount::increase("images"); + // herb sutter cache + static std::unordered_map> cache; + static std::mutex mutex; + + std::lock_guard lock(mutex); + + auto shared = cache[url].lock(); + + if (!shared) { + cache[url] = shared = ImagePtr(new Image(url, scale)); + } else { + Warn("same image loaded multiple times: {}", url.string); + } + + return shared; } -Image::Image(QPixmap *image, qreal scale, const QString &name, const QString &tooltip, - const QMargins &margin, bool isHat) - : currentPixmap(image) - , name(name) - , tooltip(tooltip) - , margin(margin) - , ishat(isHat) - , scale(scale) - , isLoading(true) - , isLoaded(true) +ImagePtr Image::fromOwningPixmap(std::unique_ptr pixmap, qreal scale) { - DebugCount::increase("images"); + return ImagePtr(new Image(std::move(pixmap), scale)); } -Image::~Image() +ImagePtr Image::fromNonOwningPixmap(QPixmap *pixmap, qreal scale) { - DebugCount::decrease("images"); + return ImagePtr(new Image(pixmap, scale)); +} - if (this->isAnimated()) { +ImagePtr Image::getEmpty() +{ + static auto empty = ImagePtr(new Image); + return empty; +} + +Image::Image() +{ + this->isLoaded_ = true; + this->isNull_ = true; +} + +Image::Image(const Url &url, qreal scale) +{ + this->url_ = url; + this->scale_ = scale; + + if (url.string.isEmpty()) { + this->isLoaded_ = true; + this->isNull_ = true; + } +} + +Image::Image(std::unique_ptr owning, qreal scale) +{ + this->frames_.push_back(Frame(std::move(owning))); + this->scale_ = scale; + this->isLoaded_ = true; + this->currentFramePixmap_ = this->frames_.front().getPixmap(); +} + +Image::Image(QPixmap *nonOwning, qreal scale) +{ + this->frames_.push_back(Frame(nonOwning)); + this->scale_ = scale; + this->isLoaded_ = true; + this->currentFramePixmap_ = this->frames_.front().getPixmap(); +} + +const Url &Image::getUrl() const +{ + return this->url_; +} + +NullablePtr Image::getPixmap() const +{ + assertInGuiThread(); + + if (!this->isLoaded_) { + const_cast(this)->load(); + } + + return this->currentFramePixmap_; +} + +qreal Image::getScale() const +{ + return this->scale_; +} + +bool Image::isAnimated() const +{ + return this->isAnimated_; +} + +int Image::getWidth() const +{ + if (!this->isLoaded_) return 16; + + return this->frames_.front().getPixmap()->width() * this->scale_; +} + +int Image::getHeight() const +{ + if (!this->isLoaded_) return 16; + + return this->frames_.front().getPixmap()->height() * this->scale_; +} + +bool Image::isLoaded() const +{ + return this->isLoaded_; +} + +bool Image::isValid() const +{ + return !this->isNull_; +} + +bool Image::isNull() const +{ + return this->isNull_; +} + +void Image::load() +{ + // decrease debug count + if (this->isAnimated_) { DebugCount::decrease("animated images"); } - - if (this->isLoaded) { + if (this->isLoaded_) { DebugCount::decrease("loaded images"); } -} -void Image::loadImage() -{ - NetworkRequest req(this->getUrl()); - req.setCaller(this); + this->isLoaded_ = false; + this->isLoading_ = true; + this->frames_.clear(); + + NetworkRequest req(this->getUrl().string); + req.setCaller(&this->object_); req.setUseQuickLoadCache(true); - req.onSuccess([this](auto result) -> bool { - auto bytes = result.getData(); + req.onSuccess([this, weak = weakOf(this)](auto result) -> Outcome { + auto shared = weak.lock(); + if (!shared) return Failure; + + auto &bytes = result.getData(); QByteArray copy = QByteArray::fromRawData(bytes.constData(), bytes.length()); - QBuffer buffer(©); - buffer.open(QIODevice::ReadOnly); - QImage image; - QImageReader reader(&buffer); - - bool first = true; - - // clear stuff before loading the image again - this->allFrames.clear(); - if (this->isAnimated()) { - DebugCount::decrease("animated images"); - } - if (this->isLoaded) { - DebugCount::decrease("loaded images"); - } - - if (reader.imageCount() == -1) { - // An error occured in the reader - Log("An error occured reading the image: '{}'", reader.errorString()); - Log("Image url: {}", this->url); - return false; - } - - if (reader.imageCount() == 0) { - Log("Error: No images read in the buffer"); - // No images read in the buffer. maybe a cache error? - return false; - } - - for (int index = 0; index < reader.imageCount(); ++index) { - if (reader.read(&image)) { - auto pixmap = new QPixmap(QPixmap::fromImage(image)); - - if (first) { - first = false; - this->loadedPixmap = pixmap; - } - - Image::FrameData data; - data.duration = std::max(20, reader.nextImageDelay()); - data.image = pixmap; - - this->allFrames.push_back(data); - } - } - - if (this->allFrames.size() != reader.imageCount()) { - // Log("Error: Wrong amount of images read"); - // One or more images failed to load from the buffer - // return false; - } - - if (this->allFrames.size() > 1) { - if (!this->animated) { - postToThread([this] { - getApp()->emotes->gifTimer.signal.connect([=]() { - this->gifUpdateTimout(); - }); // For some reason when Boost signal is in - // thread scope and thread deletes the signal - // doesn't work, so this is the fix. - }); - } - - this->animated = true; - - DebugCount::increase("animated images"); - } - - this->currentPixmap = this->loadedPixmap; - - this->isLoaded = true; - DebugCount::increase("loaded images"); - - if (!loadedEventQueued) { - loadedEventQueued = true; - - QTimer::singleShot(500, [] { - getApp()->windows->incGeneration(); - - auto app = getApp(); - app->windows->layoutChannelViews(); - loadedEventQueued = false; - }); - } - - return true; + return this->parse(result.getData()); }); req.execute(); } -void Image::gifUpdateTimout() +Outcome Image::parse(const QByteArray &data) { - if (this->animated) { - this->currentFrameOffset += GIF_FRAME_LENGTH; + // const cast since we are only reading from it + QBuffer buffer(const_cast(&data)); + buffer.open(QIODevice::ReadOnly); + QImageReader reader(&buffer); + + return this->setFrames(this->readFrames(reader)); +} + +std::vector Image::readFrames(QImageReader &reader) +{ + std::vector frames; + + if (reader.imageCount() <= 0) { + Log("Error while reading image {}: '{}'", this->url_.string, reader.errorString()); + return frames; + } + + QImage image; + for (int index = 0; index < reader.imageCount(); ++index) { + if (reader.read(&image)) { + auto pixmap = new QPixmap(QPixmap::fromImage(image)); + + int duration = std::max(20, reader.nextImageDelay()); + frames.push_back(Image::Frame(pixmap, duration)); + } + } + + if (frames.size() != 0) { + Log("Error while reading image {}: '{}'", this->url_.string, reader.errorString()); + } + + return frames; +} + +Outcome Image::setFrames(std::vector frames) +{ + std::lock_guard lock(this->framesMutex_); + + if (frames.size() > 0) { + this->currentFramePixmap_ = frames.front().getPixmap(); + + if (frames.size() > 1) { + if (!this->isAnimated_) { + getApp()->emotes->gifTimer.signal.connect([=]() { this->updateAnimation(); }); + } + + this->isAnimated_ = true; + DebugCount::increase("animated images"); + } + + this->isLoaded_ = true; + DebugCount::increase("loaded images"); + + return Success; + } + + this->frames_ = std::move(frames); + this->queueLoadedEvent(); + + return Failure; +} + +void Image::queueLoadedEvent() +{ + if (!loadedEventQueued) { + loadedEventQueued = true; + + QTimer::singleShot(250, [] { + getApp()->windows->incGeneration(); + getApp()->windows->layoutChannelViews(); + loadedEventQueued = false; + }); + } +} + +void Image::updateAnimation() +{ + if (this->isAnimated_) { + std::lock_guard lock(this->framesMutex_); + + this->currentFrameOffset_ += GIF_FRAME_LENGTH; while (true) { - if (this->currentFrameOffset > this->allFrames.at(this->currentFrame).duration) { - this->currentFrameOffset -= this->allFrames.at(this->currentFrame).duration; - this->currentFrame = (this->currentFrame + 1) % this->allFrames.size(); + this->currentFrameIndex_ %= this->frames_.size(); + if (this->currentFrameOffset_ > this->frames_[this->currentFrameIndex_].getDuration()) { + this->currentFrameOffset_ -= this->frames_[this->currentFrameIndex_].getDuration(); + this->currentFrameIndex_ = (this->currentFrameIndex_ + 1) % this->frames_.size(); } else { break; } } - this->currentPixmap = this->allFrames[this->currentFrame].image; + this->currentFramePixmap_ = this->frames_[this->currentFrameIndex_].getPixmap(); } } -const QPixmap *Image::getPixmap() +bool Image::operator==(const Image &other) const { - if (!this->isLoading) { - this->isLoading = true; - - this->loadImage(); - - return nullptr; + if (this->isNull() && other.isNull()) { + return true; } - if (this->isLoaded) { - return this->currentPixmap; - } else { - return nullptr; - } -} - -qreal Image::getScale() const -{ - return this->scale; -} - -const QString &Image::getUrl() const -{ - return this->url; -} - -const QString &Image::getName() const -{ - return this->name; -} - -const QString &Image::getCopyString() const -{ - if (this->copyString.isEmpty()) { - return this->name; + if (!this->url_.string.isEmpty() && this->url_ == other.url_) { + return true; } - return this->copyString; -} + assert(this->frames_.size() == 1); + assert(other.frames_.size() == 1); -const QString &Image::getTooltip() const -{ - return this->tooltip; -} - -const QMargins &Image::getMargin() const -{ - return this->margin; -} - -bool Image::isAnimated() const -{ - return this->animated; -} - -bool Image::isHat() const -{ - return this->ishat; -} - -int Image::getWidth() const -{ - if (this->currentPixmap == nullptr) { - return 16; + if (this->currentFramePixmap_ == other.currentFramePixmap_) { + return true; } - return this->currentPixmap->width(); + return false; } -int Image::getScaledWidth() const +bool Image::operator!=(const Image &other) const { - return static_cast((float)this->getWidth() * this->scale * - getApp()->settings->emoteScale.getValue()); + return !this->operator==(other); } -int Image::getHeight() const +// FRAME +Image::Frame::Frame(QPixmap *nonOwning, int duration) + : nonOwning_(nonOwning) + , duration_(duration) { - if (this->currentPixmap == nullptr) { - return 16; - } - return this->currentPixmap->height(); } -int Image::getScaledHeight() const +Image::Frame::Frame(std::unique_ptr nonOwning, int duration) + : owning_(std::move(nonOwning)) + , duration_(duration) { - return static_cast((float)this->getHeight() * this->scale * - getApp()->settings->emoteScale.getValue()); } -void Image::setCopyString(const QString &newCopyString) +int Image::Frame::getDuration() const { - this->copyString = newCopyString; + return this->duration_; +} + +QPixmap *Image::Frame::getPixmap() const +{ + if (this->nonOwning_) return this->nonOwning_; + + return this->owning_.get(); } } // namespace chatterino diff --git a/src/messages/Image.hpp b/src/messages/Image.hpp index c25c4db45..eec7ea9a9 100644 --- a/src/messages/Image.hpp +++ b/src/messages/Image.hpp @@ -1,69 +1,86 @@ #pragma once +#include "common/Common.hpp" + #include #include -#include - #include +#include +#include +#include + +#include "common/NullablePtr.hpp" namespace chatterino { -class Image : public QObject, boost::noncopyable +class Image; +using ImagePtr = std::shared_ptr; + +class Image : public std::enable_shared_from_this, boost::noncopyable { public: - explicit Image(const QString &_url, qreal _scale = 1, const QString &_name = "", - const QString &_tooltip = "", const QMargins &_margin = QMargins(), - bool isHat = false); + static ImagePtr fromUrl(const Url &url, qreal scale = 1); + static ImagePtr fromOwningPixmap(std::unique_ptr pixmap, qreal scale = 1); + static ImagePtr fromNonOwningPixmap(QPixmap *pixmap, qreal scale = 1); + static ImagePtr getEmpty(); - explicit Image(QPixmap *_currentPixmap, qreal _scale = 1, const QString &_name = "", - const QString &_tooltip = "", const QMargins &_margin = QMargins(), - bool isHat = false); - ~Image(); - - const QPixmap *getPixmap(); + const Url &getUrl() const; + NullablePtr getPixmap() const; qreal getScale() const; - const QString &getUrl() const; - const QString &getName() const; - const QString &getCopyString() const; - const QString &getTooltip() const; - const QMargins &getMargin() const; bool isAnimated() const; - bool isHat() const; int getWidth() const; - int getScaledWidth() const; int getHeight() const; - int getScaledHeight() const; + bool isLoaded() const; + bool isError() const; + bool isValid() const; + bool isNull() const; - void setCopyString(const QString &newCopyString); + bool operator==(const Image &image) const; + bool operator!=(const Image &image) const; private: - struct FrameData { - QPixmap *image; - int duration; + class Frame + { + public: + QPixmap *getPixmap() const; + int getDuration() const; + + Frame(QPixmap *nonOwning, int duration = 1); + Frame(std::unique_ptr nonOwning, int duration = 1); + + private: + QPixmap *nonOwning_; + std::unique_ptr owning_; + int duration_; }; - static bool loadedEventQueued; + Image(); + Image(const Url &url, qreal scale); + Image(std::unique_ptr owning, qreal scale); + Image(QPixmap *nonOwning, qreal scale); - QPixmap *currentPixmap = nullptr; - QPixmap *loadedPixmap = nullptr; - std::vector allFrames; - int currentFrame = 0; - int currentFrameOffset = 0; + void load(); + Outcome parse(const QByteArray &data); + std::vector readFrames(QImageReader &reader); + Outcome setFrames(std::vector frames); + void updateAnimation(); + void queueLoadedEvent(); - QString url; - QString name; - QString copyString; - QString tooltip; - bool animated = false; - QMargins margin; - bool ishat; - qreal scale; + Url url_; + bool isLoaded_{false}; + bool isLoading_{false}; + bool isAnimated_{false}; + bool isError_{false}; + bool isNull_ = false; + qreal scale_ = 1; + QObject object_; - bool isLoading = false; - std::atomic isLoaded{false}; + std::vector frames_; + std::mutex framesMutex_; + NullablePtr currentFramePixmap_; + int currentFrameIndex_ = 0; + int currentFrameOffset_ = 0; - void loadImage(); - void gifUpdateTimout(); + static std::atomic loadedEventQueued; }; - } // namespace chatterino diff --git a/src/messages/ImageSet.cpp b/src/messages/ImageSet.cpp new file mode 100644 index 000000000..0a3e60802 --- /dev/null +++ b/src/messages/ImageSet.cpp @@ -0,0 +1,95 @@ +#include "ImageSet.hpp" + +#include "Application.hpp" +#include "singletons/Resources.hpp" +#include "singletons/Settings.hpp" + +namespace chatterino { + +ImageSet::ImageSet() + : imageX1_(Image::getEmpty()) + , imageX2_(Image::getEmpty()) + , imageX3_(Image::getEmpty()) +{ +} + +ImageSet::ImageSet(const ImagePtr &image1, const ImagePtr &image2, const ImagePtr &image3) + : imageX1_(image1) + , imageX2_(image2) + , imageX3_(image3) +{ +} + +ImageSet::ImageSet(const Url &image1, const Url &image2, const Url &image3) + : imageX1_(Image::fromUrl(image1, 1)) + , imageX2_(Image::fromUrl(image2, 0.5)) + , imageX3_(Image::fromUrl(image3, 0.25)) +{ +} + +void ImageSet::setImage1(const ImagePtr &image) +{ + this->imageX1_ = image; +} + +void ImageSet::setImage2(const ImagePtr &image) +{ + this->imageX2_ = image; +} + +void ImageSet::setImage3(const ImagePtr &image) +{ + this->imageX3_ = image; +} + +const ImagePtr &ImageSet::getImage1() const +{ + return this->imageX1_; +} + +const ImagePtr &ImageSet::getImage2() const +{ + return this->imageX2_; +} + +const ImagePtr &ImageSet::getImage3() const +{ + return this->imageX3_; +} + +const ImagePtr &ImageSet::getImage(float scale) const +{ + int quality = getSettings()->preferredEmoteQuality; + + if (!quality) { + if (scale > 3.999) + quality = 3; + else if (scale > 1.999) + quality = 2; + else + scale = 1; + } + + if (this->imageX3_->isValid() && quality == 3) { + return this->imageX3_; + } + + if (this->imageX2_->isValid() && quality == 2) { + return this->imageX3_; + } + + return this->imageX1_; +} + +bool ImageSet::operator==(const ImageSet &other) const +{ + return std::tie(this->imageX1_, this->imageX2_, this->imageX3_) == + std::tie(other.imageX1_, other.imageX2_, other.imageX3_); +} + +bool ImageSet::operator!=(const ImageSet &other) const +{ + return !this->operator==(other); +} + +} // namespace chatterino diff --git a/src/messages/ImageSet.hpp b/src/messages/ImageSet.hpp new file mode 100644 index 000000000..8f2efab2d --- /dev/null +++ b/src/messages/ImageSet.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include "messages/Image.hpp" + +namespace chatterino { + +class ImageSet +{ +public: + ImageSet(); + ImageSet(const ImagePtr &image1, const ImagePtr &image2 = Image::getEmpty(), + const ImagePtr &image3 = Image::getEmpty()); + ImageSet(const Url &image1, const Url &image2 = {}, const Url &image3 = {}); + + void setImage1(const ImagePtr &image); + void setImage2(const ImagePtr &image); + void setImage3(const ImagePtr &image); + const ImagePtr &getImage1() const; + const ImagePtr &getImage2() const; + const ImagePtr &getImage3() const; + + const ImagePtr &getImage(float scale) const; + + ImagePtr getImage(float scale); + + bool operator==(const ImageSet &other) const; + bool operator!=(const ImageSet &other) const; + +private: + ImagePtr imageX1_; + ImagePtr imageX2_; + ImagePtr imageX3_; +}; + +} // namespace chatterino diff --git a/src/messages/MessageElement.cpp b/src/messages/MessageElement.cpp index 5081f5e47..ed29957b9 100644 --- a/src/messages/MessageElement.cpp +++ b/src/messages/MessageElement.cpp @@ -60,18 +60,18 @@ MessageElement::Flags MessageElement::getFlags() const } // IMAGE -ImageElement::ImageElement(Image *image, MessageElement::Flags flags) +ImageElement::ImageElement(ImagePtr image, MessageElement::Flags flags) : MessageElement(flags) , image_(image) { - this->setTooltip(image->getTooltip()); + // this->setTooltip(image->getTooltip()); } void ImageElement::addToContainer(MessageLayoutContainer &container, MessageElement::Flags flags) { if (flags & this->getFlags()) { - QSize size(this->image_->getScaledWidth() * container.getScale(), - this->image_->getScaledHeight() * container.getScale()); + auto size = QSize(this->image_->getWidth() * container.getScale(), + this->image_->getHeight() * container.getScale()); container.addElement( (new ImageLayoutElement(*this, this->image_, size))->setLink(this->getLink())); @@ -79,29 +79,32 @@ void ImageElement::addToContainer(MessageLayoutContainer &container, MessageElem } // EMOTE -EmoteElement::EmoteElement(const EmoteData &data, MessageElement::Flags flags) +EmoteElement::EmoteElement(const EmotePtr &emote, MessageElement::Flags flags) : MessageElement(flags) - , data(data) + , emote_(emote) { - if (data.isValid()) { - this->setTooltip(data.image1x->getTooltip()); - this->textElement_.reset( - new TextElement(data.image1x->getCopyString(), MessageElement::Misc)); + auto image = emote->images.getImage1(); + if (image->isValid()) { + this->textElement_.reset(new TextElement(emote->getCopyString(), MessageElement::Misc)); } + + this->setTooltip(emote->tooltip.string); +} + +EmotePtr EmoteElement::getEmote() const +{ + return this->emote_; } void EmoteElement::addToContainer(MessageLayoutContainer &container, MessageElement::Flags flags) { if (flags & this->getFlags()) { if (flags & MessageElement::EmoteImages) { - if (!this->data.isValid()) { - return; - } + auto image = this->emote_->images.getImage(container.getScale()); + if (!image->isValid()) return; - Image *image = this->data.getImage(container.getScale()); - - QSize size(int(container.getScale() * image->getScaledWidth()), - int(container.getScale() * image->getScaledHeight())); + QSize size(int(container.getScale() * image->getWidth()), + int(container.getScale() * image->getHeight())); container.addElement( (new ImageLayoutElement(*this, image, size))->setLink(this->getLink())); @@ -173,7 +176,6 @@ void TextElement::addToContainer(MessageLayoutContainer &container, MessageEleme int textLength = text.length(); int wordStart = 0; int width = metrics.width(text[0]); - int lastWidth = 0; for (int i = 1; i < textLength; i++) { int charWidth = metrics.width(text[i]); @@ -184,7 +186,6 @@ void TextElement::addToContainer(MessageLayoutContainer &container, MessageEleme container.breakLine(); wordStart = i; - lastWidth = width; width = 0; if (textLength > i + 2) { width += metrics.width(text[i]); @@ -196,8 +197,6 @@ void TextElement::addToContainer(MessageLayoutContainer &container, MessageEleme width += charWidth; } - UNUSED(lastWidth); // XXX: What should this be used for (if anything)? KKona - container.addElement( getTextLayoutElement(text.mid(wordStart), width, this->hasTrailingSpace())); container.breakLine(); @@ -249,14 +248,15 @@ void TwitchModerationElement::addToContainer(MessageLayoutContainer &container, if (flags & MessageElement::ModeratorTools) { QSize size(int(container.getScale() * 16), int(container.getScale() * 16)); - for (const ModerationAction &m : getApp()->moderationActions->items.getVector()) { - if (m.isImage()) { - container.addElement((new ImageLayoutElement(*this, m.getImage(), size)) - ->setLink(Link(Link::UserAction, m.getAction()))); + for (const auto &action : getApp()->moderationActions->items.getVector()) { + if (auto image = action.getImage()) { + container.addElement((new ImageLayoutElement(*this, image.get(), size)) + ->setLink(Link(Link::UserAction, action.getAction()))); } else { - container.addElement((new TextIconLayoutElement(*this, m.getLine1(), m.getLine2(), - container.getScale(), size)) - ->setLink(Link(Link::UserAction, m.getAction()))); + container.addElement( + (new TextIconLayoutElement(*this, action.getLine1(), action.getLine2(), + container.getScale(), size)) + ->setLink(Link(Link::UserAction, action.getAction()))); } } } diff --git a/src/messages/MessageElement.hpp b/src/messages/MessageElement.hpp index 764101641..51cb5a770 100644 --- a/src/messages/MessageElement.hpp +++ b/src/messages/MessageElement.hpp @@ -1,6 +1,7 @@ #pragma once #include "common/Emotemap.hpp" +#include "messages/Emote.hpp" #include "messages/Image.hpp" #include "messages/Link.hpp" #include "messages/MessageColor.hpp" @@ -16,7 +17,6 @@ namespace chatterino { class Channel; -struct EmoteData; struct MessageLayoutContainer; class MessageElement : boost::noncopyable @@ -133,12 +133,12 @@ private: class ImageElement : public MessageElement { public: - ImageElement(Image *image, MessageElement::Flags flags); + ImageElement(ImagePtr image, MessageElement::Flags flags); void addToContainer(MessageLayoutContainer &container, MessageElement::Flags flags) override; private: - Image *image_; + ImagePtr image_; }; // contains a text, it will split it into words @@ -169,15 +169,14 @@ private: class EmoteElement : public MessageElement { public: - EmoteElement(const EmoteData &data, MessageElement::Flags flags_); - ~EmoteElement() override = default; + EmoteElement(const EmotePtr &data, MessageElement::Flags flags_); void addToContainer(MessageLayoutContainer &container, MessageElement::Flags flags_) override; - - const EmoteData data; + EmotePtr getEmote() const; private: std::unique_ptr textElement_; + EmotePtr emote_; }; // contains a text, formated depending on the preferences diff --git a/src/messages/layouts/MessageLayoutElement.cpp b/src/messages/layouts/MessageLayoutElement.cpp index c71858eec..6d253e7a5 100644 --- a/src/messages/layouts/MessageLayoutElement.cpp +++ b/src/messages/layouts/MessageLayoutElement.cpp @@ -63,16 +63,17 @@ const Link &MessageLayoutElement::getLink() const // IMAGE // -ImageLayoutElement::ImageLayoutElement(MessageElement &_creator, Image *_image, const QSize &_size) - : MessageLayoutElement(_creator, _size) - , image(_image) +ImageLayoutElement::ImageLayoutElement(MessageElement &creator, ImagePtr image, const QSize &size) + : MessageLayoutElement(creator, size) + , image_(image) { - this->trailingSpace = _creator.hasTrailingSpace(); + this->trailingSpace = creator.hasTrailingSpace(); } void ImageLayoutElement::addCopyTextToString(QString &str, int from, int to) const { - str += this->image->getCopyString(); + // str += this->image_->getCopyString(); + str += "not implemented"; if (this->hasTrailingSpace()) { str += " "; @@ -86,13 +87,12 @@ int ImageLayoutElement::getSelectionIndexCount() void ImageLayoutElement::paint(QPainter &painter) { - if (this->image == nullptr) { + if (this->image_ == nullptr) { return; } - const QPixmap *pixmap = this->image->getPixmap(); - - if (pixmap != nullptr && !this->image->isAnimated()) { + auto pixmap = this->image_->getPixmap(); + if (pixmap && !this->image_->isAnimated()) { // fourtf: make it use qreal values painter.drawPixmap(QRectF(this->getRect()), *pixmap, QRectF()); } @@ -100,19 +100,15 @@ void ImageLayoutElement::paint(QPainter &painter) void ImageLayoutElement::paintAnimated(QPainter &painter, int yOffset) { - if (this->image == nullptr) { + if (this->image_ == nullptr) { return; } - if (this->image->isAnimated()) { - // qDebug() << this->image->getUrl(); - auto pixmap = this->image->getPixmap(); - - if (pixmap != nullptr) { - // fourtf: make it use qreal values - QRect _rect = this->getRect(); - _rect.moveTop(_rect.y() + yOffset); - painter.drawPixmap(QRectF(_rect), *pixmap, QRectF()); + if (this->image_->isAnimated()) { + if (auto pixmap = this->image_->getPixmap()) { + auto rect = this->getRect(); + rect.moveTop(rect.y() + yOffset); + painter.drawPixmap(QRectF(rect), *pixmap, QRectF()); } } } diff --git a/src/messages/layouts/MessageLayoutElement.hpp b/src/messages/layouts/MessageLayoutElement.hpp index 3d14b0f32..d8221726f 100644 --- a/src/messages/layouts/MessageLayoutElement.hpp +++ b/src/messages/layouts/MessageLayoutElement.hpp @@ -7,6 +7,7 @@ #include #include +#include "messages/Image.hpp" #include "messages/Link.hpp" #include "messages/MessageColor.hpp" #include "singletons/Fonts.hpp" @@ -15,7 +16,6 @@ class QPainter; namespace chatterino { class MessageElement; -class Image; class MessageLayoutElement : boost::noncopyable { @@ -52,7 +52,7 @@ private: class ImageLayoutElement : public MessageLayoutElement { public: - ImageLayoutElement(MessageElement &creator_, Image *image, const QSize &size); + ImageLayoutElement(MessageElement &creator, ImagePtr image, const QSize &size); protected: void addCopyTextToString(QString &str, int from = 0, int to = INT_MAX) const override; @@ -63,7 +63,7 @@ protected: int getXFromIndex(int index) override; private: - Image *image; + ImagePtr image_; }; // TEXT diff --git a/src/providers/bttv/BttvEmotes.cpp b/src/providers/bttv/BttvEmotes.cpp index 391be6e6f..570879210 100644 --- a/src/providers/bttv/BttvEmotes.cpp +++ b/src/providers/bttv/BttvEmotes.cpp @@ -3,122 +3,107 @@ #include "common/NetworkRequest.hpp" #include "debug/Log.hpp" #include "messages/Image.hpp" +#include "messages/ImageSet.hpp" +#include "providers/twitch/TwitchChannel.hpp" + +#include +#include namespace chatterino { namespace { -QString getEmoteLink(QString urlTemplate, const QString &id, const QString &emoteScale) +Url getEmoteLink(QString urlTemplate, const EmoteId &id, const QString &emoteScale) { urlTemplate.detach(); - return urlTemplate.replace("{{id}}", id).replace("{{image}}", emoteScale); + return {urlTemplate.replace("{{id}}", id.string).replace("{{image}}", emoteScale)}; } } // namespace -void BTTVEmotes::loadGlobalEmotes() +AccessGuard BttvEmotes::accessGlobalEmotes() const { - QString url("https://api.betterttv.net/2/emotes"); + return this->globalEmotes_.accessConst(); +} + +boost::optional BttvEmotes::getGlobalEmote(const EmoteName &name) +{ + auto emotes = this->globalEmotes_.access(); + auto it = emotes->find(name); + + if (it == emotes->end()) return boost::none; + return it->second; +} + +// FOURTF: never returns anything +// boost::optional BttvEmotes::getEmote(const EmoteId &id) +//{ +// auto cache = this->channelEmoteCache_.access(); +// auto it = cache->find(id); +// +// if (it != cache->end()) { +// auto shared = it->second.lock(); +// if (shared) { +// return shared; +// } +// } +// +// return boost::none; +//} + +void BttvEmotes::loadGlobalEmotes() +{ + auto request = NetworkRequest(QString(globalEmoteApiUrl)); - NetworkRequest request(url); request.setCaller(QThread::currentThread()); request.setTimeout(30000); - request.onSuccess([this](auto result) { - auto root = result.parseJson(); - auto emotes = root.value("emotes").toArray(); + request.onSuccess([this](auto result) -> Outcome { + // if (auto shared = weak.lock()) { + auto currentEmotes = this->globalEmotes_.access(); - QString urlTemplate = "https:" + root.value("urlTemplate").toString(); + auto pair = this->parseGlobalEmotes(result.parseJson(), *currentEmotes); - std::vector codes; - for (const QJsonValue &emote : emotes) { - QString id = emote.toObject().value("id").toString(); - QString code = emote.toObject().value("code").toString(); - - EmoteData emoteData; - emoteData.image1x = new Image(getEmoteLink(urlTemplate, id, "1x"), 1, code, - code + "
Global BTTV Emote"); - emoteData.image2x = new Image(getEmoteLink(urlTemplate, id, "2x"), 0.5, code, - code + "
Global BTTV Emote"); - emoteData.image3x = new Image(getEmoteLink(urlTemplate, id, "3x"), 0.25, code, - code + "
Global BTTV Emote"); - emoteData.pageLink = "https://manage.betterttv.net/emotes/" + id; - - this->globalEmotes.insert(code, emoteData); - codes.push_back(code); + if (pair.first) { + *currentEmotes = std::move(pair.second); } - this->globalEmoteCodes = codes; - - return true; + return pair.first; + // } + return Failure; }); request.execute(); } -void BTTVEmotes::loadChannelEmotes(const QString &channelName, std::weak_ptr _map) +std::pair BttvEmotes::parseGlobalEmotes(const QJsonObject &jsonRoot, + const EmoteMap ¤tEmotes) { - printf("[BTTVEmotes] Reload BTTV Channel Emotes for channel %s\n", qPrintable(channelName)); + auto emotes = EmoteMap(); + auto jsonEmotes = jsonRoot.value("emotes").toArray(); + auto urlTemplate = QString("https:" + jsonRoot.value("urlTemplate").toString()); - QString url("https://api.betterttv.net/2/channels/" + channelName); + for (const QJsonValue &jsonEmote : jsonEmotes) { + auto id = EmoteId{jsonEmote.toObject().value("id").toString()}; + auto name = EmoteName{jsonEmote.toObject().value("code").toString()}; - Log("Request bttv channel emotes for {}", channelName); + auto emote = Emote({name, + ImageSet{Image::fromUrl(getEmoteLink(urlTemplate, id, "1x"), 1), + Image::fromUrl(getEmoteLink(urlTemplate, id, "2x"), 0.5), + Image::fromUrl(getEmoteLink(urlTemplate, id, "3x"), 0.25)}, + Tooltip{name.string + "
Global Bttv Emote"}, + Url{"https://manage.betterttv.net/emotes/" + id.string}}); - NetworkRequest request(url); - request.setCaller(QThread::currentThread()); - request.setTimeout(3000); - request.onSuccess([this, channelName, _map](auto result) { - auto rootNode = result.parseJson(); - auto map = _map.lock(); - - if (_map.expired()) { - return false; + auto it = currentEmotes.find(name); + if (it != currentEmotes.end() && *it->second == emote) { + // reuse old shared_ptr if nothing changed + emotes[name] = it->second; + } else { + emotes[name] = std::make_shared(std::move(emote)); } + } - map->clear(); - - auto emotesNode = rootNode.value("emotes").toArray(); - - QString linkTemplate = "https:" + rootNode.value("urlTemplate").toString(); - - std::vector codes; - for (const QJsonValue &emoteNode : emotesNode) { - QJsonObject emoteObject = emoteNode.toObject(); - - QString id = emoteObject.value("id").toString(); - QString code = emoteObject.value("code").toString(); - // emoteObject.value("imageType").toString(); - - auto emote = this->channelEmoteCache_.getOrAdd(id, [&] { - EmoteData emoteData; - QString link = linkTemplate; - link.detach(); - emoteData.image1x = new Image(link.replace("{{id}}", id).replace("{{image}}", "1x"), - 1, code, code + "
Channel BTTV Emote"); - link = linkTemplate; - link.detach(); - emoteData.image2x = new Image(link.replace("{{id}}", id).replace("{{image}}", "2x"), - 0.5, code, code + "
Channel BTTV Emote"); - link = linkTemplate; - link.detach(); - emoteData.image3x = new Image(link.replace("{{id}}", id).replace("{{image}}", "3x"), - 0.25, code, code + "
Channel BTTV Emote"); - emoteData.pageLink = "https://manage.betterttv.net/emotes/" + id; - - return emoteData; - }); - - this->channelEmotes.insert(code, emote); - map->insert(code, emote); - codes.push_back(code); - } - - this->channelEmoteCodes[channelName] = codes; - - return true; - }); - - request.execute(); + return {Success, std::move(emotes)}; } } // namespace chatterino diff --git a/src/providers/bttv/BttvEmotes.hpp b/src/providers/bttv/BttvEmotes.hpp index 134e5a157..f8273afb8 100644 --- a/src/providers/bttv/BttvEmotes.hpp +++ b/src/providers/bttv/BttvEmotes.hpp @@ -1,27 +1,32 @@ #pragma once -#include "common/Emotemap.hpp" -#include "common/SimpleSignalVector.hpp" -#include "util/ConcurrentMap.hpp" +#include -#include +#include "common/UniqueAccess.hpp" +#include "messages/Emote.hpp" +#include "messages/EmoteCache.hpp" namespace chatterino { -class BTTVEmotes +class BttvEmotes final : std::enable_shared_from_this { -public: - EmoteMap globalEmotes; - SimpleSignalVector globalEmoteCodes; + static constexpr const char *globalEmoteApiUrl = "https://api.betterttv.net/2/emotes"; - EmoteMap channelEmotes; - std::map> channelEmoteCodes; +public: + // BttvEmotes(); + + AccessGuard accessGlobalEmotes() const; + boost::optional getGlobalEmote(const EmoteName &name); + boost::optional getEmote(const EmoteId &id); void loadGlobalEmotes(); - void loadChannelEmotes(const QString &channelName, std::weak_ptr channelEmoteMap); private: - EmoteMap channelEmoteCache_; + std::pair parseGlobalEmotes(const QJsonObject &jsonRoot, + const EmoteMap ¤tEmotes); + + UniqueAccess globalEmotes_; + // UniqueAccess channelEmoteCache_; }; } // namespace chatterino diff --git a/src/providers/bttv/LoadBttvChannelEmote.cpp b/src/providers/bttv/LoadBttvChannelEmote.cpp new file mode 100644 index 000000000..9829c6e07 --- /dev/null +++ b/src/providers/bttv/LoadBttvChannelEmote.cpp @@ -0,0 +1,76 @@ +#include "LoadBttvChannelEmote.hpp" + +#include +#include +#include +#include +#include "common/Common.hpp" +#include "common/NetworkRequest.hpp" +#include "common/UniqueAccess.hpp" +#include "messages/Emote.hpp" + +namespace chatterino { + +static Url getEmoteLink(QString urlTemplate, const EmoteId &id, const QString &emoteScale); +static std::pair bttvParseChannelEmotes(const QJsonObject &jsonRoot); + +void loadBttvChannelEmotes(const QString &channelName, std::function callback) +{ + auto request = NetworkRequest(QString(bttvChannelEmoteApiUrl) + channelName); + + request.setCaller(QThread::currentThread()); + request.setTimeout(3000); + request.onSuccess([callback = std::move(callback)](auto result) -> Outcome { + auto pair = bttvParseChannelEmotes(result.parseJson()); + + if (pair.first == Success) callback(std::move(pair.second)); + + return pair.first; + }); + + request.execute(); +} + +static std::pair bttvParseChannelEmotes(const QJsonObject &jsonRoot) +{ + static UniqueAccess>> cache_; + + auto cache = cache_.access(); + auto emotes = EmoteMap(); + auto jsonEmotes = jsonRoot.value("emotes").toArray(); + auto urlTemplate = QString("https:" + jsonRoot.value("urlTemplate").toString()); + + for (auto jsonEmote_ : jsonEmotes) { + auto jsonEmote = jsonEmote_.toObject(); + + auto id = EmoteId{jsonEmote.value("id").toString()}; + auto name = EmoteName{jsonEmote.value("code").toString()}; + // emoteObject.value("imageType").toString(); + + auto emote = Emote({name, + ImageSet{Image::fromUrl(getEmoteLink(urlTemplate, id, "1x"), 1), + Image::fromUrl(getEmoteLink(urlTemplate, id, "2x"), 0.5), + Image::fromUrl(getEmoteLink(urlTemplate, id, "3x"), 0.25)}, + Tooltip{name.string + "
Channel Bttv Emote"}, + Url{"https://manage.betterttv.net/emotes/" + id.string}}); + + auto shared = (*cache)[id].lock(); + if (shared && *shared == emote) { + // reuse old shared_ptr if nothing changed + emotes[name] = shared; + } else { + (*cache)[id] = emotes[name] = std::make_shared(std::move(emote)); + } + } + + return {Success, std::move(emotes)}; +} + +static Url getEmoteLink(QString urlTemplate, const EmoteId &id, const QString &emoteScale) +{ + urlTemplate.detach(); + + return {urlTemplate.replace("{{id}}", id.string).replace("{{image}}", emoteScale)}; +} + +} // namespace chatterino diff --git a/src/providers/bttv/LoadBttvChannelEmote.hpp b/src/providers/bttv/LoadBttvChannelEmote.hpp new file mode 100644 index 000000000..ef9ca160e --- /dev/null +++ b/src/providers/bttv/LoadBttvChannelEmote.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +class QString; + +namespace chatterino { + +class EmoteMap; +constexpr const char *bttvChannelEmoteApiUrl = "https://api.betterttv.net/2/channels/"; + +void loadBttvChannelEmotes(const QString &channelName, std::function callback); + +} // namespace chatterino diff --git a/src/providers/chatterino/ChatterinoBadges.cpp b/src/providers/chatterino/ChatterinoBadges.cpp new file mode 100644 index 000000000..01dc82aa4 --- /dev/null +++ b/src/providers/chatterino/ChatterinoBadges.cpp @@ -0,0 +1,50 @@ +#include "ChatterinoBadges.hpp" + +#include +#include +#include +#include +#include "common/NetworkRequest.hpp" + +namespace chatterino { + +ChatterinoBadges::ChatterinoBadges() +{ +} + +boost::optional ChatterinoBadges::getBadge(const UserName &username) +{ + return this->badges.access()->get(username); +} + +void ChatterinoBadges::loadChatterinoBadges() +{ + static QString url("https://fourtf.com/chatterino/badges.json"); + + NetworkRequest req(url); + req.setCaller(QThread::currentThread()); + + req.onSuccess([this](auto result) { + auto jsonRoot = result.parseJson(); + auto badges = this->badges.access(); + auto replacement = badges->makeReplacment(); + + for (auto jsonBadge_ : jsonRoot.value("badges").toArray()) { + auto jsonBadge = jsonBadge_.toObject(); + + auto emote = Emote{EmoteName{}, ImageSet{Url{jsonBadge.value("image").toString()}}, + Tooltip{jsonBadge.value("tooltip").toString()}, Url{}}; + + for (auto jsonUser : jsonBadge.value("users").toArray()) { + replacement.add(UserName{jsonUser.toString()}, std::move(emote)); + } + } + + replacement.apply(); + return Success; + }); + + req.execute(); +} + +} // namespace chatterino diff --git a/src/providers/chatterino/ChatterinoBadges.hpp b/src/providers/chatterino/ChatterinoBadges.hpp new file mode 100644 index 000000000..3162c75b1 --- /dev/null +++ b/src/providers/chatterino/ChatterinoBadges.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include "common/Common.hpp" +#include "common/UniqueAccess.hpp" +#include "messages/Emote.hpp" +#include "messages/EmoteCache.hpp" + +namespace chatterino { + +class ChatterinoBadges +{ +public: + ChatterinoBadges(); + + boost::optional getBadge(const UserName &username); + +private: + void loadChatterinoBadges(); + + UniqueAccess> badges; +}; + +} // namespace chatterino diff --git a/src/providers/emoji/Emojis.cpp b/src/providers/emoji/Emojis.cpp index 10a0f28e6..064e693f1 100644 --- a/src/providers/emoji/Emojis.cpp +++ b/src/providers/emoji/Emojis.cpp @@ -4,7 +4,12 @@ #include "debug/Log.hpp" #include "singletons/Settings.hpp" +#include +#include +#include #include +#include +#include namespace chatterino { @@ -146,7 +151,7 @@ void Emojis::loadEmojis() emojiData->shortCodes[0] + "_" + toneNameIt->second); this->emojiShortCodeToEmoji_.insert(variationEmojiData->shortCodes[0], - variationEmojiData); + variationEmojiData); this->shortCodes.push_back(variationEmojiData->shortCodes[0]); this->emojiFirstByte_[variationEmojiData->value.at(0)].append(variationEmojiData); @@ -260,14 +265,16 @@ void Emojis::loadEmojiSet() urlPrefix = it->second; } QString url = urlPrefix + code + ".png"; - emoji->emoteData.image1x = - new Image(url, 0.35, emoji->value, ":" + emoji->shortCodes[0] + ":
Emoji"); + emoji->emote = std::make_shared( + Emote{EmoteName{emoji->value}, ImageSet{Image::fromUrl({url}, 0.35)}, + Tooltip{":" + emoji->shortCodes[0] + ":
Emoji"}, Url{}}); }); }); } -void Emojis::parse(std::vector> &parsedWords, const QString &text) +std::vector> Emojis::parse(const QString &text) { + auto result = std::vector>(); int lastParsedEmojiEndIndex = 0; for (auto i = 0; i < text.length(); ++i) { @@ -327,12 +334,11 @@ void Emojis::parse(std::vector> &parsedWords, con if (charactersFromLastParsedEmoji > 0) { // Add characters inbetween emojis - parsedWords.emplace_back( - EmoteData(), text.mid(lastParsedEmojiEndIndex, charactersFromLastParsedEmoji)); + result.emplace_back(text.mid(lastParsedEmojiEndIndex, charactersFromLastParsedEmoji)); } // Push the emoji as a word to parsedWords - parsedWords.push_back(std::tuple(matchedEmoji->emoteData, QString())); + result.emplace_back(matchedEmoji->emote); lastParsedEmojiEndIndex = currentParsedEmojiEndIndex; @@ -341,8 +347,10 @@ void Emojis::parse(std::vector> &parsedWords, con if (lastParsedEmojiEndIndex < text.length()) { // Add remaining characters - parsedWords.emplace_back(EmoteData(), text.mid(lastParsedEmojiEndIndex)); + result.emplace_back(text.mid(lastParsedEmojiEndIndex)); } + + return result; } QString Emojis::replaceShortCodes(const QString &text) diff --git a/src/providers/emoji/Emojis.hpp b/src/providers/emoji/Emojis.hpp index dc56c4e5d..59d184d01 100644 --- a/src/providers/emoji/Emojis.hpp +++ b/src/providers/emoji/Emojis.hpp @@ -2,12 +2,15 @@ #include "common/Emotemap.hpp" #include "common/SimpleSignalVector.hpp" +#include "messages/Emote.hpp" #include "util/ConcurrentMap.hpp" #include #include - +#include #include +#include +#include namespace chatterino { @@ -26,7 +29,7 @@ struct EmojiData { std::vector variations; - EmoteData emoteData; + EmotePtr emote; }; using EmojiMap = ConcurrentMap>; @@ -36,7 +39,7 @@ class Emojis public: void initialize(); void load(); - void parse(std::vector> &parsedWords, const QString &text); + std::vector> parse(const QString &text); EmojiMap emojis; std::vector shortCodes; diff --git a/src/providers/ffz/FfzEmotes.cpp b/src/providers/ffz/FfzEmotes.cpp index 9fe7ac5ce..3a9763b22 100644 --- a/src/providers/ffz/FfzEmotes.cpp +++ b/src/providers/ffz/FfzEmotes.cpp @@ -1,142 +1,168 @@ #include "providers/ffz/FfzEmotes.hpp" +#include + #include "common/NetworkRequest.hpp" #include "debug/Log.hpp" #include "messages/Image.hpp" namespace chatterino { - namespace { - -QString getEmoteLink(const QJsonObject &urls, const QString &emoteScale) +Url getEmoteLink(const QJsonObject &urls, const QString &emoteScale) { auto emote = urls.value(emoteScale); if (emote.isUndefined()) { - return ""; + return {""}; } assert(emote.isString()); - return "https:" + emote.toString(); + return {"https:" + emote.toString()}; } -void fillInEmoteData(const QJsonObject &urls, const QString &code, const QString &tooltip, - EmoteData &emoteData) +void fillInEmoteData(const QJsonObject &urls, const EmoteName &name, const QString &tooltip, + Emote &emoteData) { - QString url1x = getEmoteLink(urls, "1"); - QString url2x = getEmoteLink(urls, "2"); - QString url3x = getEmoteLink(urls, "4"); + auto url1x = getEmoteLink(urls, "1"); + auto url2x = getEmoteLink(urls, "2"); + auto url3x = getEmoteLink(urls, "4"); - assert(!url1x.isEmpty()); - - emoteData.image1x = new Image(url1x, 1, code, tooltip); - - if (!url2x.isEmpty()) { - emoteData.image2x = new Image(url2x, 0.5, code, tooltip); - } - - if (!url3x.isEmpty()) { - emoteData.image3x = new Image(url3x, 0.25, code, tooltip); - } + //, code, tooltip + emoteData.name = name; + emoteData.images = + ImageSet{Image::fromUrl(url1x, 1), Image::fromUrl(url2x, 0.5), Image::fromUrl(url3x, 0.25)}; + emoteData.tooltip = {tooltip}; } - } // namespace -void FFZEmotes::loadGlobalEmotes() +AccessGuard> FfzEmotes::accessGlobalEmotes() const +{ + return this->globalEmotes_.accessConst(); +} + +boost::optional FfzEmotes::getEmote(const EmoteId &id) +{ + auto cache = this->channelEmoteCache_.access(); + auto it = cache->find(id); + + if (it != cache->end()) { + auto shared = it->second.lock(); + if (shared) { + return shared; + } + } + + return boost::none; +} + +boost::optional FfzEmotes::getGlobalEmote(const EmoteName &name) +{ + return this->globalEmotes_.access()->get(name); +} + +void FfzEmotes::loadGlobalEmotes() { QString url("https://api.frankerfacez.com/v1/set/global"); NetworkRequest request(url); request.setCaller(QThread::currentThread()); request.setTimeout(30000); - request.onSuccess([this](auto result) { - auto root = result.parseJson(); - auto sets = root.value("sets").toObject(); - - std::vector codes; - for (const QJsonValue &set : sets) { - auto emoticons = set.toObject().value("emoticons").toArray(); - - for (const QJsonValue &emote : emoticons) { - QJsonObject object = emote.toObject(); - - QString code = object.value("name").toString(); - int id = object.value("id").toInt(); - QJsonObject urls = object.value("urls").toObject(); - - EmoteData emoteData; - fillInEmoteData(urls, code, code + "
Global FFZ Emote", emoteData); - emoteData.pageLink = - QString("https://www.frankerfacez.com/emoticon/%1-%2").arg(id).arg(code); - - this->globalEmotes.insert(code, emoteData); - codes.push_back(code); - } - - this->globalEmoteCodes = codes; - } - - return true; - }); + request.onSuccess( + [this](auto result) -> Outcome { return this->parseGlobalEmotes(result.parseJson()); }); request.execute(); } -void FFZEmotes::loadChannelEmotes(const QString &channelName, std::weak_ptr _map) +Outcome FfzEmotes::parseGlobalEmotes(const QJsonObject &jsonRoot) { - printf("[FFZEmotes] Reload FFZ Channel Emotes for channel %s\n", qPrintable(channelName)); + auto jsonSets = jsonRoot.value("sets").toObject(); + auto emotes = this->globalEmotes_.access(); + auto replacement = emotes->makeReplacment(); - QString url("https://api.frankerfacez.com/v1/room/" + channelName); + for (auto jsonSet : jsonSets) { + auto jsonEmotes = jsonSet.toObject().value("emoticons").toArray(); - NetworkRequest request(url); - request.setCaller(QThread::currentThread()); - request.setTimeout(3000); - request.onSuccess([this, channelName, _map](auto result) { - auto rootNode = result.parseJson(); - auto map = _map.lock(); + for (auto jsonEmoteValue : jsonEmotes) { + auto jsonEmote = jsonEmoteValue.toObject(); - if (_map.expired()) { - return false; + auto name = EmoteName{jsonEmote.value("name").toString()}; + auto id = EmoteId{jsonEmote.value("id").toString()}; + auto urls = jsonEmote.value("urls").toObject(); + + auto emote = Emote(); + fillInEmoteData(urls, name, name.string + "
Global FFZ Emote", emote); + emote.homePage = Url{QString("https://www.frankerfacez.com/emoticon/%1-%2") + .arg(id.string) + .arg(name.string)}; + + replacement.add(name, emote); } + } - map->clear(); + return Success; +} - auto setsNode = rootNode.value("sets").toObject(); +void FfzEmotes::loadChannelEmotes(const QString &channelName, + std::function callback) +{ + // printf("[FFZEmotes] Reload FFZ Channel Emotes for channel %s\n", qPrintable(channelName)); - std::vector codes; - for (const QJsonValue &setNode : setsNode) { - auto emotesNode = setNode.toObject().value("emoticons").toArray(); + // QString url("https://api.frankerfacez.com/v1/room/" + channelName); - for (const QJsonValue &emoteNode : emotesNode) { - QJsonObject emoteObject = emoteNode.toObject(); + // NetworkRequest request(url); + // request.setCaller(QThread::currentThread()); + // request.setTimeout(3000); + // request.onSuccess([this, channelName, _map](auto result) -> Outcome { + // return this->parseChannelEmotes(result.parseJson()); + //}); - // margins - int id = emoteObject.value("id").toInt(); - QString code = emoteObject.value("name").toString(); + // request.execute(); +} - QJsonObject urls = emoteObject.value("urls").toObject(); +Outcome parseChannelEmotes(const QJsonObject &jsonRoot) +{ + // auto rootNode = result.parseJson(); + // auto map = _map.lock(); - auto emote = this->channelEmoteCache_.getOrAdd(id, [id, &code, &urls] { - EmoteData emoteData; - fillInEmoteData(urls, code, code + "
Channel FFZ Emote", emoteData); - emoteData.pageLink = - QString("https://www.frankerfacez.com/emoticon/%1-%2").arg(id).arg(code); + // if (_map.expired()) { + // return false; + //} - return emoteData; - }); + // map->clear(); - this->channelEmotes.insert(code, emote); - map->insert(code, emote); - codes.push_back(code); - } + // auto setsNode = rootNode.value("sets").toObject(); - this->channelEmoteCodes[channelName] = codes; - } + // std::vector codes; + // for (const QJsonValue &setNode : setsNode) { + // auto emotesNode = setNode.toObject().value("emoticons").toArray(); - return true; - }); + // for (const QJsonValue &emoteNode : emotesNode) { + // QJsonObject emoteObject = emoteNode.toObject(); - request.execute(); + // // margins + // int id = emoteObject.value("id").toInt(); + // QString code = emoteObject.value("name").toString(); + + // QJsonObject urls = emoteObject.value("urls").toObject(); + + // auto emote = this->channelEmoteCache_.getOrAdd(id, [id, &code, &urls] { + // EmoteData emoteData; + // fillInEmoteData(urls, code, code + "
Channel FFZ Emote", emoteData); + // emoteData.pageLink = + // QString("https://www.frankerfacez.com/emoticon/%1-%2").arg(id).arg(code); + + // return emoteData; + // }); + + // this->channelEmotes.insert(code, emote); + // map->insert(code, emote); + // codes.push_back(code); + // } + + // this->channelEmoteCodes[channelName] = codes; + //} + + return Success; } } // namespace chatterino diff --git a/src/providers/ffz/FfzEmotes.hpp b/src/providers/ffz/FfzEmotes.hpp index b216de0b0..d42885440 100644 --- a/src/providers/ffz/FfzEmotes.hpp +++ b/src/providers/ffz/FfzEmotes.hpp @@ -1,27 +1,36 @@ #pragma once -#include "common/Emotemap.hpp" -#include "common/SimpleSignalVector.hpp" -#include "util/ConcurrentMap.hpp" +#include -#include +#include "common/UniqueAccess.hpp" +#include "messages/Emote.hpp" +#include "messages/EmoteCache.hpp" namespace chatterino { -class FFZEmotes +class FfzEmotes final : std::enable_shared_from_this { -public: - EmoteMap globalEmotes; - SimpleSignalVector globalEmoteCodes; + static constexpr const char *globalEmoteApiUrl = "https://api.frankerfacez.com/v1/set/global"; + static constexpr const char *channelEmoteApiUrl = "https://api.betterttv.net/2/channels/"; - EmoteMap channelEmotes; - std::map> channelEmoteCodes; +public: + // FfzEmotes(); + + static std::shared_ptr create(); + + AccessGuard> accessGlobalEmotes() const; + boost::optional getGlobalEmote(const EmoteName &name); + boost::optional getEmote(const EmoteId &id); void loadGlobalEmotes(); - void loadChannelEmotes(const QString &channelName, std::weak_ptr channelEmoteMap); + void loadChannelEmotes(const QString &channelName, std::function callback); -private: - ConcurrentMap channelEmoteCache_; +protected: + Outcome parseGlobalEmotes(const QJsonObject &jsonRoot); + Outcome parseChannelEmotes(const QJsonObject &jsonRoot); + + UniqueAccess> globalEmotes_; + UniqueAccess channelEmoteCache_; }; } // namespace chatterino diff --git a/src/providers/irc/AbstractIrcServer.cpp b/src/providers/irc/AbstractIrcServer.cpp index fcbd306ae..532b78710 100644 --- a/src/providers/irc/AbstractIrcServer.cpp +++ b/src/providers/irc/AbstractIrcServer.cpp @@ -54,12 +54,9 @@ void AbstractIrcServer::connect() std::lock_guard lock2(this->channelMutex); for (std::weak_ptr &weak : this->channels.values()) { - std::shared_ptr chan = weak.lock(); - if (!chan) { - continue; + if (auto channel = std::shared_ptr(weak.lock())) { + this->readConnection_->sendRaw("JOIN #" + channel->getName()); } - - this->readConnection_->sendRaw("JOIN #" + chan->name); } this->writeConnection_->open(); diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 29d64d397..cd02c94ef 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -94,8 +94,6 @@ void IrcMessageHandler::handleRoomStateMessage(Communi::IrcMessage *message) auto roomId = it.value().toString(); twitchChannel->setRoomId(roomId); - - app->resources->loadChannelData(roomId); } // Room modes diff --git a/src/providers/twitch/PartialTwitchUser.cpp b/src/providers/twitch/PartialTwitchUser.cpp index 2033e977d..cc8c06f51 100644 --- a/src/providers/twitch/PartialTwitchUser.cpp +++ b/src/providers/twitch/PartialTwitchUser.cpp @@ -4,6 +4,7 @@ #include "debug/Log.hpp" #include "providers/twitch/TwitchCommon.hpp" +#include #include namespace chatterino { @@ -36,32 +37,32 @@ void PartialTwitchUser::getId(std::function successCallback, cons request.setCaller(caller); request.makeAuthorizedV5(getDefaultClientID()); - request.onSuccess([successCallback](auto result) { + request.onSuccess([successCallback](auto result) -> Outcome { auto root = result.parseJson(); if (!root.value("users").isArray()) { Log("API Error while getting user id, users is not an array"); - return false; + return Failure; } auto users = root.value("users").toArray(); if (users.size() != 1) { Log("API Error while getting user id, users array size is not 1"); - return false; + return Failure; } if (!users[0].isObject()) { Log("API Error while getting user id, first user is not an object"); - return false; + return Failure; } auto firstUser = users[0].toObject(); auto id = firstUser.value("_id"); if (!id.isString()) { Log("API Error: while getting user id, first user object `_id` key is not a " "string"); - return false; + return Failure; } successCallback(id.toString()); - return true; + return Success; }); request.execute(); diff --git a/src/providers/twitch/TwitchAccount.cpp b/src/providers/twitch/TwitchAccount.cpp index d5908166c..bb8510231 100644 --- a/src/providers/twitch/TwitchAccount.cpp +++ b/src/providers/twitch/TwitchAccount.cpp @@ -1,13 +1,45 @@ #include "providers/twitch/TwitchAccount.hpp" +#include + +#include "Application.hpp" #include "common/NetworkRequest.hpp" #include "debug/Log.hpp" #include "providers/twitch/PartialTwitchUser.hpp" #include "providers/twitch/TwitchCommon.hpp" +#include "singletons/Emotes.hpp" #include "util/RapidjsonHelpers.hpp" namespace chatterino { +namespace { + +EmoteName cleanUpCode(const EmoteName &dirtyEmoteCode) +{ + auto cleanCode = dirtyEmoteCode.string; + cleanCode.detach(); + + static QMap emoteNameReplacements{ + {"[oO](_|\\.)[oO]", "O_o"}, {"\\>\\;\\(", ">("}, {"\\<\\;3", "<3"}, + {"\\:-?(o|O)", ":O"}, {"\\:-?(p|P)", ":P"}, {"\\:-?[\\\\/]", ":/"}, + {"\\:-?[z|Z|\\|]", ":Z"}, {"\\:-?\\(", ":("}, {"\\:-?\\)", ":)"}, + {"\\:-?D", ":D"}, {"\\;-?(p|P)", ";P"}, {"\\;-?\\)", ";)"}, + {"R-?\\)", "R)"}, {"B-?\\)", "B)"}, + }; + + auto it = emoteNameReplacements.find(dirtyEmoteCode.string); + if (it != emoteNameReplacements.end()) { + cleanCode = it.value(); + } + + cleanCode.replace("<", "<"); + cleanCode.replace(">", ">"); + + return {cleanCode}; +} + +} // namespace + TwitchAccount::TwitchAccount(const QString &username, const QString &oauthToken, const QString &oauthClient, const QString &userID) : Account(ProviderId::Twitch) @@ -78,20 +110,20 @@ void TwitchAccount::loadIgnores() NetworkRequest req(url); req.setCaller(QThread::currentThread()); req.makeAuthorizedV5(this->getOAuthClient(), this->getOAuthToken()); - req.onSuccess([=](auto result) { + req.onSuccess([=](auto result) -> Outcome { auto document = result.parseRapidJson(); if (!document.IsObject()) { - return false; + return Failure; } auto blocksIt = document.FindMember("blocks"); if (blocksIt == document.MemberEnd()) { - return false; + return Failure; } const auto &blocks = blocksIt->value; if (!blocks.IsArray()) { - return false; + return Failure; } { @@ -116,7 +148,7 @@ void TwitchAccount::loadIgnores() } } - return true; + return Success; }); req.execute(); @@ -148,25 +180,25 @@ void TwitchAccount::ignoreByID(const QString &targetUserID, const QString &targe return true; }); - req.onSuccess([=](auto result) { + req.onSuccess([=](auto result) -> Outcome { auto document = result.parseRapidJson(); if (!document.IsObject()) { onFinished(IgnoreResult_Failed, "Bad JSON data while ignoring user " + targetName); - return false; + return Failure; } auto userIt = document.FindMember("user"); if (userIt == document.MemberEnd()) { onFinished(IgnoreResult_Failed, "Bad JSON data while ignoring user (missing user) " + targetName); - return false; + return Failure; } TwitchUser ignoredUser; if (!rj::getSafe(userIt->value, ignoredUser)) { onFinished(IgnoreResult_Failed, "Bad JSON data while ignoring user (invalid user) " + targetName); - return false; + return Failure; } { std::lock_guard lock(this->ignoresMutex_); @@ -177,12 +209,12 @@ void TwitchAccount::ignoreByID(const QString &targetUserID, const QString &targe existingUser.update(ignoredUser); onFinished(IgnoreResult_AlreadyIgnored, "User " + targetName + " is already ignored"); - return false; + return Failure; } } onFinished(IgnoreResult_Success, "Successfully ignored user " + targetName); - return true; + return Success; }); req.execute(); @@ -217,7 +249,7 @@ void TwitchAccount::unignoreByID( return true; }); - req.onSuccess([=](auto result) { + req.onSuccess([=](auto result) -> Outcome { auto document = result.parseRapidJson(); TwitchUser ignoredUser; ignoredUser.id = targetUserID; @@ -228,7 +260,7 @@ void TwitchAccount::unignoreByID( } onFinished(UnignoreResult_Success, "Successfully unignored user " + targetName); - return true; + return Success; }); req.execute(); @@ -254,10 +286,10 @@ void TwitchAccount::checkFollow(const QString targetUserID, return true; }); - req.onSuccess([=](auto result) { + req.onSuccess([=](auto result) -> Outcome { auto document = result.parseRapidJson(); onFinished(FollowResult_Following); - return true; + return Success; }); req.execute(); @@ -274,10 +306,10 @@ void TwitchAccount::followUser(const QString userID, std::function succe request.makeAuthorizedV5(this->getOAuthClient(), this->getOAuthToken()); // TODO: Properly check result of follow request - request.onSuccess([successCallback](auto result) { + request.onSuccess([successCallback](auto result) -> Outcome { successCallback(); - return true; + return Success; }); request.execute(); @@ -301,10 +333,10 @@ void TwitchAccount::unfollowUser(const QString userID, std::function suc return true; }); - request.onSuccess([successCallback](const auto &document) { + request.onSuccess([successCallback](const auto &document) -> Outcome { successCallback(); - return true; + return Success; }); request.execute(); @@ -317,7 +349,7 @@ std::set TwitchAccount::getIgnores() const return this->ignores_; } -void TwitchAccount::loadEmotes(std::function cb) +void TwitchAccount::loadEmotes() { Log("Loading Twitch emotes for user {}", this->getUserName()); @@ -346,11 +378,126 @@ void TwitchAccount::loadEmotes(std::function return true; }); - req.onSuccess([=](auto result) { - cb(result.parseRapidJson()); + req.onSuccess([=](auto result) -> Outcome { + this->parseEmotes(result.parseRapidJson()); + + return Success; + }); + + req.execute(); +} + +AccessGuard TwitchAccount::accessEmotes() const +{ + return this->emotes_.accessConst(); +} + +void TwitchAccount::parseEmotes(const rapidjson::Document &root) +{ + auto emoteData = this->emotes_.access(); + + emoteData->emoteSets.clear(); + emoteData->allEmoteNames.clear(); + + auto emoticonSets = root.FindMember("emoticon_sets"); + if (emoticonSets == root.MemberEnd() || !emoticonSets->value.IsObject()) { + Log("No emoticon_sets in load emotes response"); + return; + } + + for (const auto &emoteSetJSON : emoticonSets->value.GetObject()) { + auto emoteSet = std::make_shared(); + + emoteSet->key = emoteSetJSON.name.GetString(); + + this->loadEmoteSetData(emoteSet); + + for (const rapidjson::Value &emoteJSON : emoteSetJSON.value.GetArray()) { + if (!emoteJSON.IsObject()) { + Log("Emote value was invalid"); + return; + } + + uint64_t idNumber; + if (!rj::getSafe(emoteJSON, "id", idNumber)) { + Log("No ID key found in Emote value"); + return; + } + + EmoteName code; + if (!rj::getSafe(emoteJSON, "code", code)) { + Log("No code key found in Emote value"); + return; + } + + auto id = EmoteId{QString::number(idNumber)}; + + auto cleanCode = cleanUpCode(code); + emoteSet->emotes.emplace_back(TwitchEmote{id, cleanCode}); + emoteData->allEmoteNames.push_back(cleanCode); + + auto emote = getApp()->emotes->twitch.getOrCreateEmote(id, code); + emoteData->emotes.emplace(code, emote); + } + + emoteData->emoteSets.emplace_back(emoteSet); + } +}; + +void TwitchAccount::loadEmoteSetData(std::shared_ptr emoteSet) +{ + if (!emoteSet) { + Log("null emote set sent"); + return; + } + + auto staticSetIt = this->staticEmoteSets.find(emoteSet->key); + if (staticSetIt != this->staticEmoteSets.end()) { + const auto &staticSet = staticSetIt->second; + emoteSet->channelName = staticSet.channelName; + emoteSet->text = staticSet.text; + return; + } + + NetworkRequest req("https://braize.pajlada.com/chatterino/twitchemotes/set/" + emoteSet->key + + "/"); + req.setUseQuickLoadCache(true); + + req.onError([](int errorCode) -> bool { + Log("Error code {} while loading emote set data", errorCode); return true; }); + req.onSuccess([emoteSet](auto result) -> Outcome { + auto root = result.parseRapidJson(); + if (!root.IsObject()) { + return Failure; + } + + std::string emoteSetID; + QString channelName; + QString type; + if (!rj::getSafe(root, "channel_name", channelName)) { + return Failure; + } + + if (!rj::getSafe(root, "type", type)) { + return Failure; + } + + Log("Loaded twitch emote set data for {}!", emoteSet->key); + + if (type == "sub") { + emoteSet->text = QString("Twitch Subscriber Emote (%1)").arg(channelName); + } else { + emoteSet->text = QString("Twitch Account Emote (%1)").arg(channelName); + } + + emoteSet->channelName = channelName; + + return Success; + }); + req.execute(); } diff --git a/src/providers/twitch/TwitchAccount.hpp b/src/providers/twitch/TwitchAccount.hpp index ee215209a..be97a0653 100644 --- a/src/providers/twitch/TwitchAccount.hpp +++ b/src/providers/twitch/TwitchAccount.hpp @@ -1,6 +1,8 @@ #pragma once +#include "common/UniqueAccess.hpp" #include "controllers/accounts/Account.hpp" +#include "messages/Emote.hpp" #include "providers/twitch/TwitchUser.hpp" #include @@ -33,6 +35,28 @@ enum FollowResult { class TwitchAccount : public Account { public: + struct TwitchEmote { + EmoteId id; + EmoteName name; + }; + + struct EmoteSet { + QString key; + QString channelName; + QString text; + std::vector emotes; + }; + + std::map staticEmoteSets; + + struct TwitchAccountEmoteData { + std::vector> emoteSets; + + std::vector allEmoteNames; + + EmoteMap emotes; + }; + TwitchAccount(const QString &username, const QString &oauthToken_, const QString &oauthClient_, const QString &_userID); @@ -70,11 +94,15 @@ public: std::set getIgnores() const; - void loadEmotes(std::function cb); + void loadEmotes(); + AccessGuard accessEmotes() const; QColor color; private: + void parseEmotes(const rapidjson::Document &document); + void loadEmoteSetData(std::shared_ptr emoteSet); + QString oauthClient_; QString oauthToken_; QString userName_; @@ -83,6 +111,9 @@ private: mutable std::mutex ignoresMutex_; std::set ignores_; + + // std::map emotes; + UniqueAccess emotes_; }; } // namespace chatterino diff --git a/src/providers/twitch/TwitchApi.cpp b/src/providers/twitch/TwitchApi.cpp index 82d9dc3f3..58ab03af1 100644 --- a/src/providers/twitch/TwitchApi.cpp +++ b/src/providers/twitch/TwitchApi.cpp @@ -16,23 +16,23 @@ void TwitchApi::findUserId(const QString user, std::function succ request.setCaller(QThread::currentThread()); request.makeAuthorizedV5(getDefaultClientID()); request.setTimeout(30000); - request.onSuccess([successCallback](auto result) mutable { + request.onSuccess([successCallback](auto result) mutable -> Outcome { auto root = result.parseJson(); if (!root.value("users").isArray()) { Log("API Error while getting user id, users is not an array"); successCallback(""); - return false; + return Failure; } auto users = root.value("users").toArray(); if (users.size() != 1) { Log("API Error while getting user id, users array size is not 1"); successCallback(""); - return false; + return Failure; } if (!users[0].isObject()) { Log("API Error while getting user id, first user is not an object"); successCallback(""); - return false; + return Failure; } auto firstUser = users[0].toObject(); auto id = firstUser.value("_id"); @@ -40,10 +40,10 @@ void TwitchApi::findUserId(const QString user, std::function succ Log("API Error: while getting user id, first user object `_id` key is not a " "string"); successCallback(""); - return false; + return Failure; } successCallback(id.toString()); - return true; + return Success; }); request.execute(); diff --git a/src/providers/twitch/TwitchBadges.cpp b/src/providers/twitch/TwitchBadges.cpp new file mode 100644 index 000000000..db329d306 --- /dev/null +++ b/src/providers/twitch/TwitchBadges.cpp @@ -0,0 +1,58 @@ +#include "TwitchBadges.hpp" + +#include +#include +#include +#include +#include "common/NetworkRequest.hpp" + +namespace chatterino { + +TwitchBadges::TwitchBadges() +{ +} + +void TwitchBadges::initialize(Settings &settings, Paths &paths) +{ + this->loadTwitchBadges(); +} + +void TwitchBadges::loadTwitchBadges() +{ + static QString url("https://badges.twitch.tv/v1/badges/global/display?language=en"); + + NetworkRequest req(url); + req.setCaller(QThread::currentThread()); + req.onSuccess([this](auto result) -> Outcome { + auto root = result.parseJson(); + QJsonObject sets = root.value("badge_sets").toObject(); + + for (QJsonObject::iterator it = sets.begin(); it != sets.end(); ++it) { + QJsonObject versions = it.value().toObject().value("versions").toObject(); + + for (auto versionIt = std::begin(versions); versionIt != std::end(versions); + ++versionIt) { + auto emote = + Emote{{""}, + ImageSet{ + Image::fromUrl({root.value("image_url_1x").toString()}, 1), + Image::fromUrl({root.value("image_url_2x").toString()}, 0.5), + Image::fromUrl({root.value("image_url_4x").toString()}, 0.25), + }, + Tooltip{root.value("description").toString()}, + Url{root.value("clickURL").toString()}}; + // "title" + // "clickAction" + + QJsonObject versionObj = versionIt.value().toObject(); + this->badges.emplace(versionIt.key(), std::make_shared(emote)); + } + } + + return Success; + }); + + req.execute(); +} + +} // namespace chatterino diff --git a/src/providers/twitch/TwitchBadges.hpp b/src/providers/twitch/TwitchBadges.hpp new file mode 100644 index 000000000..a228365da --- /dev/null +++ b/src/providers/twitch/TwitchBadges.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include +#include "util/QStringHash.hpp" + +namespace chatterino { + +class Settings; +class Paths; + +class TwitchBadges +{ +public: + TwitchBadges(); + + void initialize(Settings &settings, Paths &paths); + +private: + void loadTwitchBadges(); + + std::unordered_map badges; +}; + +} // namespace chatterino diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index 6a77a3e89..ea1d0e3ae 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -5,32 +5,34 @@ #include "controllers/accounts/AccountController.hpp" #include "debug/Log.hpp" #include "messages/Message.hpp" +#include "providers/bttv/LoadBttvChannelEmote.hpp" #include "providers/twitch/PubsubClient.hpp" #include "providers/twitch/TwitchCommon.hpp" #include "providers/twitch/TwitchMessageBuilder.hpp" +#include "providers/twitch/TwitchParseCheerEmotes.hpp" #include "singletons/Emotes.hpp" #include "singletons/Settings.hpp" #include "util/PostToThread.hpp" #include #include +#include +#include #include #include namespace chatterino { -TwitchChannel::TwitchChannel(const QString &channelName) - : Channel(channelName, Channel::Type::Twitch) - , bttvEmotes_(new EmoteMap) - , ffzEmotes_(new EmoteMap) +TwitchChannel::TwitchChannel(const QString &name) + : Channel(name, Channel::Type::Twitch) , subscriptionUrl_("https://www.twitch.tv/subs/" + name) , channelUrl_("https://twitch.tv/" + name) , popoutPlayerUrl_("https://player.twitch.tv/?channel=" + name) , mod_(false) { - Log("[TwitchChannel:{}] Opened", this->name); + Log("[TwitchChannel:{}] Opened", name); - this->refreshChannelEmotes(); + // this->refreshChannelEmotes(); // this->refreshViewerList(); this->managedConnect(getApp()->accounts->twitch.currentUserChanged, @@ -38,13 +40,17 @@ TwitchChannel::TwitchChannel(const QString &channelName) // pubsub this->userStateChanged.connect([=] { this->refreshPubsub(); }); - this->roomIdChanged.connect([=] { this->refreshPubsub(); }); this->managedConnect(getApp()->accounts->twitch.currentUserChanged, [=] { this->refreshPubsub(); }); this->refreshPubsub(); // room id loaded -> refresh live status - this->roomIdChanged.connect([this]() { this->refreshLiveStatus(); }); + this->roomIdChanged.connect([this]() { + this->refreshPubsub(); + this->refreshLiveStatus(); + this->loadBadges(); + this->loadCheerEmotes(); + }); // timers QObject::connect(&this->chattersListTimer_, &QTimer::timeout, @@ -68,7 +74,7 @@ TwitchChannel::TwitchChannel(const QString &channelName) bool TwitchChannel::isEmpty() const { - return this->name.isEmpty(); + return this->getName().isEmpty(); } bool TwitchChannel::canSendMessage() const @@ -78,12 +84,15 @@ bool TwitchChannel::canSendMessage() const void TwitchChannel::refreshChannelEmotes() { - auto app = getApp(); - - Log("[TwitchChannel:{}] Reloading channel emotes", this->name); - - app->emotes->bttv.loadChannelEmotes(this->name, this->bttvEmotes_); - app->emotes->ffz.loadChannelEmotes(this->name, this->ffzEmotes_); + loadBttvChannelEmotes(this->getName(), [this, weak = weakOf(this)](auto &&emoteMap) { + if (auto shared = weak.lock()) // + *this->bttvEmotes_.access() = emoteMap; + }); + getApp()->emotes->ffz.loadChannelEmotes(this->getName(), + [this, weak = weakOf(this)](auto &&emoteMap) { + if (auto shared = weak.lock()) + *this->ffzEmotes_.access() = emoteMap; + }); } void TwitchChannel::sendMessage(const QString &message) @@ -99,7 +108,7 @@ void TwitchChannel::sendMessage(const QString &message) return; } - Log("[TwitchChannel:{}] Send message: {}", this->name, message); + Log("[TwitchChannel:{}] Send message: {}", this->getName(), message); // Do last message processing QString parsedMessage = app->emotes->emojis.replaceShortCodes(message); @@ -119,7 +128,7 @@ void TwitchChannel::sendMessage(const QString &message) } bool messageSent = false; - this->sendMessageSignal.invoke(this->name, parsedMessage, messageSent); + this->sendMessageSignal.invoke(this->getName(), parsedMessage, messageSent); if (messageSent) { qDebug() << "sent"; @@ -145,7 +154,7 @@ bool TwitchChannel::isBroadcaster() const { auto app = getApp(); - return this->name == app->accounts->twitch.getCurrent()->getUserName(); + return this->getName() == app->accounts->twitch.getCurrent()->getUserName(); } void TwitchChannel::addRecentChatter(const std::shared_ptr &message) @@ -243,14 +252,32 @@ const AccessGuard TwitchChannel::accessStreamStatus return this->streamStatus_.access(); } -const EmoteMap &TwitchChannel::getFfzEmotes() const +boost::optional TwitchChannel::getBttvEmote(const EmoteName &name) const { - return *this->ffzEmotes_; + auto emotes = this->bttvEmotes_.access(); + auto it = emotes->find(name); + + if (it == emotes->end()) return boost::none; + return it->second; } -const EmoteMap &TwitchChannel::getBttvEmotes() const +boost::optional TwitchChannel::getFfzEmote(const EmoteName &name) const { - return *this->bttvEmotes_; + auto emotes = this->bttvEmotes_.access(); + auto it = emotes->find(name); + + if (it == emotes->end()) return boost::none; + return it->second; +} + +AccessGuard TwitchChannel::accessBttvEmotes() const +{ + return this->bttvEmotes_.accessConst(); +} + +AccessGuard TwitchChannel::accessFfzEmotes() const +{ + return this->ffzEmotes_.accessConst(); } const QString &TwitchChannel::getSubscriptionUrl() @@ -289,12 +316,12 @@ void TwitchChannel::refreshLiveStatus() auto roomID = this->getRoomId(); if (roomID.isEmpty()) { - Log("[TwitchChannel:{}] Refreshing live status (Missing ID)", this->name); + Log("[TwitchChannel:{}] Refreshing live status (Missing ID)", this->getName()); this->setLive(false); return; } - Log("[TwitchChannel:{}] Refreshing live status", this->name); + Log("[TwitchChannel:{}] Refreshing live status", this->getName()); QString url("https://api.twitch.tv/kraken/streams/" + roomID); @@ -315,16 +342,16 @@ void TwitchChannel::refreshLiveStatus() // request.execute(); } -bool TwitchChannel::parseLiveStatus(const rapidjson::Document &document) +Outcome TwitchChannel::parseLiveStatus(const rapidjson::Document &document) { if (!document.IsObject()) { Log("[TwitchChannel:refreshLiveStatus] root is not an object"); - return false; + return Failure; } if (!document.HasMember("stream")) { Log("[TwitchChannel:refreshLiveStatus] Missing stream in root"); - return false; + return Failure; } const auto &stream = document["stream"]; @@ -332,21 +359,21 @@ bool TwitchChannel::parseLiveStatus(const rapidjson::Document &document) if (!stream.IsObject()) { // Stream is offline (stream is most likely null) this->setLive(false); - return false; + return Failure; } if (!stream.HasMember("viewers") || !stream.HasMember("game") || !stream.HasMember("channel") || !stream.HasMember("created_at")) { Log("[TwitchChannel:refreshLiveStatus] Missing members in stream"); this->setLive(false); - return false; + return Failure; } const rapidjson::Value &streamChannel = stream["channel"]; if (!streamChannel.IsObject() || !streamChannel.HasMember("status")) { Log("[TwitchChannel:refreshLiveStatus] Missing member \"status\" in channel"); - return false; + return Failure; } // Stream is live @@ -384,7 +411,7 @@ bool TwitchChannel::parseLiveStatus(const rapidjson::Document &document) // Signal all listeners that the stream status has been updated this->liveStatusChanged.invoke(); - return true; + return Success; } void TwitchChannel::loadRecentMessages() @@ -396,24 +423,20 @@ void TwitchChannel::loadRecentMessages() request.makeAuthorizedV5(getDefaultClientID()); request.setCaller(QThread::currentThread()); - request.onSuccess([this, weak = this->weak_from_this()](auto result) { - // channel still exists? + request.onSuccess([this, weak = weakOf(this)](auto result) -> Outcome { ChannelPtr shared = weak.lock(); - if (!shared) return false; + if (!shared) return Failure; - // parse json return this->parseRecentMessages(result.parseJson()); }); request.execute(); } -bool TwitchChannel::parseRecentMessages(const QJsonObject &jsonRoot) +Outcome TwitchChannel::parseRecentMessages(const QJsonObject &jsonRoot) { QJsonArray jsonMessages = jsonRoot.value("messages").toArray(); - if (jsonMessages.empty()) { - return false; - } + if (jsonMessages.empty()) return Failure; std::vector messages; @@ -434,7 +457,7 @@ bool TwitchChannel::parseRecentMessages(const QJsonObject &jsonRoot) this->addMessagesAtStart(messages); - return true; + return Success; } void TwitchChannel::refreshPubsub() @@ -460,13 +483,13 @@ void TwitchChannel::refreshViewerList() } // get viewer list - NetworkRequest request("https://tmi.twitch.tv/group/user/" + this->name + "/chatters"); + NetworkRequest request("https://tmi.twitch.tv/group/user/" + this->getName() + "/chatters"); request.setCaller(QThread::currentThread()); - request.onSuccess([this, weak = this->weak_from_this()](auto result) { + request.onSuccess([this, weak = this->weak_from_this()](auto result) -> Outcome { // channel still exists? auto shared = weak.lock(); - if (!shared) return false; + if (!shared) return Failure; return this->parseViewerList(result.parseJson()); }); @@ -474,7 +497,7 @@ void TwitchChannel::refreshViewerList() request.execute(); } -bool TwitchChannel::parseViewerList(const QJsonObject &jsonRoot) +Outcome TwitchChannel::parseViewerList(const QJsonObject &jsonRoot) { static QStringList categories = {"moderators", "staff", "admins", "global_mods", "viewers"}; @@ -487,7 +510,118 @@ bool TwitchChannel::parseViewerList(const QJsonObject &jsonRoot) } } - return true; + return Success; +} + +void TwitchChannel::loadBadges() +{ + auto url = Url{"https://badges.twitch.tv/v1/badges/channels/" + this->getRoomId() + + "/display?language=en"}; + NetworkRequest req(url.string); + req.setCaller(QThread::currentThread()); + + req.onSuccess([this, weak = weakOf(this)](auto result) -> Outcome { + auto shared = weak.lock(); + if (!shared) return Failure; + + auto badgeSets = this->badgeSets_.access(); + + auto jsonRoot = result.parseJson(); + + auto _ = jsonRoot["badge_sets"].toObject(); + for (auto jsonBadgeSet = _.begin(); jsonBadgeSet != _.end(); jsonBadgeSet++) { + auto &versions = (*badgeSets)[jsonBadgeSet.key()]; + + auto _ = jsonBadgeSet->toObject()["versions"].toObject(); + for (auto jsonVersion_ = _.begin(); jsonVersion_ != _.end(); jsonVersion_++) { + auto jsonVersion = jsonVersion_->toObject(); + auto emote = std::make_shared( + Emote{EmoteName{}, + ImageSet{Image::fromUrl({jsonVersion["image_url_1x"].toString()}), + Image::fromUrl({jsonVersion["image_url_2x"].toString()}), + Image::fromUrl({jsonVersion["image_url_4x"].toString()})}, + Tooltip{jsonRoot["description"].toString()}, + Url{jsonVersion["clickURL"].toString()}}); + + versions.emplace(jsonVersion_.key(), emote); + }; + } + + return Success; + }); + + req.execute(); +} + +void TwitchChannel::loadCheerEmotes() +{ + auto url = Url{"https://api.twitch.tv/kraken/bits/actions?channel_id=" + this->getRoomId()}; + auto request = NetworkRequest::twitchRequest(url.string); + request.setCaller(QThread::currentThread()); + + request.onSuccess([this, weak = weakOf(this)](auto result) -> Outcome { + auto cheerEmoteSets = ParseCheermoteSets(result.parseRapidJson()); + + for (auto &set : cheerEmoteSets) { + auto cheerEmoteSet = CheerEmoteSet(); + cheerEmoteSet.regex = QRegularExpression("^" + set.prefix.toLower() + "([1-9][0-9]*)$"); + + for (auto &tier : set.tiers) { + CheerEmote cheerEmote; + + cheerEmote.color = QColor(tier.color); + cheerEmote.minBits = tier.minBits; + + // TODO(pajlada): We currently hardcode dark here :| + // We will continue to do so for now since we haven't had to + // solve that anywhere else + + cheerEmote.animatedEmote = + std::make_shared(Emote{EmoteName{"cheer emote"}, + ImageSet{ + tier.images["dark"]["animated"]["1"], + tier.images["dark"]["animated"]["2"], + tier.images["dark"]["animated"]["4"], + }, + Tooltip{}, Url{}}); + cheerEmote.staticEmote = + std::make_shared(Emote{EmoteName{"cheer emote"}, + ImageSet{ + tier.images["dark"]["static"]["1"], + tier.images["dark"]["static"]["2"], + tier.images["dark"]["static"]["4"], + }, + Tooltip{}, Url{}}); + + cheerEmoteSet.cheerEmotes.emplace_back(cheerEmote); + } + + std::sort(cheerEmoteSet.cheerEmotes.begin(), cheerEmoteSet.cheerEmotes.end(), + [](const auto &lhs, const auto &rhs) { + return lhs.minBits < rhs.minBits; // + }); + + this->cheerEmoteSets_.emplace_back(cheerEmoteSet); + } + + return Success; + }); + + request.execute(); +} + +boost::optional TwitchChannel::getTwitchBadge(const QString &set, + const QString &version) const +{ + auto badgeSets = this->badgeSets_.access(); + auto it = badgeSets->find(set); + if (it != badgeSets->end()) { + auto it2 = it->second.find(version); + if (it2 != it->second.end()) { + return it2->second; + } + } + return boost::none; } } // namespace chatterino diff --git a/src/providers/twitch/TwitchChannel.hpp b/src/providers/twitch/TwitchChannel.hpp index c2986029d..480b3d637 100644 --- a/src/providers/twitch/TwitchChannel.hpp +++ b/src/providers/twitch/TwitchChannel.hpp @@ -6,12 +6,14 @@ #include "common/Common.hpp" #include "common/MutexValue.hpp" #include "common/UniqueAccess.hpp" +#include "messages/Emote.hpp" #include "singletons/Emotes.hpp" #include "util/ConcurrentMap.hpp" #include #include +#include namespace chatterino { @@ -68,12 +70,16 @@ public: void setRoomModes(const RoomModes &roomModes_); const AccessGuard accessStreamStatus() const; - const EmoteMap &getFfzEmotes() const; - const EmoteMap &getBttvEmotes() const; + boost::optional getBttvEmote(const EmoteName &name) const; + boost::optional getFfzEmote(const EmoteName &name) const; + AccessGuard accessBttvEmotes() const; + AccessGuard accessFfzEmotes() const; const QString &getSubscriptionUrl(); const QString &getChannelUrl(); const QString &getPopoutPlayerUrl(); + boost::optional getTwitchBadge(const QString &set, const QString &version) const; + // Signals pajlada::Signals::NoArgSignal roomIdChanged; pajlada::Signals::NoArgSignal liveStatusChanged; @@ -86,26 +92,43 @@ private: QString localizedName; }; + struct CheerEmote { + // a Cheermote indicates one tier + QColor color; + int minBits; + + EmotePtr animatedEmote; + EmotePtr staticEmote; + }; + + struct CheerEmoteSet { + QRegularExpression regex; + std::vector cheerEmotes; + }; + explicit TwitchChannel(const QString &channelName); // Methods void refreshLiveStatus(); - bool parseLiveStatus(const rapidjson::Document &document); + Outcome parseLiveStatus(const rapidjson::Document &document); void refreshPubsub(); void refreshViewerList(); - bool parseViewerList(const QJsonObject &jsonRoot); + Outcome parseViewerList(const QJsonObject &jsonRoot); void loadRecentMessages(); - bool parseRecentMessages(const QJsonObject &jsonRoot); + Outcome parseRecentMessages(const QJsonObject &jsonRoot); void setLive(bool newLiveStatus); + void loadBadges(); + void loadCheerEmotes(); + // Twitch data UniqueAccess streamStatus_; UniqueAccess userState_; UniqueAccess roomModes_; - const std::shared_ptr bttvEmotes_; - const std::shared_ptr ffzEmotes_; + UniqueAccess bttvEmotes_; + UniqueAccess ffzEmotes_; const QString subscriptionUrl_; const QString channelUrl_; const QString popoutPlayerUrl_; @@ -118,6 +141,10 @@ private: UniqueAccess partedUsers_; bool partedUsersMergeQueued_ = false; + // "subscribers": { "0": ... "3": ... "6": ... + UniqueAccess>> badgeSets_; + std::vector cheerEmoteSets_; + // -- QByteArray messageSuffix_; QString lastSentMessage_; diff --git a/src/providers/twitch/TwitchEmotes.cpp b/src/providers/twitch/TwitchEmotes.cpp index 3f94a80b3..80337912e 100644 --- a/src/providers/twitch/TwitchEmotes.cpp +++ b/src/providers/twitch/TwitchEmotes.cpp @@ -6,228 +6,64 @@ #include "messages/Image.hpp" #include "util/RapidjsonHelpers.hpp" -#define TWITCH_EMOTE_TEMPLATE "https://static-cdn.jtvnw.net/emoticons/v1/{id}/{scale}" - namespace chatterino { -namespace { - -QString getEmoteLink(const QString &id, const QString &emoteScale) -{ - QString value = TWITCH_EMOTE_TEMPLATE; - - value.detach(); - - return value.replace("{id}", id).replace("{scale}", emoteScale); -} - -QString cleanUpCode(const QString &dirtyEmoteCode) -{ - QString cleanCode = dirtyEmoteCode; - // clang-format off - static QMap emoteNameReplacements{ - {"[oO](_|\\.)[oO]", "O_o"}, {"\\>\\;\\(", ">("}, {"\\<\\;3", "<3"}, - {"\\:-?(o|O)", ":O"}, {"\\:-?(p|P)", ":P"}, {"\\:-?[\\\\/]", ":/"}, - {"\\:-?[z|Z|\\|]", ":Z"}, {"\\:-?\\(", ":("}, {"\\:-?\\)", ":)"}, - {"\\:-?D", ":D"}, {"\\;-?(p|P)", ";P"}, {"\\;-?\\)", ";)"}, - {"R-?\\)", "R)"}, {"B-?\\)", "B)"}, - }; - // clang-format on - - auto it = emoteNameReplacements.find(dirtyEmoteCode); - if (it != emoteNameReplacements.end()) { - cleanCode = it.value(); - } - - cleanCode.replace("<", "<"); - cleanCode.replace(">", ">"); - - return cleanCode; -} - -} // namespace - TwitchEmotes::TwitchEmotes() { - { - EmoteSet emoteSet; - emoteSet.key = "19194"; - emoteSet.text = "Twitch Prime Emotes"; - this->staticEmoteSets[emoteSet.key] = std::move(emoteSet); - } - - { - EmoteSet emoteSet; - emoteSet.key = "0"; - emoteSet.text = "Twitch Global Emotes"; - this->staticEmoteSets[emoteSet.key] = std::move(emoteSet); - } } // id is used for lookup // emoteName is used for giving a name to the emote in case it doesn't exist -EmoteData TwitchEmotes::getEmoteById(const QString &id, const QString &emoteName) +EmotePtr TwitchEmotes::getOrCreateEmote(const EmoteId &id, const EmoteName &name_) { - QString _emoteName = emoteName; - _emoteName.replace("<", "<"); - _emoteName.replace(">", ">"); - - // clang-format off - static QMap emoteNameReplacements{ - {"[oO](_|\\.)[oO]", "O_o"}, {"\\>\\;\\(", ">("}, {"\\<\\;3", "<3"}, - {"\\:-?(o|O)", ":O"}, {"\\:-?(p|P)", ":P"}, {"\\:-?[\\\\/]", ":/"}, - {"\\:-?[z|Z|\\|]", ":Z"}, {"\\:-?\\(", ":("}, {"\\:-?\\)", ":)"}, - {"\\:-?D", ":D"}, {"\\;-?(p|P)", ";P"}, {"\\;-?\\)", ";)"}, + static QMap replacements{ + {"[oO](_|\\.)[oO]", "O_o"}, {"\\>\\;\\(", ">("}, {"\\<\\;3", "<3"}, + {"\\:-?(o|O)", ":O"}, {"\\:-?(p|P)", ":P"}, {"\\:-?[\\\\/]", ":/"}, + {"\\:-?[z|Z|\\|]", ":Z"}, {"\\:-?\\(", ":("}, {"\\:-?\\)", ":)"}, + {"\\:-?D", ":D"}, {"\\;-?(p|P)", ";P"}, {"\\;-?\\)", ";)"}, {"R-?\\)", "R)"}, {"B-?\\)", "B)"}, }; - // clang-format on - auto it = emoteNameReplacements.find(_emoteName); - if (it != emoteNameReplacements.end()) { - _emoteName = it.value(); + auto name = name_.string; + name.detach(); + + // replace < > + name.replace("<", "<"); + name.replace(">", ">"); + + // replace regexes + auto it = replacements.find(name); + if (it != replacements.end()) { + name = it.value(); } - return twitchEmoteFromCache_.getOrAdd(id, [&emoteName, &_emoteName, &id] { - EmoteData newEmoteData; - auto cleanCode = cleanUpCode(emoteName); - newEmoteData.image1x = - new Image(getEmoteLink(id, "1.0"), 1, emoteName, _emoteName + "
Twitch Emote"); - newEmoteData.image1x->setCopyString(cleanCode); + // search in cache or create new emote + auto cache = this->twitchEmotesCache_.access(); + auto shared = (*cache)[id].lock(); - newEmoteData.image2x = - new Image(getEmoteLink(id, "2.0"), .5, emoteName, _emoteName + "
Twitch Emote"); - newEmoteData.image2x->setCopyString(cleanCode); + if (!shared) { + (*cache)[id] = shared = + std::make_shared(Emote{EmoteName{name}, + ImageSet{ + Image::fromUrl(getEmoteLink(id, "1.0"), 1), + Image::fromUrl(getEmoteLink(id, "2.0"), 0.5), + Image::fromUrl(getEmoteLink(id, "3.0"), 0.25), + }, + Tooltip{name}, Url{}}); + } - newEmoteData.image3x = - new Image(getEmoteLink(id, "3.0"), .25, emoteName, _emoteName + "
Twitch Emote"); - - newEmoteData.image3x->setCopyString(cleanCode); - - return newEmoteData; - }); + return shared; } -void TwitchEmotes::refresh(const std::shared_ptr &user) +Url TwitchEmotes::getEmoteLink(const EmoteId &id, const QString &emoteScale) { - const auto &roomID = user->getUserId(); - TwitchAccountEmoteData &emoteData = this->emotes[roomID]; - - if (emoteData.filled) { - Log("Emotes are already loaded for room id {}", roomID); - return; - } - - auto loadEmotes = [=, &emoteData](const rapidjson::Document &root) { - emoteData.emoteSets.clear(); - emoteData.emoteCodes.clear(); - - auto emoticonSets = root.FindMember("emoticon_sets"); - if (emoticonSets == root.MemberEnd() || !emoticonSets->value.IsObject()) { - Log("No emoticon_sets in load emotes response"); - return; - } - - for (const auto &emoteSetJSON : emoticonSets->value.GetObject()) { - auto emoteSet = std::make_shared(); - - emoteSet->key = emoteSetJSON.name.GetString(); - - this->loadSetData(emoteSet); - - for (const rapidjson::Value &emoteJSON : emoteSetJSON.value.GetArray()) { - if (!emoteJSON.IsObject()) { - Log("Emote value was invalid"); - return; - } - - QString id, code; - - uint64_t idNumber; - - if (!rj::getSafe(emoteJSON, "id", idNumber)) { - Log("No ID key found in Emote value"); - return; - } - - if (!rj::getSafe(emoteJSON, "code", code)) { - Log("No code key found in Emote value"); - return; - } - - id = QString::number(idNumber); - - auto cleanCode = cleanUpCode(code); - emoteSet->emotes.emplace_back(id, cleanCode); - emoteData.emoteCodes.push_back(cleanCode); - - EmoteData emote = this->getEmoteById(id, code); - emoteData.emotes.insert(code, emote); - } - - emoteData.emoteSets.emplace_back(emoteSet); - } - - emoteData.filled = true; - }; - - user->loadEmotes(loadEmotes); + return { + QString(TWITCH_EMOTE_TEMPLATE).replace("{id}", id.string).replace("{scale}", emoteScale)}; } -void TwitchEmotes::loadSetData(std::shared_ptr emoteSet) +AccessGuard> TwitchEmotes::accessAll() { - if (!emoteSet) { - Log("null emote set sent"); - return; - } - - auto staticSetIt = this->staticEmoteSets.find(emoteSet->key); - if (staticSetIt != this->staticEmoteSets.end()) { - const auto &staticSet = staticSetIt->second; - emoteSet->channelName = staticSet.channelName; - emoteSet->text = staticSet.text; - return; - } - - NetworkRequest req("https://braize.pajlada.com/chatterino/twitchemotes/set/" + emoteSet->key + - "/"); - req.setUseQuickLoadCache(true); - - req.onError([](int errorCode) -> bool { - Log("Error code {} while loading emote set data", errorCode); - return true; - }); - - req.onSuccess([emoteSet](auto result) -> bool { - auto root = result.parseRapidJson(); - if (!root.IsObject()) { - return false; - } - - std::string emoteSetID; - QString channelName; - QString type; - if (!rj::getSafe(root, "channel_name", channelName)) { - return false; - } - - if (!rj::getSafe(root, "type", type)) { - return false; - } - - Log("Loaded twitch emote set data for {}!", emoteSet->key); - - if (type == "sub") { - emoteSet->text = QString("Twitch Subscriber Emote (%1)").arg(channelName); - } else { - emoteSet->text = QString("Twitch Account Emote (%1)").arg(channelName); - } - - emoteSet->channelName = channelName; - - return true; - }); - - req.execute(); + return this->twitchEmotes_.access(); } } // namespace chatterino diff --git a/src/providers/twitch/TwitchEmotes.hpp b/src/providers/twitch/TwitchEmotes.hpp index 295fca634..e403ebb43 100644 --- a/src/providers/twitch/TwitchEmotes.hpp +++ b/src/providers/twitch/TwitchEmotes.hpp @@ -1,14 +1,17 @@ #pragma once +#include +#include + #include "common/Emotemap.hpp" +#include "common/UniqueAccess.hpp" +#include "messages/Emote.hpp" #include "providers/twitch/EmoteValue.hpp" #include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchEmotes.hpp" #include "util/ConcurrentMap.hpp" -#include - -#include +#define TWITCH_EMOTE_TEMPLATE "https://static-cdn.jtvnw.net/emoticons/v1/{id}/{scale}" namespace chatterino { @@ -17,55 +20,13 @@ class TwitchEmotes public: TwitchEmotes(); - EmoteData getEmoteById(const QString &id, const QString &emoteName); - - /// Twitch emotes - void refresh(const std::shared_ptr &user); - - struct TwitchEmote { - TwitchEmote(const QString &_id, const QString &_code) - : id(_id) - , code(_code) - { - } - - // i.e. "403921" - QString id; - - // i.e. "forsenE" - QString code; - }; - - struct EmoteSet { - QString key; - QString channelName; - QString text; - std::vector emotes; - }; - - std::map staticEmoteSets; - - struct TwitchAccountEmoteData { - std::vector> emoteSets; - - std::vector emoteCodes; - - EmoteMap emotes; - - bool filled = false; - }; - - // Key is the user ID - std::map emotes; + EmotePtr getOrCreateEmote(const EmoteId &id, const EmoteName &name); + Url getEmoteLink(const EmoteId &id, const QString &emoteScale); + AccessGuard> accessAll(); private: - void loadSetData(std::shared_ptr emoteSet); - - // emote code - ConcurrentMap twitchEmotes_; - - // emote id - ConcurrentMap twitchEmoteFromCache_; + UniqueAccess> twitchEmotes_; + UniqueAccess>> twitchEmotesCache_; }; } // namespace chatterino diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index 4b75ecd5d..f2a4aa7a5 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -16,6 +16,7 @@ #include #include #include +#include namespace chatterino { @@ -83,7 +84,7 @@ MessagePtr TwitchMessageBuilder::build() // PARSING this->parseUsername(); - if (this->userName == this->channel->name) { + if (this->userName == this->channel->getName()) { this->senderIsBroadcaster = true; } @@ -143,14 +144,15 @@ MessagePtr TwitchMessageBuilder::build() // highlights this->parseHighlights(isPastMsg); - QString bits; + // QString bits; auto iterator = this->tags.find("bits"); if (iterator != this->tags.end()) { - bits = iterator.value().toString(); + this->hasBits_ = true; + // bits = iterator.value().toString(); } // twitch emotes - std::vector> twitchEmotes; + std::vector> twitchEmotes; iterator = this->tags.find("emotes"); if (iterator != this->tags.end()) { @@ -164,113 +166,117 @@ MessagePtr TwitchMessageBuilder::build() [](const auto &a, const auto &b) { return a.first < b.first; }); } - auto currentTwitchEmote = twitchEmotes.begin(); - // words - QStringList splits = this->originalMessage_.split(' '); - long int i = 0; + this->addWords(splits, twitchEmotes); - for (QString split : splits) { - MessageColor textColor = - this->action_ ? MessageColor(this->usernameColor_) : MessageColor(MessageColor::Text); + this->message_->searchText = this->userName + ": " + this->originalMessage_; - // twitch emote + return this->getMessage(); +} + +void TwitchMessageBuilder::addWords(const QStringList &words, + const std::vector> &twitchEmotes) +{ + auto i = int(); + auto currentTwitchEmote = twitchEmotes.begin(); + + for (const auto &word : words) { + // check if it's a twitch emote twitch emote if (currentTwitchEmote != twitchEmotes.end() && currentTwitchEmote->first == i) { auto emoteImage = currentTwitchEmote->second; this->emplace(emoteImage, MessageElement::TwitchEmote); - i += split.length() + 1; - currentTwitchEmote = std::next(currentTwitchEmote); + i += word.length() + 1; + currentTwitchEmote++; continue; } // split words - std::vector> parsed; - - // Parse emojis and take all non-emojis and put them in parsed as full text-words - app->emotes->emojis.parse(parsed, split); - - for (const auto &tuple : parsed) { - const EmoteData &emoteData = std::get<0>(tuple); - - if (!emoteData.isValid()) { // is text - QString string = std::get<1>(tuple); - - if (!bits.isEmpty() && this->tryParseCheermote(string)) { - // This string was parsed as a cheermote - continue; - } - - // TODO: Implement ignored emotes - // Format of ignored emotes: - // Emote name: "forsenPuke" - if string in ignoredEmotes - // Will match emote regardless of source (i.e. bttv, ffz) - // Emote source + name: "bttv:nyanPls" - if (this->tryAppendEmote(string)) { - // Successfully appended an emote - continue; - } - - // Actually just text - QString linkString = this->matchLink(string); - - Link link; - - if (linkString.isEmpty()) { - link = Link(); - } else { - if (app->settings->lowercaseLink) { - QRegularExpression httpRegex("\\bhttps?://", - QRegularExpression::CaseInsensitiveOption); - QRegularExpression ftpRegex("\\bftps?://", - QRegularExpression::CaseInsensitiveOption); - QRegularExpression getDomain("\\/\\/([^\\/]*)"); - QString tempString = string; - - if (!string.contains(httpRegex)) { - if (!string.contains(ftpRegex)) { - tempString.insert(0, "http://"); - } - } - QString domain = getDomain.match(tempString).captured(1); - string.replace(domain, domain.toLower()); - } - link = Link(Link::Url, linkString); - textColor = MessageColor(MessageColor::Link); - } - if (string.startsWith('@')) { - this->emplace(string, TextElement::BoldUsername, textColor, - FontStyle::ChatMediumBold) // - ->setLink(link); - this->emplace(string, TextElement::NonBoldUsername, textColor) // - ->setLink(link); - } else { - this->emplace(string, TextElement::Text, textColor) // - ->setLink(link); - } - - } else { // is emoji - this->emplace(emoteData, EmoteElement::EmojiAll); - } + for (auto &variant : getApp()->emotes->emojis.parse(word)) { + // if (std::holds_alternative(variant)) { + // this->addTextOrEmoji(std::get<1>(variant)); + // } else { + // // this->addTextOrEmoji(std::get(variant)); + // } + boost::apply_visitor(/*overloaded{[&](EmotePtr arg) { this->addTextOrEmoji(arg); }, + [&](const QString &arg) { this->addTextOrEmoji(arg); }}*/ + [&](auto &&arg) { this->addTextOrEmoji(arg); }, variant); } - for (int j = 0; j < split.size(); j++) { + for (int j = 0; j < word.size(); j++) { i++; - if (split.at(j).isHighSurrogate()) { + if (word.at(j).isHighSurrogate()) { j++; } } i++; } +} - this->message_->searchText = this->userName + ": " + this->originalMessage_; +void TwitchMessageBuilder::addTextOrEmoji(EmotePtr emote) +{ + this->emplace(emote, EmoteElement::EmojiAll); +} - return this->getMessage(); +void TwitchMessageBuilder::addTextOrEmoji(const QString &string_) +{ + auto string = QString(string_); + + if (this->hasBits_ && this->tryParseCheermote(string)) { + // This string was parsed as a cheermote + return; + } + + // TODO: Implement ignored emotes + // Format of ignored emotes: + // Emote name: "forsenPuke" - if string in ignoredEmotes + // Will match emote regardless of source (i.e. bttv, ffz) + // Emote source + name: "bttv:nyanPls" + if (this->tryAppendEmote({string})) { + // Successfully appended an emote + return; + } + + // Actually just text + auto linkString = this->matchLink(string); + auto link = Link(); + auto textColor = + this->action_ ? MessageColor(this->usernameColor_) : MessageColor(MessageColor::Text); + + if (!linkString.isEmpty()) { + if (getApp()->settings->lowercaseLink) { + QRegularExpression httpRegex("\\bhttps?://", QRegularExpression::CaseInsensitiveOption); + QRegularExpression ftpRegex("\\bftps?://", QRegularExpression::CaseInsensitiveOption); + QRegularExpression getDomain("\\/\\/([^\\/]*)"); + QString tempString = string; + + if (!string.contains(httpRegex)) { + if (!string.contains(ftpRegex)) { + tempString.insert(0, "http://"); + } + } + QString domain = getDomain.match(tempString).captured(1); + string.replace(domain, domain.toLower()); + } + link = Link(Link::Url, linkString); + textColor = MessageColor(MessageColor::Link); + } + if (string.startsWith('@')) { + this->emplace(string, TextElement::BoldUsername, textColor, + FontStyle::ChatMediumBold) // + ->setLink(link); + this->emplace(string, TextElement::NonBoldUsername, + textColor) // + ->setLink(link); + } else { + this->emplace(string, TextElement::Text, textColor) // + ->setLink(link); + } } void TwitchMessageBuilder::parseMessageID() @@ -301,8 +307,8 @@ void TwitchMessageBuilder::parseRoomID() void TwitchMessageBuilder::appendChannelName() { - QString channelName("#" + this->channel->name); - Link link(Link::Url, this->channel->name + "\n" + this->messageID); + QString channelName("#" + this->channel->getName()); + Link link(Link::Url, this->channel->getName() + "\n" + this->messageID); this->emplace(channelName, MessageElement::ChannelName, MessageColor::System) // ->setLink(link); @@ -531,71 +537,64 @@ void TwitchMessageBuilder::parseHighlights(bool isPastMsg) void TwitchMessageBuilder::appendTwitchEmote(const Communi::IrcMessage *ircMessage, const QString &emote, - std::vector> &vec) + std::vector> &vec) { auto app = getApp(); if (!emote.contains(':')) { return; } - QStringList parameters = emote.split(':'); + auto parameters = emote.split(':'); if (parameters.length() < 2) { return; } - const auto &id = parameters.at(0); + auto id = EmoteId{parameters.at(0)}; - QStringList occurences = parameters.at(1).split(','); + auto occurences = parameters.at(1).split(','); for (QString occurence : occurences) { - QStringList coords = occurence.split('-'); + auto coords = occurence.split('-'); if (coords.length() < 2) { return; } - int start = coords.at(0).toInt(); - int end = coords.at(1).toInt(); + auto start = coords.at(0).toInt(); + auto end = coords.at(1).toInt(); if (start >= end || start < 0 || end > this->originalMessage_.length()) { return; } - QString name = this->originalMessage_.mid(start, end - start + 1); + auto name = EmoteName{this->originalMessage_.mid(start, end - start + 1)}; - vec.push_back( - std::pair(start, app->emotes->twitch.getEmoteById(id, name))); + vec.push_back(std::make_pair(start, app->emotes->twitch.getOrCreateEmote(id, name))); } } -bool TwitchMessageBuilder::tryAppendEmote(QString &emoteString) +Outcome TwitchMessageBuilder::tryAppendEmote(const EmoteName &name) { - auto app = getApp(); - EmoteData emoteData; + auto flags = MessageElement::Flags::None; + auto emote = boost::optional{}; - auto appendEmote = [&](MessageElement::Flags flags) { - this->emplace(emoteData, flags); - return true; - }; - - if (app->emotes->bttv.globalEmotes.tryGet(emoteString, emoteData)) { - // BTTV Global Emote - return appendEmote(MessageElement::BttvEmote); - } else if (this->twitchChannel != nullptr && - this->twitchChannel->getBttvEmotes().tryGet(emoteString, emoteData)) { - // BTTV Channel Emote - return appendEmote(MessageElement::BttvEmote); - } else if (app->emotes->ffz.globalEmotes.tryGet(emoteString, emoteData)) { - // FFZ Global Emote - return appendEmote(MessageElement::FfzEmote); - } else if (this->twitchChannel != nullptr && - this->twitchChannel->getFfzEmotes().tryGet(emoteString, emoteData)) { - // FFZ Channel Emote - return appendEmote(MessageElement::FfzEmote); + if ((emote = getApp()->emotes->bttv.getGlobalEmote(name))) { + flags = MessageElement::BttvEmote; + } else if (twitchChannel && (emote = this->twitchChannel->getBttvEmote(name))) { + flags = MessageElement::BttvEmote; + } else if ((emote = getApp()->emotes->ffz.getGlobalEmote(name))) { + flags = MessageElement::FfzEmote; + } else if (twitchChannel && (emote = this->twitchChannel->getFfzEmote(name))) { + flags = MessageElement::FfzEmote; } - return false; + if (emote) { + this->emplace(emote.get(), flags); + return Success; + } + + return Failure; } // fourtf: this is ugly @@ -604,8 +603,6 @@ void TwitchMessageBuilder::appendTwitchBadges() { auto app = getApp(); - const auto &channelResources = app->resources->channels[this->roomID_]; - auto iterator = this->tags.find("badges"); if (iterator == this->tags.end()) { @@ -621,68 +618,75 @@ void TwitchMessageBuilder::appendTwitchBadges() } if (badge.startsWith("bits/")) { - if (!app->resources->dynamicBadgesLoaded) { - // Do nothing - continue; - } + // if (!app->resources->dynamicBadgesLoaded) { + // // Do nothing + // continue; + // } - QString cheerAmountQS = badge.mid(5); - std::string versionKey = cheerAmountQS.toStdString(); - QString tooltip = QString("Twitch cheer ") + cheerAmountQS; + QString cheerAmount = badge.mid(5); + QString tooltip = QString("Twitch cheer ") + cheerAmount; // Try to fetch channel-specific bit badge try { - const auto &badge = channelResources.badgeSets.at("bits").versions.at(versionKey); - this->emplace(badge.badgeImage1x, MessageElement::BadgeVanity) - ->setTooltip(tooltip); - continue; + if (twitchChannel) + if (const auto &badge = + this->twitchChannel->getTwitchBadge("bits", cheerAmount)) { + this->emplace(badge.get(), MessageElement::BadgeVanity) + ->setTooltip(tooltip); + continue; + } } catch (const std::out_of_range &) { // Channel does not contain a special bit badge for this version } // Use default bit badge - try { - const auto &badge = app->resources->badgeSets.at("bits").versions.at(versionKey); - this->emplace(badge.badgeImage1x, MessageElement::BadgeVanity) - ->setTooltip(tooltip); - } catch (const std::out_of_range &) { - Log("No default bit badge for version {} found", versionKey); - continue; - } + // try { + // const auto &badge = app->resources->badgeSets.at("bits").versions.at(cheerAmount); + // this->emplace(badge.badgeImage1x, MessageElement::BadgeVanity) + // ->setTooltip(tooltip); + //} catch (const std::out_of_range &) { + // Log("No default bit badge for version {} found", cheerAmount); + // continue; + //} } else if (badge == "staff/1") { - this->emplace(app->resources->badgeStaff, + this->emplace(Image::fromNonOwningPixmap(&app->resources->twitch.staff), MessageElement::BadgeGlobalAuthority) ->setTooltip("Twitch Staff"); } else if (badge == "admin/1") { - this->emplace(app->resources->badgeAdmin, + this->emplace(Image::fromNonOwningPixmap(&app->resources->twitch.admin), MessageElement::BadgeGlobalAuthority) ->setTooltip("Twitch Admin"); } else if (badge == "global_mod/1") { - this->emplace(app->resources->badgeGlobalModerator, - MessageElement::BadgeGlobalAuthority) + this->emplace( + Image::fromNonOwningPixmap(&app->resources->twitch.globalmod), + MessageElement::BadgeGlobalAuthority) ->setTooltip("Twitch Global Moderator"); } else if (badge == "moderator/1") { // TODO: Implement custom FFZ moderator badge - this->emplace(app->resources->badgeModerator, - MessageElement::BadgeChannelAuthority) + this->emplace( + Image::fromNonOwningPixmap(&app->resources->twitch.moderator), + MessageElement::BadgeChannelAuthority) ->setTooltip("Twitch Channel Moderator"); } else if (badge == "turbo/1") { - this->emplace(app->resources->badgeTurbo, + this->emplace(Image::fromNonOwningPixmap(&app->resources->twitch.turbo), MessageElement::BadgeGlobalAuthority) ->setTooltip("Twitch Turbo Subscriber"); } else if (badge == "broadcaster/1") { - this->emplace(app->resources->badgeBroadcaster, - MessageElement::BadgeChannelAuthority) + this->emplace( + Image::fromNonOwningPixmap(&app->resources->twitch.broadcaster), + MessageElement::BadgeChannelAuthority) ->setTooltip("Twitch Broadcaster"); } else if (badge == "premium/1") { - this->emplace(app->resources->badgePremium, MessageElement::BadgeVanity) + this->emplace(Image::fromNonOwningPixmap(&app->resources->twitch.prime), + MessageElement::BadgeVanity) ->setTooltip("Twitch Prime Subscriber"); } else if (badge.startsWith("partner/")) { int index = badge.midRef(8).toInt(); switch (index) { case 1: { - this->emplace(app->resources->badgeVerified, - MessageElement::BadgeVanity) + this->emplace( + Image::fromNonOwningPixmap(&app->resources->twitch.verified), + MessageElement::BadgeVanity) ->setTooltip("Twitch Verified"); } break; default: { @@ -690,140 +694,142 @@ void TwitchMessageBuilder::appendTwitchBadges() } break; } } else if (badge.startsWith("subscriber/")) { - if (channelResources.loaded == false) { - // qDebug() << "Channel resources are not loaded, can't add the subscriber badge"; - continue; - } + // if (channelResources.loaded == false) { + // // qDebug() << "Channel resources are not loaded, can't add the + // subscriber + // // badge"; + // continue; + // } - auto badgeSetIt = channelResources.badgeSets.find("subscriber"); - if (badgeSetIt == channelResources.badgeSets.end()) { - // Fall back to default badge - this->emplace(app->resources->badgeSubscriber, - MessageElement::BadgeSubscription) - ->setTooltip("Twitch Subscriber"); - continue; - } + // auto badgeSetIt = channelResources.badgeSets.find("subscriber"); + // if (badgeSetIt == channelResources.badgeSets.end()) { + // // Fall back to default badge + // this->emplace(app->resources->badgeSubscriber, + // MessageElement::BadgeSubscription) + // ->setTooltip("Twitch Subscriber"); + // continue; + //} - const auto &badgeSet = badgeSetIt->second; + // const auto &badgeSet = badgeSetIt->second; - std::string versionKey = badge.mid(11).toStdString(); + // std::string versionKey = badge.mid(11).toStdString(); - auto badgeVersionIt = badgeSet.versions.find(versionKey); + // auto badgeVersionIt = badgeSet.versions.find(versionKey); - if (badgeVersionIt == badgeSet.versions.end()) { - // Fall back to default badge - this->emplace(app->resources->badgeSubscriber, - MessageElement::BadgeSubscription) - ->setTooltip("Twitch Subscriber"); - continue; - } + // if (badgeVersionIt == badgeSet.versions.end()) { + // // Fall back to default badge + // this->emplace(app->resources->badgeSubscriber, + // MessageElement::BadgeSubscription) + // ->setTooltip("Twitch Subscriber"); + // continue; + //} - auto &badgeVersion = badgeVersionIt->second; + // auto &badgeVersion = badgeVersionIt->second; - this->emplace(badgeVersion.badgeImage1x, - MessageElement::BadgeSubscription) - ->setTooltip("Twitch " + QString::fromStdString(badgeVersion.title)); + // this->emplace(badgeVersion.badgeImage1x, + // MessageElement::BadgeSubscription) + // ->setTooltip("Twitch " + QString::fromStdString(badgeVersion.title)); } else { - if (!app->resources->dynamicBadgesLoaded) { - // Do nothing - continue; - } + // if (!app->resources->dynamicBadgesLoaded) { + // // Do nothing + // continue; + //} - QStringList parts = badge.split('/'); + // QStringList parts = badge.split('/'); - if (parts.length() != 2) { - qDebug() << "Bad number of parts: " << parts.length() << " in " << parts; - continue; - } + // if (parts.length() != 2) { + // qDebug() << "Bad number of parts: " << parts.length() << " in " << parts; + // continue; + //} - MessageElement::Flags badgeType = MessageElement::Flags::BadgeVanity; + // MessageElement::Flags badgeType = MessageElement::Flags::BadgeVanity; - std::string badgeSetKey = parts[0].toStdString(); - std::string versionKey = parts[1].toStdString(); + // std::string badgeSetKey = parts[0].toStdString(); + // std::string versionKey = parts[1].toStdString(); - try { - auto &badgeSet = app->resources->badgeSets.at(badgeSetKey); + // try { + // auto &badgeSet = app->resources->badgeSets.at(badgeSetKey); - try { - auto &badgeVersion = badgeSet.versions.at(versionKey); + // try { + // auto &badgeVersion = badgeSet.versions.at(versionKey); - this->emplace(badgeVersion.badgeImage1x, badgeType) - ->setTooltip("Twitch " + QString::fromStdString(badgeVersion.title)); - } catch (const std::exception &e) { - qDebug() << "Exception caught:" << e.what() - << "when trying to fetch badge version " << versionKey.c_str(); - } - } catch (const std::exception &e) { - qDebug() << "No badge set with key" << badgeSetKey.c_str() - << ". Exception: " << e.what(); - } + // this->emplace(badgeVersion.badgeImage1x, badgeType) + // ->setTooltip("Twitch " + QString::fromStdString(badgeVersion.title)); + // } catch (const std::exception &e) { + // qDebug() << "Exception caught:" << e.what() + // << "when trying to fetch badge version " << versionKey.c_str(); + // } + //} catch (const std::exception &e) { + // qDebug() << "No badge set with key" << badgeSetKey.c_str() + // << ". Exception: " << e.what(); + //} } } } void TwitchMessageBuilder::appendChatterinoBadges() { - auto app = getApp(); + // auto app = getApp(); - auto &badges = app->resources->chatterinoBadges; - auto it = badges.find(this->userName.toStdString()); + // auto &badges = app->resources->chatterinoBadges; + // auto it = badges.find(this->userName.toStdString()); - if (it == badges.end()) { - return; - } + // if (it == badges.end()) { + // return; + // } - const auto badge = it->second; + // const auto badge = it->second; - this->emplace(badge->image, MessageElement::BadgeChatterino) - ->setTooltip(QString::fromStdString(badge->tooltip)); + // this->emplace(badge->image, MessageElement::BadgeChatterino) + // ->setTooltip(QString::fromStdString(badge->tooltip)); } -bool TwitchMessageBuilder::tryParseCheermote(const QString &string) +Outcome TwitchMessageBuilder::tryParseCheermote(const QString &string) { - auto app = getApp(); - // Try to parse custom cheermotes - const auto &channelResources = app->resources->channels[this->roomID_]; - if (channelResources.loaded) { - for (const auto &cheermoteSet : channelResources.cheermoteSets) { - auto match = cheermoteSet.regex.match(string); - if (!match.hasMatch()) { - continue; - } - QString amount = match.captured(1); - bool ok = false; - int numBits = amount.toInt(&ok); - if (!ok) { - Log("Error parsing bit amount in tryParseCheermote"); - return false; - } + // auto app = getApp(); + //// Try to parse custom cheermotes + // const auto &channelResources = app->resources->channels[this->roomID_]; + // if (channelResources.loaded) { + // for (const auto &cheermoteSet : channelResources.cheermoteSets) { + // auto match = cheermoteSet.regex.match(string); + // if (!match.hasMatch()) { + // continue; + // } + // QString amount = match.captured(1); + // bool ok = false; + // int numBits = amount.toInt(&ok); + // if (!ok) { + // Log("Error parsing bit amount in tryParseCheermote"); + // return Failure; + // } - auto savedIt = cheermoteSet.cheermotes.end(); + // auto savedIt = cheermoteSet.cheermotes.end(); - // Fetch cheermote that matches our numBits - for (auto it = cheermoteSet.cheermotes.begin(); it != cheermoteSet.cheermotes.end(); - ++it) { - if (numBits >= it->minBits) { - savedIt = it; - } else { - break; - } - } + // // Fetch cheermote that matches our numBits + // for (auto it = cheermoteSet.cheermotes.begin(); it != cheermoteSet.cheermotes.end(); + // ++it) { + // if (numBits >= it->minBits) { + // savedIt = it; + // } else { + // break; + // } + // } - if (savedIt == cheermoteSet.cheermotes.end()) { - Log("Error getting a cheermote from a cheermote set for the bit amount {}", - numBits); - return false; - } + // if (savedIt == cheermoteSet.cheermotes.end()) { + // Log("Error getting a cheermote from a cheermote set for the bit amount {}", + // numBits); + // return Failure; + // } - const auto &cheermote = *savedIt; + // const auto &cheermote = *savedIt; - this->emplace(cheermote.emoteDataAnimated, EmoteElement::BitsAnimated); - this->emplace(amount, EmoteElement::Text, cheermote.color); + // this->emplace(cheermote.animatedEmote, EmoteElement::BitsAnimated); + // this->emplace(amount, EmoteElement::Text, cheermote.color); - return true; - } - } + // return Success; + // } + //} - return false; + return Failure; } } // namespace chatterino diff --git a/src/providers/twitch/TwitchMessageBuilder.hpp b/src/providers/twitch/TwitchMessageBuilder.hpp index b27ce8a0a..1bf538f35 100644 --- a/src/providers/twitch/TwitchMessageBuilder.hpp +++ b/src/providers/twitch/TwitchMessageBuilder.hpp @@ -51,14 +51,20 @@ private: void parseHighlights(bool isPastMsg); void appendTwitchEmote(const Communi::IrcMessage *ircMessage, const QString &emote, - std::vector> &vec); - bool tryAppendEmote(QString &emoteString); + std::vector> &vec); + Outcome tryAppendEmote(const EmoteName &name); + + void addWords(const QStringList &words, + const std::vector> &twitchEmotes); + void addTextOrEmoji(EmotePtr emote); + void addTextOrEmoji(const QString &value); void appendTwitchBadges(); void appendChatterinoBadges(); - bool tryParseCheermote(const QString &string); + Outcome tryParseCheermote(const QString &string); QString roomID_; + bool hasBits_ = false; QColor usernameColor_; const QString originalMessage_; diff --git a/src/providers/twitch/TwitchParseCheerEmotes.cpp b/src/providers/twitch/TwitchParseCheerEmotes.cpp new file mode 100644 index 000000000..a7193d2f6 --- /dev/null +++ b/src/providers/twitch/TwitchParseCheerEmotes.cpp @@ -0,0 +1,264 @@ +#include "TwitchParseCheerEmotes.hpp" + +#include +#include +#include + +namespace chatterino { + +namespace { + +template +inline bool ReadValue(const rapidjson::Value &object, const char *key, Type &out) +{ + if (!object.HasMember(key)) { + return false; + } + + const auto &value = object[key]; + + if (!value.Is()) { + return false; + } + + out = value.Get(); + + return true; +} + +template <> +inline bool ReadValue(const rapidjson::Value &object, const char *key, QString &out) +{ + if (!object.HasMember(key)) { + return false; + } + + const auto &value = object[key]; + + if (!value.IsString()) { + return false; + } + + out = value.GetString(); + + return true; +} + +template <> +inline bool ReadValue>(const rapidjson::Value &object, const char *key, + std::vector &out) +{ + if (!object.HasMember(key)) { + return false; + } + + const auto &value = object[key]; + + if (!value.IsArray()) { + return false; + } + + for (const rapidjson::Value &innerValue : value.GetArray()) { + if (!innerValue.IsString()) { + return false; + } + + out.emplace_back(innerValue.GetString()); + } + + return true; +} + +// Parse a single cheermote set (or "action") from the twitch api +inline bool ParseSingleCheermoteSet(JSONCheermoteSet &set, const rapidjson::Value &action) +{ + if (!action.IsObject()) { + return false; + } + + if (!ReadValue(action, "prefix", set.prefix)) { + return false; + } + + if (!ReadValue(action, "scales", set.scales)) { + return false; + } + + if (!ReadValue(action, "backgrounds", set.backgrounds)) { + return false; + } + + if (!ReadValue(action, "states", set.states)) { + return false; + } + + if (!ReadValue(action, "type", set.type)) { + return false; + } + + if (!ReadValue(action, "updated_at", set.updatedAt)) { + return false; + } + + if (!ReadValue(action, "priority", set.priority)) { + return false; + } + + // Tiers + if (!action.HasMember("tiers")) { + return false; + } + + const auto &tiersValue = action["tiers"]; + + if (!tiersValue.IsArray()) { + return false; + } + + for (const rapidjson::Value &tierValue : tiersValue.GetArray()) { + JSONCheermoteSet::CheermoteTier tier; + + if (!tierValue.IsObject()) { + return false; + } + + if (!ReadValue(tierValue, "min_bits", tier.minBits)) { + return false; + } + + if (!ReadValue(tierValue, "id", tier.id)) { + return false; + } + + if (!ReadValue(tierValue, "color", tier.color)) { + return false; + } + + // Images + if (!tierValue.HasMember("images")) { + return false; + } + + const auto &imagesValue = tierValue["images"]; + + if (!imagesValue.IsObject()) { + return false; + } + + // Read images object + for (const auto &imageBackgroundValue : imagesValue.GetObject()) { + QString background = imageBackgroundValue.name.GetString(); + bool backgroundExists = false; + for (const auto &bg : set.backgrounds) { + if (background == bg) { + backgroundExists = true; + break; + } + } + + if (!backgroundExists) { + continue; + } + + const rapidjson::Value &imageBackgroundStates = imageBackgroundValue.value; + if (!imageBackgroundStates.IsObject()) { + continue; + } + + // Read each key which represents a background + for (const auto &imageBackgroundState : imageBackgroundStates.GetObject()) { + QString state = imageBackgroundState.name.GetString(); + bool stateExists = false; + for (const auto &_state : set.states) { + if (state == _state) { + stateExists = true; + break; + } + } + + if (!stateExists) { + continue; + } + + const rapidjson::Value &imageScalesValue = imageBackgroundState.value; + if (!imageScalesValue.IsObject()) { + continue; + } + + // Read each key which represents a scale + for (const auto &imageScaleValue : imageScalesValue.GetObject()) { + QString scale = imageScaleValue.name.GetString(); + bool scaleExists = false; + for (const auto &_scale : set.scales) { + if (scale == _scale) { + scaleExists = true; + break; + } + } + + if (!scaleExists) { + continue; + } + + const rapidjson::Value &imageScaleURLValue = imageScaleValue.value; + if (!imageScaleURLValue.IsString()) { + continue; + } + + QString url = imageScaleURLValue.GetString(); + + bool ok = false; + qreal scaleNumber = scale.toFloat(&ok); + if (!ok) { + continue; + } + + qreal chatterinoScale = 1 / scaleNumber; + + auto image = Image::fromUrl({url}, chatterinoScale); + + // TODO(pajlada): Fill in name and tooltip + tier.images[background][state][scale] = image; + } + } + } + + set.tiers.emplace_back(tier); + } + + return true; +} +} // namespace + +// Look through the results of https://api.twitch.tv/kraken/bits/actions?channel_id=11148817 for +// cheermote sets or "Actions" as they are called in the API +std::vector ParseCheermoteSets(const rapidjson::Document &d) +{ + std::vector sets; + + if (!d.IsObject()) { + return sets; + } + + if (!d.HasMember("actions")) { + return sets; + } + + const auto &actionsValue = d["actions"]; + + if (!actionsValue.IsArray()) { + return sets; + } + + for (const auto &action : actionsValue.GetArray()) { + JSONCheermoteSet set; + bool res = ParseSingleCheermoteSet(set, action); + + if (res) { + sets.emplace_back(set); + } + } + + return sets; +} +} // namespace chatterino diff --git a/src/providers/twitch/TwitchParseCheerEmotes.hpp b/src/providers/twitch/TwitchParseCheerEmotes.hpp new file mode 100644 index 000000000..150c8b447 --- /dev/null +++ b/src/providers/twitch/TwitchParseCheerEmotes.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include +#include +#include "messages/Image.hpp" + +namespace chatterino { + +struct JSONCheermoteSet { + QString prefix; + std::vector scales; + + std::vector backgrounds; + std::vector states; + + QString type; + QString updatedAt; + int priority; + + struct CheermoteTier { + int minBits; + QString id; + QString color; + + // Background State Scale + std::map>> images; + }; + + std::vector tiers; +}; + +std::vector ParseCheermoteSets(const rapidjson::Document &d); + +} // namespace chatterino diff --git a/src/providers/twitch/TwitchServer.cpp b/src/providers/twitch/TwitchServer.cpp index 8649a955f..14542afcf 100644 --- a/src/providers/twitch/TwitchServer.cpp +++ b/src/providers/twitch/TwitchServer.cpp @@ -29,22 +29,18 @@ TwitchServer::TwitchServer() this->pubsub = new PubSub; - getSettings()->twitchSeperateWriteConnection.connect([this](auto, auto) { this->connect(); }, - this->signalHolder_, false); + // getSettings()->twitchSeperateWriteConnection.connect([this](auto, auto) { this->connect(); }, + // this->signalHolder_, false); } -void TwitchServer::initialize(Application &app) +void TwitchServer::initialize(Settings &settings, Paths &paths) { - this->app = &app; - - app.accounts->twitch.currentUserChanged.connect( + getApp()->accounts->twitch.currentUserChanged.connect( [this]() { postToThread([this] { this->connect(); }); }); } void TwitchServer::initializeConnection(IrcConnection *connection, bool isRead, bool isWrite) { - assert(this->app); - this->singleConnection_ = isRead == isWrite; std::shared_ptr account = getApp()->accounts->twitch.getCurrent(); @@ -236,7 +232,7 @@ void TwitchServer::onMessageSendRequested(TwitchChannel *channel, const QString lastMessage.push(now); } - this->sendMessage(channel->name, message); + this->sendMessage(channel->getName(), message); sent = true; } diff --git a/src/providers/twitch/TwitchServer.hpp b/src/providers/twitch/TwitchServer.hpp index 97f0859fe..769f65fd4 100644 --- a/src/providers/twitch/TwitchServer.hpp +++ b/src/providers/twitch/TwitchServer.hpp @@ -12,15 +12,18 @@ namespace chatterino { +class Settings; +class Paths; + class PubSub; -class TwitchServer : public AbstractIrcServer, public Singleton +class TwitchServer final : public AbstractIrcServer, public Singleton { public: TwitchServer(); virtual ~TwitchServer() override = default; - virtual void initialize(Application &app) override; + virtual void initialize(Settings &settings, Paths &paths) override; void forEachChannelAndSpecialChannels(std::function func); @@ -51,8 +54,6 @@ protected: private: void onMessageSendRequested(TwitchChannel *channel, const QString &message, bool &sent); - Application *app = nullptr; - std::mutex lastMessageMutex_; std::queue lastMessagePleb_; std::queue lastMessageMod_; diff --git a/src/singletons/Badges.cpp b/src/singletons/Badges.cpp new file mode 100644 index 000000000..da38def4f --- /dev/null +++ b/src/singletons/Badges.cpp @@ -0,0 +1,9 @@ +#include "Badges.hpp" + +namespace chatterino { + +Badges::Badges() +{ +} + +} // namespace chatterino diff --git a/src/singletons/Badges.hpp b/src/singletons/Badges.hpp new file mode 100644 index 000000000..d049529eb --- /dev/null +++ b/src/singletons/Badges.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include "common/Singleton.hpp" +#include "messages/Emote.hpp" + +namespace chatterino { + +class Badges : public Singleton +{ +public: + Badges(); +}; + +} // namespace chatterino diff --git a/src/singletons/Emotes.cpp b/src/singletons/Emotes.cpp index ca4e91677..0be870a5e 100644 --- a/src/singletons/Emotes.cpp +++ b/src/singletons/Emotes.cpp @@ -5,15 +5,14 @@ namespace chatterino { -void Emotes::initialize(Application &app) +Emotes::Emotes() { - const auto refreshTwitchEmotes = [this, &app] { - auto currentUser = app.accounts->twitch.getCurrent(); - assert(currentUser); - this->twitch.refresh(currentUser); - }; - app.accounts->twitch.currentUserChanged.connect(refreshTwitchEmotes); - refreshTwitchEmotes(); +} + +void Emotes::initialize(Settings &settings, Paths &paths) +{ + getApp()->accounts->twitch.currentUserChanged.connect( + [] { getApp()->accounts->twitch.getCurrent()->loadEmotes(); }); this->emojis.load(); this->bttv.loadGlobalEmotes(); diff --git a/src/singletons/Emotes.hpp b/src/singletons/Emotes.hpp index 279992435..e991fe674 100644 --- a/src/singletons/Emotes.hpp +++ b/src/singletons/Emotes.hpp @@ -12,16 +12,21 @@ namespace chatterino { +class Settings; +class Paths; + class Emotes final : public Singleton { public: - virtual void initialize(Application &app) override; + Emotes(); + + virtual void initialize(Settings &settings, Paths &paths) override; bool isIgnoredEmote(const QString &emote); TwitchEmotes twitch; - BTTVEmotes bttv; - FFZEmotes ffz; + BttvEmotes bttv; + FfzEmotes ffz; Emojis emojis; GIFTimer gifTimer; diff --git a/src/singletons/Fonts.cpp b/src/singletons/Fonts.cpp index 70aaa93b6..eb4af2877 100644 --- a/src/singletons/Fonts.cpp +++ b/src/singletons/Fonts.cpp @@ -29,28 +29,20 @@ Fonts::Fonts() this->fontsByType_.resize(size_t(EndType)); } -void Fonts::initialize(Application &app) +void Fonts::initialize(Settings &, Paths &) { - this->chatFontFamily.connect([this, &app](const std::string &, auto) { + this->chatFontFamily.connect([this](const std::string &, auto) { assertInGuiThread(); - if (app.windows) { - app.windows->incGeneration(); - } - for (auto &map : this->fontsByType_) { map.clear(); } this->fontChanged.invoke(); }); - this->chatFontSize.connect([this, &app](const int &, auto) { + this->chatFontSize.connect([this](const int &, auto) { assertInGuiThread(); - if (app.windows) { - app.windows->incGeneration(); - } - for (auto &map : this->fontsByType_) { map.clear(); } diff --git a/src/singletons/Fonts.hpp b/src/singletons/Fonts.hpp index 5a7192292..59c6550c3 100644 --- a/src/singletons/Fonts.hpp +++ b/src/singletons/Fonts.hpp @@ -13,12 +13,15 @@ namespace chatterino { +class Settings; +class Paths; + class Fonts final : public Singleton { public: Fonts(); - virtual void initialize(Application &app) override; + virtual void initialize(Settings &settings, Paths &paths) override; // font data gets set in createFontData(...) enum Type : uint8_t { diff --git a/src/singletons/Logging.cpp b/src/singletons/Logging.cpp index 711e64a9e..a8a6495c7 100644 --- a/src/singletons/Logging.cpp +++ b/src/singletons/Logging.cpp @@ -12,7 +12,7 @@ namespace chatterino { -void Logging::initialize(Application &app) +void Logging::initialize(Settings &settings, Paths &paths) { } diff --git a/src/singletons/Logging.hpp b/src/singletons/Logging.hpp index f23d8c0e6..98c6c8353 100644 --- a/src/singletons/Logging.hpp +++ b/src/singletons/Logging.hpp @@ -18,7 +18,7 @@ class Logging : public Singleton public: Logging() = default; - virtual void initialize(Application &app) override; + virtual void initialize(Settings &settings, Paths &paths) override; void addMessage(const QString &channelName, MessagePtr message); diff --git a/src/singletons/NativeMessaging.cpp b/src/singletons/NativeMessaging.cpp index 805f0f384..3bf5b0ff3 100644 --- a/src/singletons/NativeMessaging.cpp +++ b/src/singletons/NativeMessaging.cpp @@ -31,29 +31,12 @@ namespace ipc = boost::interprocess; namespace chatterino { -// fourtf: don't add this class to the application class -NativeMessaging::NativeMessaging() -{ - qDebug() << "init NativeMessagingManager"; -} +void registerNmManifest(Paths &paths, const QString &manifestFilename, + const QString ®istryKeyName, const QJsonDocument &document); -void NativeMessaging::writeByteArray(QByteArray a) +void registerNmHost(Paths &paths) { - char *data = a.data(); - uint32_t size; - size = a.size(); - std::cout.write(reinterpret_cast(&size), 4); - std::cout.write(data, a.size()); - std::cout.flush(); -} - -void NativeMessaging::registerHost() -{ - auto app = getApp(); - - if (app->paths->isPortable()) { - return; - } + if (paths.isPortable()) return; auto getBaseDocument = [&] { QJsonObject obj; @@ -65,22 +48,6 @@ void NativeMessaging::registerHost() return obj; }; - auto registerManifest = [&](const QString &manifestFilename, const QString ®istryKeyName, - const QJsonDocument &document) { - // save the manifest - QString manifestPath = app->paths->miscDirectory + manifestFilename; - QFile file(manifestPath); - file.open(QIODevice::WriteOnly | QIODevice::Truncate); - file.write(document.toJson()); - file.flush(); - -#ifdef Q_OS_WIN - // clang-format off - QProcess::execute("REG ADD \"" + registryKeyName + "\" /ve /t REG_SZ /d \"" + manifestPath + "\" /f"); -// clang-format on -#endif - }; - // chrome { QJsonDocument document; @@ -90,8 +57,8 @@ void NativeMessaging::registerHost() obj.insert("allowed_origins", allowed_origins_arr); document.setObject(obj); - registerManifest( - "/native-messaging-manifest-chrome.json", + registerNmManifest( + paths, "/native-messaging-manifest-chrome.json", "HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\com.chatterino.chatterino", document); } @@ -105,24 +72,40 @@ void NativeMessaging::registerHost() obj.insert("allowed_extensions", allowed_extensions); document.setObject(obj); - registerManifest("/native-messaging-manifest-firefox.json", - "HKCU\\Software\\Mozilla\\NativeMessagingHosts\\com.chatterino.chatterino", - document); + registerNmManifest( + paths, "/native-messaging-manifest-firefox.json", + "HKCU\\Software\\Mozilla\\NativeMessagingHosts\\com.chatterino.chatterino", document); } } -void NativeMessaging::openGuiMessageQueue() +void registerNmManifest(Paths &paths, const QString &manifestFilename, + const QString ®istryKeyName, const QJsonDocument &document) { - static ReceiverThread thread; + (void)registryKeyName; - if (thread.isRunning()) { - thread.exit(); - } + // save the manifest + QString manifestPath = paths.miscDirectory + manifestFilename; + QFile file(manifestPath); + file.open(QIODevice::WriteOnly | QIODevice::Truncate); + file.write(document.toJson()); + file.flush(); - thread.start(); +#ifdef Q_OS_WIN + // clang-format off + QProcess::execute("REG ADD \"" + registryKeyName + "\" /ve /t REG_SZ /d \"" + manifestPath + "\" /f"); +// clang-format on +#endif } -void NativeMessaging::sendToGuiProcess(const QByteArray &array) +std::string &getNmQueueName(Paths &paths) +{ + static std::string name = "chatterino_gui" + paths.applicationFilePathHash.toStdString(); + return name; +} + +// CLIENT + +void NativeMessagingClient::sendMessage(const QByteArray &array) { try { ipc::message_queue messageQueue(ipc::open_only, "chatterino_gui"); @@ -133,7 +116,24 @@ void NativeMessaging::sendToGuiProcess(const QByteArray &array) } } -void NativeMessaging::ReceiverThread::run() +void NativeMessagingClient::writeToCout(const QByteArray &array) +{ + auto *data = array.data(); + auto size = uint32_t(array.size()); + + std::cout.write(reinterpret_cast(&size), 4); + std::cout.write(data, size); + std::cout.flush(); +} + +// SERVER + +void NativeMessagingServer::start() +{ + this->thread.start(); +} + +void NativeMessagingServer::ReceiverThread::run() { ipc::message_queue::remove("chatterino_gui"); @@ -157,7 +157,7 @@ void NativeMessaging::ReceiverThread::run() } } -void NativeMessaging::ReceiverThread::handleMessage(const QJsonObject &root) +void NativeMessagingServer::ReceiverThread::handleMessage(const QJsonObject &root) { auto app = getApp(); @@ -231,11 +231,4 @@ void NativeMessaging::ReceiverThread::handleMessage(const QJsonObject &root) } } -std::string &NativeMessaging::getGuiMessageQueueName() -{ - static std::string name = - "chatterino_gui" + Paths::getInstance()->applicationFilePathHash.toStdString(); - return name; -} - } // namespace chatterino diff --git a/src/singletons/NativeMessaging.hpp b/src/singletons/NativeMessaging.hpp index b9fa7ce6b..79c90ebd2 100644 --- a/src/singletons/NativeMessaging.hpp +++ b/src/singletons/NativeMessaging.hpp @@ -1,16 +1,28 @@ #pragma once -#include "common/Singleton.hpp" - #include +class Application; +class Paths; + namespace chatterino { -class NativeMessaging final + +void registerNmHost(Application &app); +std::string &getNmQueueName(Paths &paths); + +class NativeMessagingClient final { public: - // fourtf: don't add this class to the application class - NativeMessaging(); + void sendMessage(const QByteArray &array); + void writeToCout(const QByteArray &array); +}; +class NativeMessagingServer final +{ +public: + void start(); + +private: class ReceiverThread : public QThread { public: @@ -20,12 +32,7 @@ public: void handleMessage(const QJsonObject &root); }; - void writeByteArray(QByteArray a); - void registerHost(); - void openGuiMessageQueue(); - void sendToGuiProcess(const QByteArray &array); - - static std::string &getGuiMessageQueueName(); + ReceiverThread thread; }; } // namespace chatterino diff --git a/src/singletons/Paths.cpp b/src/singletons/Paths.cpp index c695e0468..40ce32524 100644 --- a/src/singletons/Paths.cpp +++ b/src/singletons/Paths.cpp @@ -14,6 +14,8 @@ Paths *Paths::instance = nullptr; Paths::Paths() { + this->instance = this; + this->initAppFilePathHash(); this->initCheckPortable(); @@ -21,20 +23,6 @@ Paths::Paths() this->initSubDirectories(); } -void Paths::initInstance() -{ - assert(!instance); - - instance = new Paths(); -} - -Paths *Paths::getInstance() -{ - assert(instance); - - return instance; -} - bool Paths::createFolder(const QString &folderPath) { return QDir().mkpath(folderPath); @@ -116,7 +104,7 @@ void Paths::initSubDirectories() Paths *getPaths() { - return Paths::getInstance(); + return Paths::instance; } } // namespace chatterino diff --git a/src/singletons/Paths.hpp b/src/singletons/Paths.hpp index e1df46767..b1b59da61 100644 --- a/src/singletons/Paths.hpp +++ b/src/singletons/Paths.hpp @@ -7,11 +7,10 @@ namespace chatterino { class Paths { - Paths(); - public: - static void initInstance(); - static Paths *getInstance(); + static Paths *instance; + + Paths(); // Root directory for the configuration files. %APPDATA%/chatterino or ExecutablePath for // portable mode @@ -41,10 +40,9 @@ private: void initAppDataDirectory(); void initSubDirectories(); - static Paths *instance; boost::optional portable_; }; -Paths *getPaths(); +[[deprecated]] Paths *getPaths(); } // namespace chatterino diff --git a/src/singletons/Resources.cpp b/src/singletons/Resources.cpp index 4b05b718b..daec74a5f 100644 --- a/src/singletons/Resources.cpp +++ b/src/singletons/Resources.cpp @@ -1,481 +1 @@ #include "singletons/Resources.hpp" - -#include "common/NetworkRequest.hpp" - -#include -#include -#include -#include -#include - -namespace chatterino { - -namespace { - -inline Image *lli(const char *pixmapPath, qreal scale = 1) -{ - return new Image(new QPixmap(pixmapPath), scale); -} - -template -inline bool ReadValue(const rapidjson::Value &object, const char *key, Type &out) -{ - if (!object.HasMember(key)) { - return false; - } - - const auto &value = object[key]; - - if (!value.Is()) { - return false; - } - - out = value.Get(); - - return true; -} - -template <> -inline bool ReadValue(const rapidjson::Value &object, const char *key, QString &out) -{ - if (!object.HasMember(key)) { - return false; - } - - const auto &value = object[key]; - - if (!value.IsString()) { - return false; - } - - out = value.GetString(); - - return true; -} - -template <> -inline bool ReadValue>(const rapidjson::Value &object, const char *key, - std::vector &out) -{ - if (!object.HasMember(key)) { - return false; - } - - const auto &value = object[key]; - - if (!value.IsArray()) { - return false; - } - - for (const rapidjson::Value &innerValue : value.GetArray()) { - if (!innerValue.IsString()) { - return false; - } - - out.emplace_back(innerValue.GetString()); - } - - return true; -} - -// Parse a single cheermote set (or "action") from the twitch api -inline bool ParseSingleCheermoteSet(Resources::JSONCheermoteSet &set, - const rapidjson::Value &action) -{ - if (!action.IsObject()) { - return false; - } - - if (!ReadValue(action, "prefix", set.prefix)) { - return false; - } - - if (!ReadValue(action, "scales", set.scales)) { - return false; - } - - if (!ReadValue(action, "backgrounds", set.backgrounds)) { - return false; - } - - if (!ReadValue(action, "states", set.states)) { - return false; - } - - if (!ReadValue(action, "type", set.type)) { - return false; - } - - if (!ReadValue(action, "updated_at", set.updatedAt)) { - return false; - } - - if (!ReadValue(action, "priority", set.priority)) { - return false; - } - - // Tiers - if (!action.HasMember("tiers")) { - return false; - } - - const auto &tiersValue = action["tiers"]; - - if (!tiersValue.IsArray()) { - return false; - } - - for (const rapidjson::Value &tierValue : tiersValue.GetArray()) { - Resources::JSONCheermoteSet::CheermoteTier tier; - - if (!tierValue.IsObject()) { - return false; - } - - if (!ReadValue(tierValue, "min_bits", tier.minBits)) { - return false; - } - - if (!ReadValue(tierValue, "id", tier.id)) { - return false; - } - - if (!ReadValue(tierValue, "color", tier.color)) { - return false; - } - - // Images - if (!tierValue.HasMember("images")) { - return false; - } - - const auto &imagesValue = tierValue["images"]; - - if (!imagesValue.IsObject()) { - return false; - } - - // Read images object - for (const auto &imageBackgroundValue : imagesValue.GetObject()) { - QString background = imageBackgroundValue.name.GetString(); - bool backgroundExists = false; - for (const auto &bg : set.backgrounds) { - if (background == bg) { - backgroundExists = true; - break; - } - } - - if (!backgroundExists) { - continue; - } - - const rapidjson::Value &imageBackgroundStates = imageBackgroundValue.value; - if (!imageBackgroundStates.IsObject()) { - continue; - } - - // Read each key which represents a background - for (const auto &imageBackgroundState : imageBackgroundStates.GetObject()) { - QString state = imageBackgroundState.name.GetString(); - bool stateExists = false; - for (const auto &_state : set.states) { - if (state == _state) { - stateExists = true; - break; - } - } - - if (!stateExists) { - continue; - } - - const rapidjson::Value &imageScalesValue = imageBackgroundState.value; - if (!imageScalesValue.IsObject()) { - continue; - } - - // Read each key which represents a scale - for (const auto &imageScaleValue : imageScalesValue.GetObject()) { - QString scale = imageScaleValue.name.GetString(); - bool scaleExists = false; - for (const auto &_scale : set.scales) { - if (scale == _scale) { - scaleExists = true; - break; - } - } - - if (!scaleExists) { - continue; - } - - const rapidjson::Value &imageScaleURLValue = imageScaleValue.value; - if (!imageScaleURLValue.IsString()) { - continue; - } - - QString url = imageScaleURLValue.GetString(); - - bool ok = false; - qreal scaleNumber = scale.toFloat(&ok); - if (!ok) { - continue; - } - - qreal chatterinoScale = 1 / scaleNumber; - - auto image = new Image(url, chatterinoScale); - - // TODO(pajlada): Fill in name and tooltip - tier.images[background][state][scale] = image; - } - } - } - - set.tiers.emplace_back(tier); - } - - return true; -} - -// Look through the results of https://api.twitch.tv/kraken/bits/actions?channel_id=11148817 for -// cheermote sets or "Actions" as they are called in the API -inline void ParseCheermoteSets(std::vector &sets, - const rapidjson::Document &d) -{ - if (!d.IsObject()) { - return; - } - - if (!d.HasMember("actions")) { - return; - } - - const auto &actionsValue = d["actions"]; - - if (!actionsValue.IsArray()) { - return; - } - - for (const auto &action : actionsValue.GetArray()) { - Resources::JSONCheermoteSet set; - bool res = ParseSingleCheermoteSet(set, action); - - if (res) { - sets.emplace_back(set); - } - } -} - -} // namespace -Resources::Resources() - : badgeStaff(lli(":/images/staff_bg.png")) - , badgeAdmin(lli(":/images/admin_bg.png")) - , badgeGlobalModerator(lli(":/images/globalmod_bg.png")) - , badgeModerator(lli(":/images/moderator_bg.png")) - , badgeTurbo(lli(":/images/turbo_bg.png")) - , badgeBroadcaster(lli(":/images/broadcaster_bg.png")) - , badgePremium(lli(":/images/twitchprime_bg.png")) - , badgeVerified(lli(":/images/verified.png", 0.25)) - , badgeSubscriber(lli(":/images/subscriber.png", 0.25)) - , badgeCollapsed(lli(":/images/collapse.png")) - , cheerBadge100000(lli(":/images/cheer100000")) - , cheerBadge10000(lli(":/images/cheer10000")) - , cheerBadge5000(lli(":/images/cheer5000")) - , cheerBadge1000(lli(":/images/cheer1000")) - , cheerBadge100(lli(":/images/cheer100")) - , cheerBadge1(lli(":/images/cheer1")) - , moderationmode_enabled(lli(":/images/moderatormode_enabled")) - , moderationmode_disabled(lli(":/images/moderatormode_disabled")) - , splitHeaderContext(lli(":/images/tool_moreCollapser_off16.png")) - , buttonBan(lli(":/images/button_ban.png", 0.25)) - , buttonTimeout(lli(":/images/button_timeout.png", 0.25)) - , pajaDank(lli(":/images/pajaDank.png", 0.25)) - , ppHop(new Image("https://fourtf.com/ppHop.gif", 0.25)) -{ - this->split.left = QIcon(":/images/split/splitleft.png"); - this->split.right = QIcon(":/images/split/splitright.png"); - this->split.up = QIcon(":/images/split/splitup.png"); - this->split.down = QIcon(":/images/split/splitdown.png"); - this->split.move = QIcon(":/images/split/splitmove.png"); - - this->buttons.ban = QPixmap(":/images/buttons/ban.png"); - this->buttons.unban = QPixmap(":/images/buttons/unban.png"); - this->buttons.mod = QPixmap(":/images/buttons/mod.png"); - this->buttons.unmod = QPixmap(":/images/buttons/unmod.png"); - - qDebug() << "init ResourceManager"; -} - -void Resources::initialize(Application &app) -{ - this->loadDynamicTwitchBadges(); - - this->loadChatterinoBadges(); -} - -Resources::BadgeVersion::BadgeVersion(QJsonObject &&root) - : badgeImage1x(new Image(root.value("image_url_1x").toString())) - , badgeImage2x(new Image(root.value("image_url_2x").toString())) - , badgeImage4x(new Image(root.value("image_url_4x").toString())) - , description(root.value("description").toString().toStdString()) - , title(root.value("title").toString().toStdString()) - , clickAction(root.value("clickAction").toString().toStdString()) - , clickURL(root.value("clickURL").toString().toStdString()) -{ -} - -void Resources::loadChannelData(const QString &roomID, bool bypassCache) -{ - QString url = "https://badges.twitch.tv/v1/badges/channels/" + roomID + "/display?language=en"; - - NetworkRequest req(url); - req.setCaller(QThread::currentThread()); - - req.onSuccess([this, roomID](auto result) { - auto root = result.parseJson(); - QJsonObject sets = root.value("badge_sets").toObject(); - - Resources::Channel &ch = this->channels[roomID]; - - for (QJsonObject::iterator it = sets.begin(); it != sets.end(); ++it) { - QJsonObject versions = it.value().toObject().value("versions").toObject(); - - auto &badgeSet = ch.badgeSets[it.key().toStdString()]; - auto &versionsMap = badgeSet.versions; - - for (auto versionIt = std::begin(versions); versionIt != std::end(versions); - ++versionIt) { - std::string kkey = versionIt.key().toStdString(); - QJsonObject versionObj = versionIt.value().toObject(); - BadgeVersion v(std::move(versionObj)); - versionsMap.emplace(kkey, v); - } - } - - ch.loaded = true; - - return true; - }); - - req.execute(); - - QString cheermoteUrl = "https://api.twitch.tv/kraken/bits/actions?channel_id=" + roomID; - auto request = NetworkRequest::twitchRequest(cheermoteUrl); - request.setCaller(QThread::currentThread()); - - request.onSuccess([this, roomID](auto result) { - auto d = result.parseRapidJson(); - Resources::Channel &ch = this->channels[roomID]; - - ParseCheermoteSets(ch.jsonCheermoteSets, d); - - for (auto &set : ch.jsonCheermoteSets) { - CheermoteSet cheermoteSet; - cheermoteSet.regex = QRegularExpression("^" + set.prefix.toLower() + "([1-9][0-9]*)$"); - - for (auto &tier : set.tiers) { - Cheermote cheermote; - - cheermote.color = QColor(tier.color); - cheermote.minBits = tier.minBits; - - // TODO(pajlada): We currently hardcode dark here :| - // We will continue to do so for now since we haven't had to - // solve that anywhere else - cheermote.emoteDataAnimated.image1x = tier.images["dark"]["animated"]["1"]; - cheermote.emoteDataAnimated.image2x = tier.images["dark"]["animated"]["2"]; - cheermote.emoteDataAnimated.image3x = tier.images["dark"]["animated"]["4"]; - - cheermote.emoteDataStatic.image1x = tier.images["dark"]["static"]["1"]; - cheermote.emoteDataStatic.image2x = tier.images["dark"]["static"]["2"]; - cheermote.emoteDataStatic.image3x = tier.images["dark"]["static"]["4"]; - - cheermoteSet.cheermotes.emplace_back(cheermote); - } - - std::sort(cheermoteSet.cheermotes.begin(), cheermoteSet.cheermotes.end(), - [](const auto &lhs, const auto &rhs) { - return lhs.minBits < rhs.minBits; // - }); - - ch.cheermoteSets.emplace_back(cheermoteSet); - } - - return true; - }); - - request.execute(); -} - -void Resources::loadDynamicTwitchBadges() -{ - static QString url("https://badges.twitch.tv/v1/badges/global/display?language=en"); - - NetworkRequest req(url); - req.setCaller(QThread::currentThread()); - req.onSuccess([this](auto result) { - auto root = result.parseJson(); - QJsonObject sets = root.value("badge_sets").toObject(); - for (QJsonObject::iterator it = sets.begin(); it != sets.end(); ++it) { - QJsonObject versions = it.value().toObject().value("versions").toObject(); - - auto &badgeSet = this->badgeSets[it.key().toStdString()]; - auto &versionsMap = badgeSet.versions; - - for (auto versionIt = std::begin(versions); versionIt != std::end(versions); - ++versionIt) { - std::string kkey = versionIt.key().toStdString(); - QJsonObject versionObj = versionIt.value().toObject(); - BadgeVersion v(std::move(versionObj)); - versionsMap.emplace(kkey, v); - } - } - - this->dynamicBadgesLoaded = true; - - return true; - }); - - req.execute(); -} - -void Resources::loadChatterinoBadges() -{ - this->chatterinoBadges.clear(); - - static QString url("https://fourtf.com/chatterino/badges.json"); - - NetworkRequest req(url); - req.setCaller(QThread::currentThread()); - - req.onSuccess([this](auto result) { - auto root = result.parseJson(); - QJsonArray badgeVariants = root.value("badges").toArray(); - for (QJsonArray::iterator it = badgeVariants.begin(); it != badgeVariants.end(); ++it) { - QJsonObject badgeVariant = it->toObject(); - const std::string badgeVariantTooltip = - badgeVariant.value("tooltip").toString().toStdString(); - const QString &badgeVariantImageURL = badgeVariant.value("image").toString(); - - auto badgeVariantPtr = std::make_shared( - badgeVariantTooltip, new Image(badgeVariantImageURL)); - - QJsonArray badgeVariantUsers = badgeVariant.value("users").toArray(); - - for (QJsonArray::iterator it = badgeVariantUsers.begin(); it != badgeVariantUsers.end(); - ++it) { - const std::string username = it->toString().toStdString(); - this->chatterinoBadges[username] = - std::shared_ptr(badgeVariantPtr); - } - } - - return true; - }); - - req.execute(); -} - -} // namespace chatterino diff --git a/src/singletons/Resources.hpp b/src/singletons/Resources.hpp index 86db4d84a..57a1b79ad 100644 --- a/src/singletons/Resources.hpp +++ b/src/singletons/Resources.hpp @@ -1,160 +1,3 @@ #pragma once -#include "common/Singleton.hpp" - -#include "common/Emotemap.hpp" - -#include -#include - -#include -#include -#include - -namespace chatterino { - -class Resources : public Singleton -{ -public: - Resources(); - - ~Resources() = delete; - - virtual void initialize(Application &app) override; - - struct { - QIcon left; - QIcon right; - QIcon up; - QIcon down; - QIcon move; - } split; - - struct { - QPixmap ban; - QPixmap unban; - QPixmap mod; - QPixmap unmod; - } buttons; - - Image *badgeStaff; - Image *badgeAdmin; - Image *badgeGlobalModerator; - Image *badgeModerator; - Image *badgeTurbo; - Image *badgeBroadcaster; - Image *badgePremium; - Image *badgeVerified; - Image *badgeSubscriber; - Image *badgeCollapsed; - - Image *cheerBadge100000; - Image *cheerBadge10000; - Image *cheerBadge5000; - Image *cheerBadge1000; - Image *cheerBadge100; - Image *cheerBadge1; - - Image *moderationmode_enabled; - Image *moderationmode_disabled; - - Image *splitHeaderContext; - - std::map cheerBadges; - - struct BadgeVersion { - BadgeVersion() = delete; - - explicit BadgeVersion(QJsonObject &&root); - - Image *badgeImage1x; - Image *badgeImage2x; - Image *badgeImage4x; - std::string description; - std::string title; - std::string clickAction; - std::string clickURL; - }; - - struct BadgeSet { - std::map versions; - }; - - std::map badgeSets; - - bool dynamicBadgesLoaded = false; - - Image *buttonBan; - Image *buttonTimeout; - Image *pajaDank; - Image *ppHop; - - struct JSONCheermoteSet { - QString prefix; - std::vector scales; - - std::vector backgrounds; - std::vector states; - - QString type; - QString updatedAt; - int priority; - - struct CheermoteTier { - int minBits; - QString id; - QString color; - - // Background State Scale - std::map>> images; - }; - - std::vector tiers; - }; - - struct Cheermote { - // a Cheermote indicates one tier - QColor color; - int minBits; - - EmoteData emoteDataAnimated; - EmoteData emoteDataStatic; - }; - - struct CheermoteSet { - QRegularExpression regex; - std::vector cheermotes; - }; - - struct Channel { - std::map badgeSets; - std::vector jsonCheermoteSets; - std::vector cheermoteSets; - - bool loaded = false; - }; - - // channelId - std::map channels; - - // Chatterino badges - struct ChatterinoBadge { - ChatterinoBadge(const std::string &_tooltip, Image *_image) - : tooltip(_tooltip) - , image(_image) - { - } - - std::string tooltip; - Image *image; - }; - - // username - std::map> chatterinoBadges; - - void loadChannelData(const QString &roomID, bool bypassCache = false); - void loadDynamicTwitchBadges(); - void loadChatterinoBadges(); -}; - -} // namespace chatterino +#include "autogenerated/ResourcesAutogen.hpp" diff --git a/src/singletons/Settings.cpp b/src/singletons/Settings.cpp index 79398197f..10be19100 100644 --- a/src/singletons/Settings.cpp +++ b/src/singletons/Settings.cpp @@ -10,32 +10,25 @@ namespace chatterino { std::vector> _settings; +Settings *Settings::instance = nullptr; + void _actuallyRegisterSetting(std::weak_ptr setting) { _settings.push_back(setting); } -Settings::Settings() +Settings::Settings(Paths &paths) { - qDebug() << "init SettingManager"; + instance = this; + + QString settingsPath = paths.settingsDirectory + "/settings.json"; + + pajlada::Settings::SettingManager::gLoad(qPrintable(settingsPath)); } Settings &Settings::getInstance() { - static Settings instance; - - return instance; -} - -void Settings::initialize() -{ -} - -void Settings::load() -{ - QString settingsPath = getPaths()->settingsDirectory + "/settings.json"; - - pajlada::Settings::SettingManager::gLoad(qPrintable(settingsPath)); + return *instance; } void Settings::saveSnapshot() diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index a785df395..9058210f1 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -1,5 +1,7 @@ #pragma once +#include "Paths.hpp" + #include "common/ChatterinoSetting.hpp" #include "controllers/highlights/HighlightPhrase.hpp" #include "controllers/moderationactions/ModerationAction.hpp" @@ -14,13 +16,12 @@ void _actuallyRegisterSetting(std::weak_ptr set class Settings { - Settings(); + static Settings *instance; public: - static Settings &getInstance(); + Settings(Paths &paths); - void initialize(); - void load(); + static Settings &getInstance(); /// Appearance BoolSetting showTimestamps = {"/appearance/messages/showTimestamps", true}; @@ -131,6 +132,6 @@ private: std::unique_ptr snapshot_; }; -Settings *getSettings(); +[[deprecated]] Settings *getSettings(); } // namespace chatterino diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp index 2e43f7d15..61c5ec2f5 100644 --- a/src/singletons/WindowManager.cpp +++ b/src/singletons/WindowManager.cpp @@ -207,11 +207,12 @@ Window *WindowManager::windowAt(int index) return this->windows_.at(index); } -void WindowManager::initialize(Application &app) +void WindowManager::initialize(Settings &settings, Paths &paths) { assertInGuiThread(); - app.themes->repaintVisibleChatWidgets_.connect([this] { this->repaintVisibleChatWidgets(); }); + getApp()->themes->repaintVisibleChatWidgets_.connect( + [this] { this->repaintVisibleChatWidgets(); }); assert(!this->initialized_); @@ -301,20 +302,15 @@ void WindowManager::initialize(Application &app) mainWindow_->getNotebook().addPage(true); } - auto settings = getSettings(); + settings.timestampFormat.connect([this](auto, auto) { this->layoutChannelViews(); }); - settings->timestampFormat.connect([this](auto, auto) { - auto app = getApp(); - this->layoutChannelViews(); - }); + settings.emoteScale.connect([this](auto, auto) { this->forceLayoutChannelViews(); }); - settings->emoteScale.connect([this](auto, auto) { this->forceLayoutChannelViews(); }); - - settings->timestampFormat.connect([this](auto, auto) { this->forceLayoutChannelViews(); }); - settings->alternateMessageBackground.connect( + settings.timestampFormat.connect([this](auto, auto) { this->forceLayoutChannelViews(); }); + settings.alternateMessageBackground.connect( [this](auto, auto) { this->forceLayoutChannelViews(); }); - settings->separateMessages.connect([this](auto, auto) { this->forceLayoutChannelViews(); }); - settings->collpseMessagesMinLines.connect( + settings.separateMessages.connect([this](auto, auto) { this->forceLayoutChannelViews(); }); + settings.collpseMessagesMinLines.connect( [this](auto, auto) { this->forceLayoutChannelViews(); }); this->initialized_ = true; @@ -438,7 +434,7 @@ void WindowManager::encodeChannel(IndirectChannel channel, QJsonObject &obj) switch (channel.getType()) { case Channel::Type::Twitch: { obj.insert("type", "twitch"); - obj.insert("name", channel.get()->name); + obj.insert("name", channel.get()->getName()); } break; case Channel::Type::TwitchMentions: { obj.insert("type", "mentions"); diff --git a/src/singletons/WindowManager.hpp b/src/singletons/WindowManager.hpp index 93a586079..68a0dc7d5 100644 --- a/src/singletons/WindowManager.hpp +++ b/src/singletons/WindowManager.hpp @@ -6,7 +6,10 @@ namespace chatterino { -class WindowManager : public Singleton +class Settings; +class Paths; + +class WindowManager final : public Singleton { public: WindowManager(); @@ -36,7 +39,7 @@ public: int windowCount(); Window *windowAt(int index); - virtual void initialize(Application &app) override; + virtual void initialize(Settings &settings, Paths &paths) override; virtual void save() override; void closeAll(); diff --git a/src/util/JsonQuery.cpp b/src/util/JsonQuery.cpp new file mode 100644 index 000000000..1c7f18a3c --- /dev/null +++ b/src/util/JsonQuery.cpp @@ -0,0 +1,9 @@ +#include "JsonQuery.hpp" + +namespace chatterino { + +JsonQuery::JsonQuery() +{ +} + +} // namespace chatterino diff --git a/src/util/JsonQuery.hpp b/src/util/JsonQuery.hpp new file mode 100644 index 000000000..7c8f4c11f --- /dev/null +++ b/src/util/JsonQuery.hpp @@ -0,0 +1,12 @@ +#pragma once + +class QJsonObject; + +namespace chatterino { +class JsonQuery +{ +public: + JsonQuery(); +}; + +} // namespace chatterino diff --git a/src/widgets/Notebook.cpp b/src/widgets/Notebook.cpp index b076cabae..5068732b7 100644 --- a/src/widgets/Notebook.cpp +++ b/src/widgets/Notebook.cpp @@ -29,6 +29,8 @@ Notebook::Notebook(QWidget *parent) : BaseWidget(parent) , addButton_(this) { + this->addButton_.setIcon(NotebookButton::Icon::Plus); + this->addButton_.setHidden(true); auto *shortcut_next = new QShortcut(QKeySequence("Ctrl+Tab"), this); @@ -425,8 +427,7 @@ void SplitNotebook::addCustomButtons() settingsBtn->setVisible(!getApp()->settings->hidePreferencesButton.getValue()); getApp()->settings->hidePreferencesButton.connect( - [settingsBtn](bool hide, auto) { settingsBtn->setVisible(!hide); }, - this->connections_); + [settingsBtn](bool hide, auto) { settingsBtn->setVisible(!hide); }, this->connections_); settingsBtn->setIcon(NotebookButton::Settings); diff --git a/src/widgets/Window.cpp b/src/widgets/Window.cpp index ab083ace1..37d808c2f 100644 --- a/src/widgets/Window.cpp +++ b/src/widgets/Window.cpp @@ -159,8 +159,8 @@ void Window::addLayout() void Window::addCustomTitlebarButtons() { - return_unless(this->hasCustomWindowFrame()); - return_unless(this->type_ == Type::Main); + if (!this->hasCustomWindowFrame()) return; + if (this->type_ != Type::Main) return; // settings this->addTitleBarButton(TitleBarButton::Settings, [] { diff --git a/src/widgets/dialogs/EmotePopup.cpp b/src/widgets/dialogs/EmotePopup.cpp index 24e4e8dcf..aa33925e3 100644 --- a/src/widgets/dialogs/EmotePopup.cpp +++ b/src/widgets/dialogs/EmotePopup.cpp @@ -46,7 +46,7 @@ EmotePopup::EmotePopup() void EmotePopup::loadChannel(ChannelPtr _channel) { - this->setWindowTitle("Emotes from " + _channel->name); + this->setWindowTitle("Emotes from " + _channel->getName()); TwitchChannel *channel = dynamic_cast(_channel.get()); @@ -70,21 +70,19 @@ void EmotePopup::loadChannel(ChannelPtr _channel) builder2.getMessage()->flags |= Message::Centered; builder2.getMessage()->flags |= Message::DisableCompactEmotes; - map.each([&](const QString &key, const EmoteData &value) { - builder2.append((new EmoteElement(value, MessageElement::Flags::AlwaysShow)) - ->setLink(Link(Link::InsertText, key))); - }); + for (auto emote : map) { + builder2.append((new EmoteElement(emote.second, MessageElement::Flags::AlwaysShow)) + ->setLink(Link(Link::InsertText, emote.first.string))); + } emoteChannel->addMessage(builder2.getMessage()); }; auto app = getApp(); - QString userID = app->accounts->twitch.getCurrent()->getUserId(); - // fourtf: the entire emote manager needs to be refactored so there's no point in trying to // fix this pile of garbage - for (const auto &set : app->emotes->twitch.emotes[userID].emoteSets) { + for (const auto &set : app->accounts->twitch.getCurrent()->accessEmotes()->emoteSets) { // TITLE MessageBuilder builder1; @@ -110,20 +108,22 @@ void EmotePopup::loadChannel(ChannelPtr _channel) builder2.getMessage()->flags |= Message::DisableCompactEmotes; for (const auto &emote : set->emotes) { - [&](const QString &key, const EmoteData &value) { - builder2.append((new EmoteElement(value, MessageElement::Flags::AlwaysShow)) - ->setLink(Link(Link::InsertText, key))); - }(emote.code, app->emotes->twitch.getEmoteById(emote.id, emote.code)); + builder2.append( + (new EmoteElement(app->emotes->twitch.getOrCreateEmote(emote.id, emote.name), + MessageElement::Flags::AlwaysShow)) + ->setLink(Link(Link::InsertText, emote.name.string))); } emoteChannel->addMessage(builder2.getMessage()); } - addEmotes(app->emotes->bttv.globalEmotes, "BetterTTV Global Emotes", "BetterTTV Global Emote"); - addEmotes(channel->getBttvEmotes(), "BetterTTV Channel Emotes", "BetterTTV Channel Emote"); - addEmotes(app->emotes->ffz.globalEmotes, "FrankerFaceZ Global Emotes", - "FrankerFaceZ Global Emote"); - addEmotes(channel->getFfzEmotes(), "FrankerFaceZ Channel Emotes", "FrankerFaceZ Channel Emote"); + addEmotes(*app->emotes->bttv.accessGlobalEmotes(), "BetterTTV Global Emotes", + "BetterTTV Global Emote"); + addEmotes(*channel->accessBttvEmotes(), "BetterTTV Channel Emotes", "BetterTTV Channel Emote"); + // addEmotes(*app->emotes->ffz.accessGlobalEmotes(), "FrankerFaceZ Global Emotes", + // "FrankerFaceZ Global Emote"); + addEmotes(*channel->accessFfzEmotes(), "FrankerFaceZ Channel Emotes", + "FrankerFaceZ Channel Emote"); this->viewEmotes_->setChannel(emoteChannel); } @@ -146,9 +146,9 @@ void EmotePopup::loadEmojis() builder.getMessage()->flags |= Message::Centered; builder.getMessage()->flags |= Message::DisableCompactEmotes; - emojis.each([&builder](const QString &key, const auto &value) { + emojis.each([&builder](const auto &key, const auto &value) { builder.append( - (new EmoteElement(value->emoteData, MessageElement::Flags::AlwaysShow)) + (new EmoteElement(value->emote, MessageElement::Flags::AlwaysShow)) ->setLink(Link(Link::Type::InsertText, ":" + value->shortCodes[0] + ":"))); }); emojiChannel->addMessage(builder.getMessage()); diff --git a/src/widgets/dialogs/LogsPopup.cpp b/src/widgets/dialogs/LogsPopup.cpp index a2afb9301..4ad3f962e 100644 --- a/src/widgets/dialogs/LogsPopup.cpp +++ b/src/widgets/dialogs/LogsPopup.cpp @@ -34,7 +34,7 @@ void LogsPopup::setInfo(ChannelPtr channel, QString userName) { this->channel_ = channel; this->userName_ = userName; - this->setWindowTitle(this->userName_ + "'s logs in #" + this->channel_->name); + this->setWindowTitle(this->userName_ + "'s logs in #" + this->channel_->getName()); this->getLogviewerLogs(); } @@ -53,7 +53,7 @@ void LogsPopup::getLogviewerLogs() return; } - QString channelName = twitchChannel->name; + QString channelName = twitchChannel->getName(); QString url = QString("https://cbenni.com/api/logs/%1/?nick=%2&before=500") .arg(channelName, this->userName_); @@ -66,7 +66,7 @@ void LogsPopup::getLogviewerLogs() return true; }); - req.onSuccess([this, channelName](auto result) { + req.onSuccess([this, channelName](auto result) -> Outcome { auto data = result.parseJson(); std::vector messages; ChannelPtr logsChannel(new Channel("logs", Channel::Type::None)); @@ -89,7 +89,7 @@ void LogsPopup::getLogviewerLogs() }; this->setMessages(messages); - return true; + return Success; }); req.execute(); @@ -102,7 +102,7 @@ void LogsPopup::getOverrustleLogs() return; } - QString channelName = twitchChannel->name; + QString channelName = twitchChannel->getName(); QString url = QString("https://overrustlelogs.net/api/v1/stalk/%1/%2.json?limit=500") .arg(channelName, this->userName_); @@ -120,7 +120,7 @@ void LogsPopup::getOverrustleLogs() return true; }); - req.onSuccess([this, channelName](auto result) { + req.onSuccess([this, channelName](auto result) -> Outcome { auto data = result.parseJson(); std::vector messages; if (data.contains("lines")) { @@ -141,7 +141,7 @@ void LogsPopup::getOverrustleLogs() } this->setMessages(messages); - return true; + return Success; }); req.execute(); diff --git a/src/widgets/dialogs/SelectChannelDialog.cpp b/src/widgets/dialogs/SelectChannelDialog.cpp index 9a9fb99df..4f026112b 100644 --- a/src/widgets/dialogs/SelectChannelDialog.cpp +++ b/src/widgets/dialogs/SelectChannelDialog.cpp @@ -154,7 +154,7 @@ void SelectChannelDialog::setSelectedChannel(IndirectChannel _channel) case Channel::Type::Twitch: { this->ui_.notebook->selectIndex(TAB_TWITCH); this->ui_.twitch.channel->setFocus(); - this->ui_.twitch.channelName->setText(channel->name); + this->ui_.twitch.channelName->setText(channel->getName()); } break; case Channel::Type::TwitchWatching: { this->ui_.notebook->selectIndex(TAB_TWITCH); diff --git a/src/widgets/dialogs/UserInfoPopup.cpp b/src/widgets/dialogs/UserInfoPopup.cpp index edddede5e..77c30ba77 100644 --- a/src/widgets/dialogs/UserInfoPopup.cpp +++ b/src/widgets/dialogs/UserInfoPopup.cpp @@ -16,6 +16,8 @@ #include #include #include +#include +#include #define TEXT_FOLLOWERS "Followers: " #define TEXT_VIEWS "Views: " @@ -255,7 +257,7 @@ void UserInfoPopup::updateUserData() auto request = NetworkRequest::twitchRequest(url); request.setCaller(this); - request.onSuccess([this](auto result) { + request.onSuccess([this](auto result) -> Outcome { auto obj = result.parseJson(); this->ui_.followerCountLabel->setText(TEXT_FOLLOWERS + QString::number(obj.value("followers").toInt())); @@ -266,7 +268,7 @@ void UserInfoPopup::updateUserData() this->loadAvatar(QUrl(obj.value("logo").toString())); - return true; + return Success; }); request.execute(); diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index e3b8fb847..47aa5b7f6 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -32,6 +32,58 @@ #define CHAT_HOVER_PAUSE_DURATION 1000 namespace chatterino { +namespace { +void addEmoteContextMenuItems(const Emote &emote, MessageElement::Flags creatorFlags, QMenu &menu) +{ + auto openAction = menu.addAction("Open"); + auto openMenu = new QMenu; + openAction->setMenu(openMenu); + + auto copyAction = menu.addAction("Copy"); + auto copyMenu = new QMenu; + copyAction->setMenu(copyMenu); + + // see if the QMenu actually gets destroyed + QObject::connect(openMenu, &QMenu::destroyed, [] { + QMessageBox(QMessageBox::Information, "xD", "the menu got deleted").exec(); + }); + + // Add copy and open links for 1x, 2x, 3x + auto addImageLink = [&](const ImagePtr &image, char scale) { + if (image->isValid()) { + copyMenu->addAction(QString(scale) + "x link", [url = image->getUrl()] { + QApplication::clipboard()->setText(url.string); + }); + openMenu->addAction(QString(scale) + "x link", [url = image->getUrl()] { + QDesktopServices::openUrl(QUrl(url.string)); + }); + } + }; + + addImageLink(emote.images.getImage1(), '1'); + addImageLink(emote.images.getImage2(), '2'); + addImageLink(emote.images.getImage3(), '3'); + + // Copy and open emote page link + auto addPageLink = [&](const QString &name) { + copyMenu->addSeparator(); + openMenu->addSeparator(); + + copyMenu->addAction("Copy " + name + " emote link", [url = emote.homePage] { + QApplication::clipboard()->setText(url.string); // + }); + openMenu->addAction("Open " + name + " emote link", [url = emote.homePage] { + QDesktopServices::openUrl(QUrl(url.string)); // + }); + }; + + if (creatorFlags & MessageElement::Flags::BttvEmote) { + addPageLink("BTTV"); + } else if (creatorFlags & MessageElement::Flags::FfzEmote) { + addPageLink("FFZ"); + } +} +} // namespace ChannelView::ChannelView(BaseWidget *parent) : BaseWidget(parent) @@ -988,74 +1040,8 @@ void ChannelView::addContextMenuItems(const MessageLayoutElement *hoveredElement // Emote actions if (creatorFlags & (MessageElement::Flags::EmoteImages | MessageElement::Flags::EmojiImage)) { - const auto &emoteElement = static_cast(creator); - - // TODO: We might want to add direct "Open image" variants alongside the Copy - // actions - if (emoteElement.data.image1x != nullptr) { - QAction *addEntry = menu->addAction("Copy emote link..."); - - QMenu *procmenu = new QMenu; - addEntry->setMenu(procmenu); - procmenu->addAction("Copy 1x link", [url = emoteElement.data.image1x->getUrl()] { - QApplication::clipboard()->setText(url); // - }); - if (emoteElement.data.image2x != nullptr) { - procmenu->addAction("Copy 2x link", [url = emoteElement.data.image2x->getUrl()] { - QApplication::clipboard()->setText(url); // - }); - } - if (emoteElement.data.image3x != nullptr) { - procmenu->addAction("Copy 3x link", [url = emoteElement.data.image3x->getUrl()] { - QApplication::clipboard()->setText(url); // - }); - } - if ((creatorFlags & MessageElement::Flags::BttvEmote) != 0) { - procmenu->addSeparator(); - QString emotePageLink = emoteElement.data.pageLink; - procmenu->addAction("Copy BTTV emote link", [emotePageLink] { - QApplication::clipboard()->setText(emotePageLink); // - }); - } else if ((creatorFlags & MessageElement::Flags::FfzEmote) != 0) { - procmenu->addSeparator(); - QString emotePageLink = emoteElement.data.pageLink; - procmenu->addAction("Copy FFZ emote link", [emotePageLink] { - QApplication::clipboard()->setText(emotePageLink); // - }); - } - } - if (emoteElement.data.image1x != nullptr) { - QAction *addEntry = menu->addAction("Open emote link..."); - - QMenu *procmenu = new QMenu; - addEntry->setMenu(procmenu); - procmenu->addAction("Open 1x link", [url = emoteElement.data.image1x->getUrl()] { - QDesktopServices::openUrl(QUrl(url)); // - }); - if (emoteElement.data.image2x != nullptr) { - procmenu->addAction("Open 2x link", [url = emoteElement.data.image2x->getUrl()] { - QDesktopServices::openUrl(QUrl(url)); // - }); - } - if (emoteElement.data.image3x != nullptr) { - procmenu->addAction("Open 3x link", [url = emoteElement.data.image3x->getUrl()] { - QDesktopServices::openUrl(QUrl(url)); // - }); - } - if ((creatorFlags & MessageElement::Flags::BttvEmote) != 0) { - procmenu->addSeparator(); - QString emotePageLink = emoteElement.data.pageLink; - procmenu->addAction("Open BTTV emote link", [emotePageLink] { - QDesktopServices::openUrl(QUrl(emotePageLink)); // - }); - } else if ((creatorFlags & MessageElement::Flags::FfzEmote) != 0) { - procmenu->addSeparator(); - QString emotePageLink = emoteElement.data.pageLink; - procmenu->addAction("Open FFZ emote link", [emotePageLink] { - QDesktopServices::openUrl(QUrl(emotePageLink)); // - }); - } - } + const auto emoteElement = dynamic_cast(&creator); + if (emoteElement) addEmoteContextMenuItems(*emoteElement->getEmote(), creatorFlags, *menu); } // add seperator @@ -1204,8 +1190,7 @@ bool ChannelView::tryGetMessageAt(QPoint p, std::shared_ptr &_mes int ChannelView::getLayoutWidth() const { - if (this->scrollBar_.isVisible()) - return int(this->width() - 8 * this->getScale()); + if (this->scrollBar_.isVisible()) return int(this->width() - 8 * this->getScale()); return this->width(); } diff --git a/src/widgets/helper/SearchPopup.cpp b/src/widgets/helper/SearchPopup.cpp index 52d20b337..1d311eaa3 100644 --- a/src/widgets/helper/SearchPopup.cpp +++ b/src/widgets/helper/SearchPopup.cpp @@ -64,7 +64,7 @@ void SearchPopup::setChannel(ChannelPtr channel) this->snapshot_ = channel->getMessageSnapshot(); this->performSearch(); - this->setWindowTitle("Searching in " + channel->name + "s history"); + this->setWindowTitle("Searching in " + channel->getName() + "s history"); } void SearchPopup::performSearch() diff --git a/src/widgets/settingspages/AboutPage.cpp b/src/widgets/settingspages/AboutPage.cpp index 8cf9059a3..f5a699eba 100644 --- a/src/widgets/settingspages/AboutPage.cpp +++ b/src/widgets/settingspages/AboutPage.cpp @@ -28,7 +28,7 @@ AboutPage::AboutPage() auto layout = widget.setLayoutType(); { QPixmap pixmap; - pixmap.load(":/images/aboutlogo.png"); + pixmap.load(":/settings/aboutlogo.png"); auto logo = layout.emplace().assign(&this->logo_); logo->setPixmap(pixmap); diff --git a/src/widgets/settingspages/LookPage.cpp b/src/widgets/settingspages/LookPage.cpp index 6f556f025..25943243d 100644 --- a/src/widgets/settingspages/LookPage.cpp +++ b/src/widgets/settingspages/LookPage.cpp @@ -277,28 +277,32 @@ ChannelPtr LookPage::createPreviewChannel() { auto message = MessagePtr(new Message()); message->addElement(new TimestampElement(QTime(8, 13, 42))); - message->addElement(new ImageElement(getApp()->resources->badgeModerator, - MessageElement::BadgeChannelAuthority)); - message->addElement(new ImageElement(getApp()->resources->badgeSubscriber, - MessageElement::BadgeSubscription)); + message->addElement( + new ImageElement(Image::fromNonOwningPixmap(&getApp()->resources->twitch.moderator), + MessageElement::BadgeChannelAuthority)); + message->addElement( + new ImageElement(Image::fromNonOwningPixmap(&getApp()->resources->twitch.subscriber), + MessageElement::BadgeSubscription)); message->addElement(new TextElement("username1:", MessageElement::Username, QColor("#0094FF"), FontStyle::ChatMediumBold)); message->addElement(new TextElement("This is a preview message", MessageElement::Text)); - message->addElement(new EmoteElement(EmoteData(getApp()->resources->pajaDank), - MessageElement::Flags::AlwaysShow)); + message->addElement( + new ImageElement(Image::fromNonOwningPixmap(&getApp()->resources->pajaDank), + MessageElement::Flags::AlwaysShow)); // message->addElement(new) channel->addMessage(message); } { auto message = MessagePtr(new Message()); message->addElement(new TimestampElement(QTime(8, 15, 21))); - message->addElement(new ImageElement(getApp()->resources->badgePremium, - MessageElement::BadgeChannelAuthority)); + message->addElement( + new ImageElement(Image::fromNonOwningPixmap(&getApp()->resources->twitch.broadcaster), + MessageElement::BadgeChannelAuthority)); message->addElement(new TextElement("username2:", MessageElement::Username, QColor("#FF6A00"), FontStyle::ChatMediumBold)); message->addElement(new TextElement("This is another one", MessageElement::Text)); - message->addElement( - new EmoteElement(EmoteData(getApp()->resources->ppHop), MessageElement::BttvEmote)); + // message->addElement(new ImageElement( + // Image::fromNonOwningPixmap(&getApp()->resources->ppHop), MessageElement::BttvEmote)); message->addElement( (new TextElement("www.fourtf.com", MessageElement::Text, MessageColor::Link)) ->setLink(Link(Link::Url, "https://www.fourtf.com"))); diff --git a/src/widgets/splits/Split.cpp b/src/widgets/splits/Split.cpp index 1ccdf63ed..65ab3b45e 100644 --- a/src/widgets/splits/Split.cpp +++ b/src/widgets/splits/Split.cpp @@ -76,7 +76,7 @@ Split::Split(QWidget *parent) createShortcut(this, "CTRL+R", &Split::doChangeChannel); // CTRL+F: Search - createShortcut(this, "CTRL+F", &Split::doSearch); + createShortcut(this, "CTRL+F", &Split::showSearchPopup); // F12 createShortcut(this, "F10", [] { @@ -378,7 +378,7 @@ void Split::doChangeChannel() auto popup = this->findChildren(); if (popup.size() && popup.at(0)->isVisible() && !popup.at(0)->isFloating()) { popup.at(0)->hide(); - doOpenViewerList(); + showViewerList(); } } @@ -401,36 +401,33 @@ void Split::doClearChat() this->view_.clearMessages(); } -void Split::doOpenChannel() +void Split::openInBrowser() { - ChannelPtr _channel = this->getChannel(); - TwitchChannel *tc = dynamic_cast(_channel.get()); + auto channel = this->getChannel(); - if (tc != nullptr) { - QDesktopServices::openUrl("https://twitch.tv/" + tc->name); + if (auto twitchChannel = dynamic_cast(channel.get())) { + QDesktopServices::openUrl("https://twitch.tv/" + twitchChannel->getName()); } } -void Split::doOpenPopupPlayer() +void Split::openInPopupPlayer() { - ChannelPtr _channel = this->getChannel(); - TwitchChannel *tc = dynamic_cast(_channel.get()); - - if (tc != nullptr) { - QDesktopServices::openUrl("https://player.twitch.tv/?channel=" + tc->name); + ChannelPtr channel = this->getChannel(); + if (auto twitchChannel = dynamic_cast(channel.get())) { + QDesktopServices::openUrl("https://player.twitch.tv/?channel=" + twitchChannel->getName()); } } -void Split::doOpenStreamlink() +void Split::openInStreamlink() { try { - openStreamlinkForChannel(this->getChannel()->name); + openStreamlinkForChannel(this->getChannel()->getName()); } catch (const Exception &ex) { Log("Error in doOpenStreamlink: {}", ex.what()); } } -void Split::doOpenViewerList() +void Split::showViewerList() { auto viewerDock = new QDockWidget("Viewer List", this); viewerDock->setAllowedAreas(Qt::LeftDockWidgetArea); @@ -458,10 +455,10 @@ void Split::doOpenViewerList() auto loadingLabel = new QLabel("Loading..."); auto request = NetworkRequest::twitchRequest("https://tmi.twitch.tv/group/user/" + - this->getChannel()->name + "/chatters"); + this->getChannel()->getName() + "/chatters"); request.setCaller(this); - request.onSuccess([=](auto result) { + request.onSuccess([=](auto result) -> Outcome { auto obj = result.parseJson(); QJsonObject chattersObj = obj.value("chatters").toObject(); @@ -472,7 +469,7 @@ void Split::doOpenViewerList() chattersList->addItem(v.toString()); } - return true; + return Success; }); request.execute(); @@ -485,8 +482,7 @@ void Split::doOpenViewerList() chattersList->hide(); resultList->clear(); for (auto &item : results) { - if (!labels.contains(item->text())) - resultList->addItem(item->text()); + if (!labels.contains(item->text())) resultList->addItem(item->text()); } resultList->show(); } else { @@ -500,13 +496,13 @@ void Split::doOpenViewerList() QObject::connect(chattersList, &QListWidget::doubleClicked, this, [=]() { if (!labels.contains(chattersList->currentItem()->text())) { - doOpenUserInfoPopup(chattersList->currentItem()->text()); + showUserInfoPopup(chattersList->currentItem()->text()); } }); QObject::connect(resultList, &QListWidget::doubleClicked, this, [=]() { if (!labels.contains(resultList->currentItem()->text())) { - doOpenUserInfoPopup(resultList->currentItem()->text()); + showUserInfoPopup(resultList->currentItem()->text()); } }); @@ -522,22 +518,22 @@ void Split::doOpenViewerList() viewerDock->show(); } -void Split::doOpenUserInfoPopup(const QString &user) +void Split::showUserInfoPopup(const UserName &user) { auto *userPopup = new UserInfoPopup; - userPopup->setData(user, this->getChannel()); + userPopup->setData(user.string, this->getChannel()); userPopup->setAttribute(Qt::WA_DeleteOnClose); userPopup->move(QCursor::pos() - QPoint(int(150 * this->getScale()), int(70 * this->getScale()))); userPopup->show(); } -void Split::doCopy() +void Split::copyToClipboard() { QApplication::clipboard()->setText(this->view_.getSelectedText()); } -void Split::doSearch() +void Split::showSearchPopup() { SearchPopup *popup = new SearchPopup(); @@ -563,26 +559,19 @@ static Iter select_randomly(Iter start, Iter end) void Split::drag() { - auto container = dynamic_cast(this->parentWidget()); - - if (container != nullptr) { + if (auto container = dynamic_cast(this->parentWidget())) { SplitContainer::isDraggingSplit = true; SplitContainer::draggingSplit = this; auto originalLocation = container->releaseSplit(this); - - QDrag *drag = new QDrag(this); - QMimeData *mimeData = new QMimeData; + auto drag = new QDrag(this); + auto mimeData = new QMimeData; mimeData->setData("chatterino/split", "xD"); - drag->setMimeData(mimeData); - Qt::DropAction dropAction = drag->exec(Qt::MoveAction); - - if (dropAction == Qt::IgnoreAction) { - container->insertSplit(this, - originalLocation); // SplitContainer::dragOriginalPosition); + if (drag->exec(Qt::MoveAction) == Qt::IgnoreAction) { + container->insertSplit(this, originalLocation); } SplitContainer::isDraggingSplit = false; diff --git a/src/widgets/splits/Split.hpp b/src/widgets/splits/Split.hpp index dc525462a..0ddfabced 100644 --- a/src/widgets/splits/Split.hpp +++ b/src/widgets/splits/Split.hpp @@ -90,7 +90,11 @@ protected: void focusInEvent(QFocusEvent *event) override; private: - void doOpenUserInfoPopup(const QString &user); + void showUserInfoPopup(const QString &userName) + { + this->showUserInfoPopup(UserName{userName}); + } + void showUserInfoPopup(const UserName &user); void channelNameUpdated(const QString &newChannelName); void handleModifiers(Qt::KeyboardModifiers modifiers); @@ -137,22 +141,22 @@ public slots: void doClearChat(); // Open link to twitch channel in default browser - void doOpenChannel(); + void openInBrowser(); // Open popup player of twitch channel in default browser - void doOpenPopupPlayer(); + void openInPopupPlayer(); // Open twitch channel stream through streamlink - void doOpenStreamlink(); + void openInStreamlink(); // Copy text from chat - void doCopy(); + void copyToClipboard(); // Open a search popup - void doSearch(); + void showSearchPopup(); // Open viewer list of the channel - void doOpenViewerList(); + void showViewerList(); }; } // namespace chatterino diff --git a/src/widgets/splits/SplitContainer.cpp b/src/widgets/splits/SplitContainer.cpp index 4a8e6da7c..e7dc50952 100644 --- a/src/widgets/splits/SplitContainer.cpp +++ b/src/widgets/splits/SplitContainer.cpp @@ -464,11 +464,9 @@ void SplitContainer::paintEvent(QPaintEvent *) void SplitContainer::dragEnterEvent(QDragEnterEvent *event) { - if (!event->mimeData()->hasFormat("chatterino/split")) - return; + if (!event->mimeData()->hasFormat("chatterino/split")) return; - if (!SplitContainer::isDraggingSplit) - return; + if (!SplitContainer::isDraggingSplit) return; this->isDragging_ = true; this->layout(); @@ -516,7 +514,7 @@ void SplitContainer::refreshTabTitle() bool first = true; for (const auto &chatWidget : this->splits_) { - auto channelName = chatWidget->getChannel()->name; + auto channelName = chatWidget->getChannel()->getName(); if (channelName.isEmpty()) { continue; } @@ -851,10 +849,8 @@ void SplitContainer::Node::layout(bool addSpacing, float _scale, std::vector &resizeRects) { for (std::unique_ptr &node : this->children_) { - if (node->flexH_ <= 0) - node->flexH_ = 0; - if (node->flexV_ <= 0) - node->flexV_ = 0; + if (node->flexH_ <= 0) node->flexH_ = 0; + if (node->flexV_ <= 0) node->flexV_ = 0; } switch (this->type_) { diff --git a/src/widgets/splits/SplitHeader.cpp b/src/widgets/splits/SplitHeader.cpp index 8d6b92676..145c79f9f 100644 --- a/src/widgets/splits/SplitHeader.cpp +++ b/src/widgets/splits/SplitHeader.cpp @@ -115,8 +115,8 @@ void SplitHeader::addDropdownItems(RippleEffectButton *) this->dropdownMenu_.addAction("Close split", this->split_, &Split::doCloseSplit, QKeySequence(tr("Ctrl+W"))); this->dropdownMenu_.addAction("Change channel", this->split_, &Split::doChangeChannel, QKeySequence(tr("Ctrl+R"))); this->dropdownMenu_.addSeparator(); - this->dropdownMenu_.addAction("Viewer list", this->split_, &Split::doOpenViewerList); - this->dropdownMenu_.addAction("Search", this->split_, &Split::doSearch, QKeySequence(tr("Ctrl+F"))); + this->dropdownMenu_.addAction("Viewer list", this->split_, &Split::showViewerList); + this->dropdownMenu_.addAction("Search", this->split_, &Split::showSearchPopup, QKeySequence(tr("Ctrl+F"))); this->dropdownMenu_.addSeparator(); this->dropdownMenu_.addAction("Popup", this->split_, &Split::doPopup); #ifdef USEWEBENGINE @@ -131,11 +131,11 @@ void SplitHeader::addDropdownItems(RippleEffectButton *) } }); #endif - this->dropdownMenu_.addAction("Open browser", this->split_, &Split::doOpenChannel); + this->dropdownMenu_.addAction("Open browser", this->split_, &Split::openInBrowser); #ifndef USEWEBENGINE - this->dropdownMenu_.addAction("Open browser popup", this->split_, &Split::doOpenPopupPlayer); + this->dropdownMenu_.addAction("Open browser popup", this->split_, &Split::openInPopupPlayer); #endif - this->dropdownMenu_.addAction("Open streamlink", this->split_, &Split::doOpenStreamlink); + this->dropdownMenu_.addAction("Open streamlink", this->split_, &Split::openInStreamlink); this->dropdownMenu_.addSeparator(); this->dropdownMenu_.addAction("Reload channel emotes", this, SLOT(menuReloadChannelEmotes())); this->dropdownMenu_.addAction("Reconnect", this, SLOT(menuManualReconnect())); @@ -303,7 +303,7 @@ void SplitHeader::updateChannelText() auto indirectChannel = this->split_->getIndirectChannel(); auto channel = this->split_->getChannel(); - QString title = channel->name; + QString title = channel->getName(); if (indirectChannel.getType() == Channel::Type::TwitchWatching) { title = "watching: " + (title.isEmpty() ? "none" : title); @@ -349,8 +349,8 @@ void SplitHeader::updateModerationModeIcon() auto app = getApp(); this->moderationButton_->setPixmap(this->split_->getModerationMode() - ? *app->resources->moderationmode_enabled->getPixmap() - : *app->resources->moderationmode_disabled->getPixmap()); + ? app->resources->buttons.modModeEnabled + : app->resources->buttons.modModeDisabled); bool modButtonVisible = false; ChannelPtr channel = this->split_->getChannel(); @@ -473,9 +473,9 @@ void SplitHeader::themeChangedEvent() } if (this->theme->isLightTheme()) { - this->dropdownButton_->setPixmap(QPixmap(":/images/menu_black.png")); + this->dropdownButton_->setPixmap(getApp()->resources->buttons.menuDark); } else { - this->dropdownButton_->setPixmap(QPixmap(":/images/menu_white.png")); + this->dropdownButton_->setPixmap(getApp()->resources->buttons.menuLight); } this->titleLabel->setPalette(palette); diff --git a/src/widgets/splits/SplitInput.cpp b/src/widgets/splits/SplitInput.cpp index 113a05d7a..44204fa18 100644 --- a/src/widgets/splits/SplitInput.cpp +++ b/src/widgets/splits/SplitInput.cpp @@ -130,7 +130,7 @@ void SplitInput::updateEmoteButton() { float scale = this->getScale(); - QString text = ""; + QString text = ""; text.replace("xD", QString::number(int(12 * scale))); if (this->theme->isLightTheme()) { @@ -254,7 +254,7 @@ void SplitInput::installKeyPressedEvent() } } else if (event->key() == Qt::Key_C && event->modifiers() == Qt::ControlModifier) { if (this->split_->view_.hasSelection()) { - this->split_->doCopy(); + this->split_->copyToClipboard(); event->accept(); } } diff --git a/src/widgets/splits/SplitOverlay.cpp b/src/widgets/splits/SplitOverlay.cpp index d6b504c6f..2bf27d4e9 100644 --- a/src/widgets/splits/SplitOverlay.cpp +++ b/src/widgets/splits/SplitOverlay.cpp @@ -29,12 +29,11 @@ SplitOverlay::SplitOverlay(Split *parent) layout->setColumnStretch(1, 1); layout->setColumnStretch(3, 1); - QPushButton *move = new QPushButton(getApp()->resources->split.move, QString()); - QPushButton *left = this->left_ = new QPushButton(getApp()->resources->split.left, QString()); - QPushButton *right = this->right_ = - new QPushButton(getApp()->resources->split.right, QString()); - QPushButton *up = this->up_ = new QPushButton(getApp()->resources->split.up, QString()); - QPushButton *down = this->down_ = new QPushButton(getApp()->resources->split.down, QString()); + auto *move = new QPushButton(getApp()->resources->split.move, QString()); + auto *left = this->left_ = new QPushButton(getApp()->resources->split.left, QString()); + auto *right = this->right_ = new QPushButton(getApp()->resources->split.right, QString()); + auto *up = this->up_ = new QPushButton(getApp()->resources->split.up, QString()); + auto *down = this->down_ = new QPushButton(getApp()->resources->split.down, QString()); move->setGraphicsEffect(new QGraphicsOpacityEffect(this)); left->setGraphicsEffect(new QGraphicsOpacityEffect(this)); diff --git a/weakOf b/weakOf new file mode 100644 index 000000000..e69de29bb