diff --git a/.clang-format b/.clang-format index 027dcce..8e92f28 100644 --- a/.clang-format +++ b/.clang-format @@ -1,10 +1,11 @@ -BasedOnStyle: Google -UseTab: Never -IndentWidth: 4 -TabWidth: 4 -BreakBeforeBraces: Attach -AllowShortIfStatementsOnASingleLine: false -IndentCaseLabels: false -AccessModifierOffset: -4 -ColumnLimit: 120 -NamespaceIndentation: All \ No newline at end of file +BasedOnStyle: Google +UseTab: Never +IndentWidth: 4 +TabWidth: 4 +BreakBeforeBraces: Attach +AllowShortIfStatementsOnASingleLine: false +IndentCaseLabels: false +AccessModifierOffset: -4 +ColumnLimit: 120 +NamespaceIndentation: All +SortIncludes: false \ No newline at end of file diff --git a/src/backends/windows.cpp b/src/backends/windows.cpp index de5a900..f270e9e 100644 --- a/src/backends/windows.cpp +++ b/src/backends/windows.cpp @@ -1,198 +1,208 @@ -#ifdef _WIN32 -#define WIN32_LEAN_AND_MEAN -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include "../backend.hpp" -#include "../utils.hpp" - -using namespace winrt; -using namespace Windows::Media::Control; -using namespace Windows::Storage::Streams; -#define EM_DASH "\xE2\x80\x94" -// codecvt is deprecated, but there is no good portable way to do this, I could technically use the winapi as this is -// the windows backend tho -std::string toStdString(winrt::hstring in) { - std::wstring_convert> converter; - return converter.to_bytes(in.c_str()); -} - -std::string getAppModelIdOfProcess(HANDLE hProc) { - UINT32 length = 0; - LONG rc = GetApplicationUserModelId(hProc, &length, NULL); - if (rc != ERROR_INSUFFICIENT_BUFFER) - return ""; - - PWSTR fullName = (PWSTR)malloc(length * sizeof(*fullName)); - if (!fullName) - return ""; - - rc = GetApplicationUserModelId(hProc, &length, fullName); - if (rc != ERROR_SUCCESS) { - free(fullName); - return ""; - } - std::string name = toStdString(fullName); - free(fullName); - return name; -} - -std::string getProcessNameFromAppModelId(std::string appModelId) { - DWORD processes[1024]; - DWORD cbNeeded; - - if (!EnumProcesses(processes, sizeof(processes), &cbNeeded)) - return ""; - - unsigned int processCount = cbNeeded / sizeof(DWORD); - - for (DWORD i = 0; i < processCount; i++) { - DWORD processID = processes[i]; - - HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processID); - if (hProcess) { - std::string modelid = getAppModelIdOfProcess(hProcess); - - if (modelid != appModelId) { - CloseHandle(hProcess); - continue; - } - - char exeName[MAX_PATH]{}; - DWORD size = MAX_PATH; - QueryFullProcessImageNameA(hProcess, 0, exeName, &size); - std::filesystem::path exePath = exeName; - CloseHandle(hProcess); - return exePath.filename().string(); - } - } - return ""; -} - -bool CreateShortcut(std::string source, std::string target) { - CoInitialize(nullptr); - WCHAR src[MAX_PATH]; - IShellLinkW* pShellLink = nullptr; - HRESULT hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_IShellLink, - reinterpret_cast(&pShellLink)); - - if (SUCCEEDED(hr) && pShellLink) { - MultiByteToWideChar(CP_ACP, 0, source.c_str(), -1, src, MAX_PATH); - pShellLink->SetPath(src); - - IPersistFile* pPersistFile = nullptr; - hr = pShellLink->QueryInterface(IID_IPersistFile, reinterpret_cast(&pPersistFile)); - - if (SUCCEEDED(hr) && pPersistFile) { - WCHAR dst[MAX_PATH]; - MultiByteToWideChar(CP_ACP, 0, target.c_str(), -1, dst, MAX_PATH); - hr = pPersistFile->Save(dst, TRUE); - pPersistFile->Release(); - } - - pShellLink->Release(); - } - - CoUninitialize(); - return SUCCEEDED(hr); -} - -std::filesystem::path backend::getConfigDirectory() { - std::filesystem::path configDirectoryPath = std::getenv("APPDATA"); - configDirectoryPath = configDirectoryPath / "PlayerLink"; - return configDirectoryPath; -} - -bool backend::toggleAutostart(bool enabled) { - std::filesystem::path shortcutPath = std::getenv("APPDATA"); - shortcutPath = shortcutPath / "Microsoft" / "Windows" / "Start Menu" / "Programs" / "Startup"; - std::filesystem::create_directories(shortcutPath); - shortcutPath = shortcutPath / "PlayerLink.lnk"; - - if (!enabled && std::filesystem::exists(shortcutPath)) { - std::filesystem::remove(shortcutPath); - return true; - } - char binaryPath[MAX_PATH]{}; - GetModuleFileNameA(NULL, binaryPath, MAX_PATH); - bool result = CreateShortcut(binaryPath, shortcutPath.string()); - return result; -} - -std::shared_ptr backend::getMediaInformation() { - auto sessionManager = GlobalSystemMediaTransportControlsSessionManager::RequestAsync().get(); - auto currentSession = sessionManager.GetCurrentSession(); - if (!currentSession) - return nullptr; - - auto playbackInfo = currentSession.GetPlaybackInfo(); - auto mediaProperties = currentSession.TryGetMediaPropertiesAsync().get(); - auto timelineInformation = currentSession.GetTimelineProperties(); - if (!mediaProperties) - return nullptr; - - auto endTime = std::chrono::duration_cast(timelineInformation.EndTime()).count(); - auto elapsedTime = std::chrono::duration_cast(timelineInformation.Position()).count(); - - auto thumbnail = mediaProperties.Thumbnail(); - std::string thumbnailData = ""; - - if (thumbnail) { - auto stream = thumbnail.OpenReadAsync().get(); - size_t size = static_cast(stream.Size()); - - DataReader reader(stream); - reader.LoadAsync(static_cast(size)).get(); - - std::vector buffer(size); - reader.ReadBytes(buffer); - reader.Close(); - - thumbnailData = std::string(buffer.begin(), buffer.end()); - stream.Close(); - } - - std::string artist = toStdString(mediaProperties.Artist()); - std::string albumName = toStdString(mediaProperties.AlbumTitle()); - if (artist == "") - artist = toStdString(mediaProperties.AlbumArtist()); // Needed for some apps - - if (artist.find(EM_DASH) != std::string::npos) { - albumName = artist.substr(artist.find(EM_DASH) + 3); - artist = artist.substr(0, artist.find(EM_DASH)); - utils::trim(artist); - utils::trim(albumName); - } - - std::string modelId = toStdString(currentSession.SourceAppUserModelId()); - - // I do know that this is disgusting, but for some reason microsoft decided to switch out the exe name with the - // ApplicationUserModelId in some version of windows 11. So we check if it's an exe name, if not we are on some - // newer windows version and need to get the exe name from the model id. We cannot directly work with the model id - // because it's unique per machine and therefore would mess up configs with preconfigured apps. - if (modelId.find(".exe") == std::string::npos) - modelId = getProcessNameFromAppModelId(modelId); - - return std::make_shared( - playbackInfo.PlaybackStatus() == GlobalSystemMediaTransportControlsSessionPlaybackStatus::Paused, - toStdString(mediaProperties.Title()), artist, albumName, modelId, thumbnailData, endTime, elapsedTime); -} - -bool backend::init() { - return winrt::Windows::Foundation::Metadata::ApiInformation::IsTypePresent( - L"Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager"); -} - -#undef EM_DASH -#endif +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "../backend.hpp" +#include "../utils.hpp" + +using namespace winrt; +using namespace Windows::Media::Control; +using namespace Windows::Storage::Streams; +#define EM_DASH "\xE2\x80\x94" + +std::string toStdString(winrt::hstring& in) { + const wchar_t* wideStr = in.c_str(); + int wideStrLen = static_cast(in.size()); + int bufferSize = WideCharToMultiByte(CP_UTF8, 0, wideStr, wideStrLen, nullptr, 0, nullptr, nullptr); + if (bufferSize <= 0) + return ""; + + std::string result(bufferSize, 0); + + WideCharToMultiByte(CP_UTF8, 0, wideStr, wideStrLen, result.data(), bufferSize, nullptr, nullptr); + + return result; +} + +std::string getAppModelIdOfProcess(HANDLE hProc) { + UINT32 length = 0; + LONG rc = GetApplicationUserModelId(hProc, &length, NULL); + if (rc != ERROR_INSUFFICIENT_BUFFER) + return ""; + + PWSTR fullName = (PWSTR)malloc(length * sizeof(*fullName)); + if (!fullName) + return ""; + + rc = GetApplicationUserModelId(hProc, &length, fullName); + if (rc != ERROR_SUCCESS) { + free(fullName); + return ""; + } + winrt::hstring wideName = fullName; + std::string name = toStdString(wideName); + free(fullName); + return name; +} + +std::string getProcessNameFromAppModelId(std::string appModelId) { + DWORD processes[1024]; + DWORD cbNeeded; + + if (!EnumProcesses(processes, sizeof(processes), &cbNeeded)) + return ""; + + unsigned int processCount = cbNeeded / sizeof(DWORD); + + for (DWORD i = 0; i < processCount; i++) { + DWORD processID = processes[i]; + + HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processID); + if (hProcess) { + std::string modelid = getAppModelIdOfProcess(hProcess); + + if (modelid != appModelId) { + CloseHandle(hProcess); + continue; + } + + char exeName[MAX_PATH]{}; + DWORD size = MAX_PATH; + QueryFullProcessImageNameA(hProcess, 0, exeName, &size); + std::filesystem::path exePath = exeName; + CloseHandle(hProcess); + return exePath.filename().string(); + } + } + return ""; +} + +bool CreateShortcut(std::string source, std::string target) { + CoInitialize(nullptr); + WCHAR src[MAX_PATH]; + IShellLinkW* pShellLink = nullptr; + HRESULT hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_IShellLink, + reinterpret_cast(&pShellLink)); + + if (SUCCEEDED(hr) && pShellLink) { + MultiByteToWideChar(CP_ACP, 0, source.c_str(), -1, src, MAX_PATH); + pShellLink->SetPath(src); + + IPersistFile* pPersistFile = nullptr; + hr = pShellLink->QueryInterface(IID_IPersistFile, reinterpret_cast(&pPersistFile)); + + if (SUCCEEDED(hr) && pPersistFile) { + WCHAR dst[MAX_PATH]; + MultiByteToWideChar(CP_ACP, 0, target.c_str(), -1, dst, MAX_PATH); + hr = pPersistFile->Save(dst, TRUE); + pPersistFile->Release(); + } + + pShellLink->Release(); + } + + CoUninitialize(); + return SUCCEEDED(hr); +} + +std::filesystem::path backend::getConfigDirectory() { + std::filesystem::path configDirectoryPath = std::getenv("APPDATA"); + configDirectoryPath = configDirectoryPath / "PlayerLink"; + return configDirectoryPath; +} + +bool backend::toggleAutostart(bool enabled) { + std::filesystem::path shortcutPath = std::getenv("APPDATA"); + shortcutPath = shortcutPath / "Microsoft" / "Windows" / "Start Menu" / "Programs" / "Startup"; + std::filesystem::create_directories(shortcutPath); + shortcutPath = shortcutPath / "PlayerLink.lnk"; + + if (!enabled && std::filesystem::exists(shortcutPath)) { + std::filesystem::remove(shortcutPath); + return true; + } + char binaryPath[MAX_PATH]{}; + GetModuleFileNameA(NULL, binaryPath, MAX_PATH); + bool result = CreateShortcut(binaryPath, shortcutPath.string()); + return result; +} + +std::shared_ptr backend::getMediaInformation() { + static auto sessionManager = GlobalSystemMediaTransportControlsSessionManager::RequestAsync().get(); + auto currentSession = sessionManager.GetCurrentSession(); + if (!currentSession) + return nullptr; + + auto playbackInfo = currentSession.GetPlaybackInfo(); + auto mediaProperties = currentSession.TryGetMediaPropertiesAsync().get(); + auto timelineInformation = currentSession.GetTimelineProperties(); + if (!mediaProperties) + return nullptr; + + auto endTime = std::chrono::duration_cast(timelineInformation.EndTime()).count(); + auto elapsedTime = std::chrono::duration_cast(timelineInformation.Position()).count(); + + auto thumbnail = mediaProperties.Thumbnail(); + std::string thumbnailData = ""; + + if (thumbnail) { + auto stream = thumbnail.OpenReadAsync().get(); + size_t size = static_cast(stream.Size()); + + DataReader reader(stream); + reader.LoadAsync(static_cast(size)).get(); + + std::vector buffer(size); + reader.ReadBytes(buffer); + reader.Close(); + + thumbnailData = std::string(buffer.begin(), buffer.end()); + stream.Close(); + } + + std::string artist = toStdString(mediaProperties.Artist()); + std::string albumName = toStdString(mediaProperties.AlbumTitle()); + if (artist == "") + artist = toStdString(mediaProperties.AlbumArtist()); // Needed for some apps + + if (artist.find(EM_DASH) != std::string::npos) { + albumName = artist.substr(artist.find(EM_DASH) + 3); + artist = artist.substr(0, artist.find(EM_DASH)); + utils::trim(artist); + utils::trim(albumName); + } + + std::string modelId = toStdString(currentSession.SourceAppUserModelId()); + + // I do know that this is disgusting, but for some reason microsoft decided to switch out the exe name with the + // ApplicationUserModelId in some version of windows 11. So we check if it's an exe name, if not we are on some + // newer windows version and need to get the exe name from the model id. We cannot directly work with the model id + // because it's unique per machine and therefore would mess up configs with preconfigured apps. + if (modelId.find(".exe") == std::string::npos) + modelId = getProcessNameFromAppModelId(modelId); + + return std::make_shared( + playbackInfo.PlaybackStatus() == GlobalSystemMediaTransportControlsSessionPlaybackStatus::Paused, + toStdString(mediaProperties.Title()), std::move(artist), std::move(albumName), std::move(modelId), + std::move(thumbnailData), endTime, elapsedTime); +} + +bool backend::init() { + return winrt::Windows::Foundation::Metadata::ApiInformation::IsTypePresent( + L"Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager"); +} + +#undef EM_DASH +#endif diff --git a/src/main.cpp b/src/main.cpp index f8ce13c..9bb172f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,433 +1,436 @@ -#include -#include -#include -#include -#include -#include - -#include -#include - -#include "backend.hpp" -#include "lastfm.hpp" -#include "rsrc.hpp" -#include "utils.hpp" - -std::string lastPlayingSong = ""; -std::string lastMediaSource = ""; -std::string currentSongTitle = ""; -LastFM* lastfm = nullptr; - -void handleRPCTasks() { - while (true) { - while (true) { - DiscordEventHandlers discordHandler{}; - auto app = utils::getApp(lastMediaSource); - Discord_Initialize(app.clientId.c_str(), &discordHandler); - if (Discord_IsConnected()) - break; - std::this_thread::sleep_for(std::chrono::seconds(1)); - } - while (true) { - Discord_RunCallbacks(); - if (!Discord_IsConnected()) - break; - std::this_thread::sleep_for(std::chrono::seconds(1)); - } - Discord_Shutdown(); - } -} - -void initLastFM(bool checkMode = false) { - if (lastfm) - delete lastfm; - auto settings = utils::getSettings(); - if (!settings.lastfm.enabled && !checkMode) - return; - lastfm = new LastFM(settings.lastfm.username, settings.lastfm.password, settings.lastfm.api_key, - settings.lastfm.api_secret); - LastFM::LASTFM_STATUS status = lastfm->authenticate(); - if (status) - wxMessageBox(_("Error authenticating at LastFM!"), _("PlayerLink"), wxOK | wxICON_ERROR); - else if (checkMode) - wxMessageBox(_("The LastFM authentication was successful."), _("PlayerLink"), wxOK | wxICON_INFORMATION); -} - -void handleMediaTasks() { - initLastFM(); - int64_t lastMs = 0; - while (true) { - std::this_thread::sleep_for(std::chrono::seconds(1)); - auto mediaInformation = backend::getMediaInformation(); - if (!mediaInformation) { - currentSongTitle = ""; - Discord_ClearPresence(); // Nothing is playing rn, clear presence - continue; - } - - if (mediaInformation->paused) { - lastMs = 0; - currentSongTitle = ""; - Discord_ClearPresence(); - continue; - } - - std::string currentlyPlayingSong = mediaInformation->songTitle + mediaInformation->songArtist + - mediaInformation->songAlbum + std::to_string(mediaInformation->songDuration); - int64_t currentMs = mediaInformation->songElapsedTime; - - bool shouldContinue = - currentlyPlayingSong == lastPlayingSong && (lastMs <= currentMs) && (lastMs + 3000 >= currentMs); - lastMs = currentMs; - - if (shouldContinue) - continue; - - if (lastPlayingSong.find(mediaInformation->songTitle + mediaInformation->songArtist + - mediaInformation->songAlbum) == std::string::npos && - lastfm) - lastfm->scrobble(mediaInformation->songArtist, mediaInformation->songTitle); - - lastPlayingSong = currentlyPlayingSong; - currentSongTitle = mediaInformation->songArtist + " - " + mediaInformation->songTitle; - - std::string currentMediaSource = mediaInformation->playbackSource; - - if (currentMediaSource != lastMediaSource) { - lastMediaSource = currentMediaSource; - Discord_Shutdown(); - } // reinitialize with new client id - - auto app = utils::getApp(lastMediaSource); - - if (!app.enabled) { - Discord_ClearPresence(); - continue; - } - std::string serviceName = app.appName; - - std::string activityState = "by " + mediaInformation->songArtist; - DiscordRichPresence activity{}; - activity.type = ActivityType::LISTENING; - activity.details = mediaInformation->songTitle.c_str(); - activity.state = activityState.c_str(); - activity.smallImageText = serviceName.c_str(); - std::string artworkURL = utils::getArtworkURL(mediaInformation->songTitle + " " + mediaInformation->songArtist + - " " + mediaInformation->songAlbum); - - activity.smallImageKey = "appicon"; - if (artworkURL == "") { - activity.smallImageKey = ""; - activity.largeImageKey = "appicon"; - } else { - activity.largeImageKey = artworkURL.c_str(); - } - activity.largeImageText = mediaInformation->songAlbum.c_str(); - - if (mediaInformation->songDuration != 0) { - int64_t remainingTime = mediaInformation->songDuration - mediaInformation->songElapsedTime; - activity.startTimestamp = time(nullptr) - (mediaInformation->songElapsedTime / 1000); - activity.endTimestamp = time(nullptr) + (remainingTime / 1000); - } - std::string endpointURL = app.searchEndpoint; - - std::string searchQuery = mediaInformation->songTitle + " " + mediaInformation->songArtist; - std::string buttonName = "Search on " + serviceName; - std::string buttonText = endpointURL + utils::urlEncode(searchQuery); - - if (endpointURL != "") { - activity.button1name = buttonName.c_str(); - activity.button1link = buttonText.c_str(); - } - - Discord_UpdatePresence(&activity); - } -} -class PlayerLinkIcon : public wxTaskBarIcon { -public: - PlayerLinkIcon(wxFrame* s) : settingsFrame(s) {} - - void OnMenuOpen(wxCommandEvent& evt) { settingsFrame->Show(true); } - - void OnMenuExit(wxCommandEvent& evt) { settingsFrame->Close(true); } - - void OnMenuAbout(wxCommandEvent& evt) { - wxMessageBox(_("Made with <3 by EinTim"), _("PlayerLink"), wxOK | wxICON_INFORMATION); - } - -protected: - virtual wxMenu* CreatePopupMenu() override { - wxMenu* menu = new wxMenu; - menu->Append(10004, currentSongTitle == "" ? _("Not Playing") : wxString::FromUTF8(currentSongTitle)); - menu->Enable(10004, false); - menu->AppendSeparator(); - menu->Append(10001, _("Settings")); - menu->Append(10003, _("About PlayerLink")); - menu->AppendSeparator(); - menu->Append(10002, _("Quit PlayerLink...")); - Bind(wxEVT_MENU, &PlayerLinkIcon::OnMenuOpen, this, 10001); - Bind(wxEVT_MENU, &PlayerLinkIcon::OnMenuExit, this, 10002); - Bind(wxEVT_MENU, &PlayerLinkIcon::OnMenuAbout, this, 10003); - return menu; - } - -private: - wxFrame* settingsFrame; -}; - -class wxTextCtrlWithPlaceholder : public wxTextCtrl { -public: - wxTextCtrlWithPlaceholder(wxWindow* parent, wxWindowID id, const wxString& value, - const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, - long style = 0, const wxValidator& validator = wxDefaultValidator, - const wxString& name = "textCtrl") - : wxTextCtrl(parent, id, value, pos, size, style, validator, name), placeholder(""), showPlaceholder(true) { - Bind(wxEVT_SET_FOCUS, &wxTextCtrlWithPlaceholder::OnFocus, this); - Bind(wxEVT_KILL_FOCUS, &wxTextCtrlWithPlaceholder::OnBlur, this); - } - - void SetPlaceholderText(const wxString& p) { - placeholder = p; - SetValue(placeholder); - Refresh(); - } - -protected: - void OnFocus(wxFocusEvent& event) { - if (GetValue() == placeholder) - Clear(); - - showPlaceholder = false; - event.Skip(); - } - - void OnBlur(wxFocusEvent& event) { - if (GetValue().IsEmpty() || GetValue() == "") { - SetValue(placeholder); - showPlaceholder = true; - } - event.Skip(); - } - -private: - wxString placeholder; - bool showPlaceholder; -}; - -class PlayerLinkFrame : public wxFrame { -public: - PlayerLinkFrame(wxWindow* parent, wxIcon& icon, wxWindowID id = wxID_ANY, const wxString& title = wxEmptyString, - const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize(300, 200), - long style = wxDEFAULT_FRAME_STYLE & ~wxRESIZE_BORDER & ~wxMAXIMIZE_BOX) - : wxFrame(parent, id, title, pos, size, style) { - this->SetSizeHints(wxDefaultSize, wxDefaultSize); - this->SetIcon(icon); - - auto mainContainer = new wxBoxSizer(wxVERTICAL); - // header start - auto settingsText = new wxStaticText(this, wxID_ANY, _("Settings"), wxDefaultPosition, wxDefaultSize, 0); - settingsText->Wrap(-1); - mainContainer->Add(settingsText, 0, wxALIGN_CENTER | wxALL, 5); - // header end - - // enabled apps start - auto settingsDivider = new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL); - mainContainer->Add(settingsDivider, 0, wxEXPAND | wxALL, 5); - - wxBoxSizer* enabledAppsContainer; - enabledAppsContainer = new wxBoxSizer(wxHORIZONTAL); - - auto enabledAppsText = - new wxStaticText(this, wxID_ANY, _("Enabled Apps:"), wxDefaultPosition, wxDefaultSize, 0); - enabledAppsText->Wrap(-1); - enabledAppsContainer->Add(enabledAppsText, 0, wxALL, 5); - - wxBoxSizer* appCheckboxContainer; - appCheckboxContainer = new wxBoxSizer(wxVERTICAL); - - auto settings = utils::getSettings(); - - for (auto app : settings.apps) { - auto checkbox = new wxCheckBox(this, wxID_ANY, _(app.appName), wxDefaultPosition, wxDefaultSize, 0); - checkbox->SetValue(app.enabled); - checkbox->SetClientData(new utils::App(app)); - checkbox->Bind(wxEVT_CHECKBOX, [checkbox](wxCommandEvent& event) { - bool isChecked = checkbox->IsChecked(); - utils::App* appData = static_cast(checkbox->GetClientData()); - appData->enabled = isChecked; - utils::saveSettings(appData); - }); - checkbox->Bind(wxEVT_DESTROY, [checkbox](wxWindowDestroyEvent&) { - delete static_cast(checkbox->GetClientData()); - }); - appCheckboxContainer->Add(checkbox, 0, wxALL, 5); - } - - auto anyOtherCheckbox = new wxCheckBox(this, wxID_ANY, _("Any other"), wxDefaultPosition, wxDefaultSize, 0); - anyOtherCheckbox->SetValue(settings.anyOtherEnabled); - anyOtherCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) { - bool isChecked = event.IsChecked(); - auto settings = utils::getSettings(); - settings.anyOtherEnabled = isChecked; - utils::saveSettings(settings); - }); - - appCheckboxContainer->Add(anyOtherCheckbox, 0, wxALL, 5); - - enabledAppsContainer->Add(appCheckboxContainer, 1, wxEXPAND, 5); - - mainContainer->Add(enabledAppsContainer, 0, 0, 5); - // enabled apps end - - // LastFM start - auto lastfmDivider = new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL); - mainContainer->Add(lastfmDivider, 0, wxEXPAND | wxALL, 5); - - wxBoxSizer* lastFMContainer; - lastFMContainer = new wxBoxSizer(wxHORIZONTAL); - - auto lastfmText = new wxStaticText(this, wxID_ANY, _("LastFM:"), wxDefaultPosition, wxDefaultSize, 0); - lastfmText->Wrap(-1); - lastFMContainer->Add(lastfmText, 0, wxALL, 5); - - wxBoxSizer* lastfmSettingsContainer; - lastfmSettingsContainer = new wxBoxSizer(wxVERTICAL); - - auto lastfmEnabledCheckbox = new wxCheckBox(this, wxID_ANY, _("Enabled"), wxDefaultPosition, wxDefaultSize, 0); - lastfmEnabledCheckbox->SetValue(settings.lastfm.enabled); - lastfmEnabledCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) { - bool isChecked = event.IsChecked(); - auto settings = utils::getSettings(); - settings.lastfm.enabled = isChecked; - utils::saveSettings(settings); - }); - lastfmSettingsContainer->Add(lastfmEnabledCheckbox, 0, wxALIGN_CENTER | wxALL, 5); - lastFMContainer->Add(lastfmSettingsContainer, 1, wxEXPAND, 5); - - auto usernameInput = - new wxTextCtrlWithPlaceholder(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0); - usernameInput->SetPlaceholderText(_("Username")); - usernameInput->SetValue(settings.lastfm.username); - usernameInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) { - auto settings = utils::getSettings(); - std::string data = event.GetString().ToStdString(); - settings.lastfm.username = data; - utils::saveSettings(settings); - }); - auto passwordInput = new wxTextCtrlWithPlaceholder(this, wxID_ANY, wxEmptyString, wxDefaultPosition, - wxDefaultSize, wxTE_PASSWORD); - passwordInput->SetPlaceholderText(_("Password")); - passwordInput->SetValue(settings.lastfm.password); - passwordInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) { - auto settings = utils::getSettings(); - std::string data = event.GetString().ToStdString(); - settings.lastfm.password = data; - utils::saveSettings(settings); - }); - auto apikeyInput = - new wxTextCtrlWithPlaceholder(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0); - apikeyInput->SetPlaceholderText(_("API-Key")); - apikeyInput->SetValue(settings.lastfm.api_key); - apikeyInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) { - auto settings = utils::getSettings(); - std::string data = event.GetString().ToStdString(); - settings.lastfm.api_key = data; - utils::saveSettings(settings); - }); - auto apisecretInput = new wxTextCtrlWithPlaceholder(this, wxID_ANY, wxEmptyString, wxDefaultPosition, - wxDefaultSize, wxTE_PASSWORD); - apisecretInput->SetPlaceholderText(_("API-Secret")); - apisecretInput->SetValue(settings.lastfm.api_secret); - apisecretInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) { - auto settings = utils::getSettings(); - std::string data = event.GetString().ToStdString(); - settings.lastfm.api_secret = data; - utils::saveSettings(settings); - }); - - auto checkButton = new wxButton(this, wxID_ANY, _("Check credentials")); - checkButton->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { initLastFM(true); }); - mainContainer->Add(lastFMContainer, 0, 0, 5); - - mainContainer->Add(usernameInput, 0, wxEXPAND | wxALL, 5); - mainContainer->Add(passwordInput, 0, wxEXPAND | wxALL, 5); - mainContainer->Add(apikeyInput, 0, wxEXPAND | wxALL, 5); - mainContainer->Add(apisecretInput, 0, wxEXPAND | wxALL, 5); - mainContainer->Add(checkButton, 0, wxEXPAND | wxALL, 10); - - // Last FM End - - // settings start - auto appsDivider = new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL); - mainContainer->Add(appsDivider, 0, wxEXPAND | wxALL, 5); - - wxBoxSizer* settingsContainer; - settingsContainer = new wxBoxSizer(wxHORIZONTAL); - - auto startupText = new wxStaticText(this, wxID_ANY, _("Startup:"), wxDefaultPosition, wxDefaultSize, 0); - startupText->Wrap(-1); - settingsContainer->Add(startupText, 0, wxALL, 5); - - auto autostartCheckbox = - new wxCheckBox(this, wxID_ANY, _("Launch at login"), wxDefaultPosition, wxDefaultSize, 0); - autostartCheckbox->SetValue(settings.autoStart); - autostartCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) { - bool isChecked = event.IsChecked(); - auto settings = utils::getSettings(); - settings.autoStart = isChecked; - backend::toggleAutostart(isChecked); - utils::saveSettings(settings); - }); - - settingsContainer->Add(autostartCheckbox, 0, wxALL, 5); - mainContainer->Add(settingsContainer, 0, wxEXPAND, 5); - // settings end - this->SetSizerAndFit(mainContainer); - - wxSize currentSize = this->GetSize(); - this->SetSize(size.GetWidth(), currentSize.GetHeight()); - this->Layout(); - - this->Centre(wxBOTH); - } -}; -class PlayerLink : public wxApp { -public: - virtual bool OnInit() override { - if (!backend::init()) { - wxMessageBox(_("Error initializing platform backend!"), _("PlayerLink"), wxOK | wxICON_ERROR); - return false; - } - - if (wxSystemSettings::GetAppearance().IsSystemDark()) // To support the native dark mode on windows 10 and up - this->SetAppearance(wxAppBase::Appearance::Dark); - - wxInitAllImageHandlers(); - wxIcon icon = utils::loadIconFromMemory(icon_png, icon_png_size); - PlayerLinkFrame* frame = new PlayerLinkFrame(nullptr, icon, wxID_ANY, _("PlayerLink")); - trayIcon = new PlayerLinkIcon(frame); - frame->Bind(wxEVT_CLOSE_WINDOW, [=](wxCloseEvent& event) { - if (event.CanVeto()) { - frame->Hide(); - event.Veto(); - } else - std::exit(0); - }); - - trayIcon->SetIcon(icon, _("PlayerLink")); - return true; - } - -private: - PlayerLinkIcon* trayIcon; -}; - -wxIMPLEMENT_APP_NO_MAIN(PlayerLink); - -int main(int argc, char** argv) { - std::thread rpcThread(handleRPCTasks); - rpcThread.detach(); - std::thread mediaThread(handleMediaTasks); - mediaThread.detach(); - return wxEntry(argc, argv); +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "backend.hpp" +#include "lastfm.hpp" +#include "rsrc.hpp" +#include "utils.hpp" + +std::string lastPlayingSong = ""; +std::string lastMediaSource = ""; +std::string currentSongTitle = ""; +LastFM* lastfm = nullptr; + +void handleRPCTasks() { + while (true) { + while (true) { + DiscordEventHandlers discordHandler{}; + auto app = utils::getApp(lastMediaSource); + Discord_Initialize(app.clientId.c_str(), &discordHandler); + if (Discord_IsConnected()) + break; + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + while (true) { + Discord_RunCallbacks(); + if (!Discord_IsConnected()) + break; + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + Discord_Shutdown(); + } +} + +void initLastFM(bool checkMode = false) { + if (lastfm) + delete lastfm; + auto settings = utils::getSettings(); + if (!settings.lastfm.enabled && !checkMode) + return; + lastfm = new LastFM(settings.lastfm.username, settings.lastfm.password, settings.lastfm.api_key, + settings.lastfm.api_secret); + LastFM::LASTFM_STATUS status = lastfm->authenticate(); + if (status) + wxMessageBox(_("Error authenticating at LastFM!"), _("PlayerLink"), wxOK | wxICON_ERROR); + else if (checkMode) + wxMessageBox(_("The LastFM authentication was successful."), _("PlayerLink"), wxOK | wxICON_INFORMATION); +} + +void handleMediaTasks() { + initLastFM(); + int64_t lastMs = 0; + while (true) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + auto mediaInformation = backend::getMediaInformation(); + if (!mediaInformation) { + currentSongTitle = ""; + Discord_ClearPresence(); // Nothing is playing rn, clear presence + continue; + } + + if (mediaInformation->paused) { + lastMs = 0; + currentSongTitle = ""; + Discord_ClearPresence(); + continue; + } + + std::string currentlyPlayingSong = mediaInformation->songTitle + mediaInformation->songArtist + + mediaInformation->songAlbum + std::to_string(mediaInformation->songDuration); + int64_t currentMs = mediaInformation->songElapsedTime; + + bool shouldContinue = + currentlyPlayingSong == lastPlayingSong && (lastMs <= currentMs) && (lastMs + 3000 >= currentMs); + lastMs = currentMs; + + if (shouldContinue) + continue; + + if (lastPlayingSong.find(mediaInformation->songTitle + mediaInformation->songArtist + + mediaInformation->songAlbum) == std::string::npos && + lastfm) + lastfm->scrobble(mediaInformation->songArtist, mediaInformation->songTitle); + + lastPlayingSong = currentlyPlayingSong; + currentSongTitle = mediaInformation->songArtist + " - " + mediaInformation->songTitle; + + std::string currentMediaSource = mediaInformation->playbackSource; + + if (currentMediaSource != lastMediaSource) { + lastMediaSource = currentMediaSource; + Discord_Shutdown(); + } // reinitialize with new client id + + auto app = utils::getApp(lastMediaSource); + + if (!app.enabled) { + Discord_ClearPresence(); + continue; + } + std::string serviceName = app.appName; + + std::string activityState = "by " + mediaInformation->songArtist; + DiscordRichPresence activity{}; + activity.type = ActivityType::LISTENING; + activity.details = mediaInformation->songTitle.c_str(); + activity.state = activityState.c_str(); + activity.smallImageText = serviceName.c_str(); + std::string artworkURL = utils::getArtworkURL(mediaInformation->songTitle + " " + mediaInformation->songArtist + + " " + mediaInformation->songAlbum); + + activity.smallImageKey = "appicon"; + if (artworkURL == "") { + activity.smallImageKey = ""; + activity.largeImageKey = "appicon"; + } else { + activity.largeImageKey = artworkURL.c_str(); + } + activity.largeImageText = mediaInformation->songAlbum.c_str(); + + if (mediaInformation->songDuration != 0) { + int64_t remainingTime = mediaInformation->songDuration - mediaInformation->songElapsedTime; + activity.startTimestamp = time(nullptr) - (mediaInformation->songElapsedTime / 1000); + activity.endTimestamp = time(nullptr) + (remainingTime / 1000); + } + std::string endpointURL = app.searchEndpoint; + + std::string searchQuery = mediaInformation->songTitle + " " + mediaInformation->songArtist; + std::string buttonName = "Search on " + serviceName; + std::string buttonText = endpointURL + utils::urlEncode(searchQuery); + + if (endpointURL != "") { + activity.button1name = buttonName.c_str(); + activity.button1link = buttonText.c_str(); + } + + Discord_UpdatePresence(&activity); + } +} +class PlayerLinkIcon : public wxTaskBarIcon { +public: + PlayerLinkIcon(wxFrame* s) : settingsFrame(s) {} + + void OnMenuOpen(wxCommandEvent& evt) { + settingsFrame->Show(true); + settingsFrame->Raise(); + } + + void OnMenuExit(wxCommandEvent& evt) { settingsFrame->Close(true); } + + void OnMenuAbout(wxCommandEvent& evt) { + wxMessageBox(_("Made with <3 by EinTim"), _("PlayerLink"), wxOK | wxICON_INFORMATION); + } + +protected: + virtual wxMenu* CreatePopupMenu() override { + wxMenu* menu = new wxMenu; + menu->Append(10004, currentSongTitle == "" ? _("Not Playing") : wxString::FromUTF8(currentSongTitle)); + menu->Enable(10004, false); + menu->AppendSeparator(); + menu->Append(10001, _("Settings")); + menu->Append(10003, _("About PlayerLink")); + menu->AppendSeparator(); + menu->Append(10002, _("Quit PlayerLink...")); + Bind(wxEVT_MENU, &PlayerLinkIcon::OnMenuOpen, this, 10001); + Bind(wxEVT_MENU, &PlayerLinkIcon::OnMenuExit, this, 10002); + Bind(wxEVT_MENU, &PlayerLinkIcon::OnMenuAbout, this, 10003); + return menu; + } + +private: + wxFrame* settingsFrame; +}; + +class wxTextCtrlWithPlaceholder : public wxTextCtrl { +public: + wxTextCtrlWithPlaceholder(wxWindow* parent, wxWindowID id, const wxString& value, + const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, + long style = 0, const wxValidator& validator = wxDefaultValidator, + const wxString& name = "textCtrl") + : wxTextCtrl(parent, id, value, pos, size, style, validator, name), placeholder(""), showPlaceholder(true) { + Bind(wxEVT_SET_FOCUS, &wxTextCtrlWithPlaceholder::OnFocus, this); + Bind(wxEVT_KILL_FOCUS, &wxTextCtrlWithPlaceholder::OnBlur, this); + } + + void SetPlaceholderText(const wxString& p) { + placeholder = p; + SetValue(placeholder); + Refresh(); + } + +protected: + void OnFocus(wxFocusEvent& event) { + if (GetValue() == placeholder) + Clear(); + + showPlaceholder = false; + event.Skip(); + } + + void OnBlur(wxFocusEvent& event) { + if (GetValue().IsEmpty() || GetValue() == "") { + SetValue(placeholder); + showPlaceholder = true; + } + event.Skip(); + } + +private: + wxString placeholder; + bool showPlaceholder; +}; + +class PlayerLinkFrame : public wxFrame { +public: + PlayerLinkFrame(wxWindow* parent, wxIcon& icon, wxWindowID id = wxID_ANY, const wxString& title = wxEmptyString, + const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize(300, 200), + long style = wxDEFAULT_FRAME_STYLE & ~wxRESIZE_BORDER & ~wxMAXIMIZE_BOX) + : wxFrame(parent, id, title, pos, size, style) { + this->SetSizeHints(wxDefaultSize, wxDefaultSize); + this->SetIcon(icon); + + auto mainContainer = new wxBoxSizer(wxVERTICAL); + // header start + auto settingsText = new wxStaticText(this, wxID_ANY, _("Settings"), wxDefaultPosition, wxDefaultSize, 0); + settingsText->Wrap(-1); + mainContainer->Add(settingsText, 0, wxALIGN_CENTER | wxALL, 5); + // header end + + // enabled apps start + auto settingsDivider = new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL); + mainContainer->Add(settingsDivider, 0, wxEXPAND | wxALL, 5); + + wxBoxSizer* enabledAppsContainer; + enabledAppsContainer = new wxBoxSizer(wxHORIZONTAL); + + auto enabledAppsText = + new wxStaticText(this, wxID_ANY, _("Enabled Apps:"), wxDefaultPosition, wxDefaultSize, 0); + enabledAppsText->Wrap(-1); + enabledAppsContainer->Add(enabledAppsText, 0, wxALL, 5); + + wxBoxSizer* appCheckboxContainer; + appCheckboxContainer = new wxBoxSizer(wxVERTICAL); + + auto settings = utils::getSettings(); + + for (auto app : settings.apps) { + auto checkbox = new wxCheckBox(this, wxID_ANY, app.appName, wxDefaultPosition, wxDefaultSize, 0); + checkbox->SetValue(app.enabled); + checkbox->SetClientData(new utils::App(app)); + checkbox->Bind(wxEVT_CHECKBOX, [checkbox](wxCommandEvent& event) { + bool isChecked = checkbox->IsChecked(); + utils::App* appData = static_cast(checkbox->GetClientData()); + appData->enabled = isChecked; + utils::saveSettings(appData); + }); + checkbox->Bind(wxEVT_DESTROY, [checkbox](wxWindowDestroyEvent&) { + delete static_cast(checkbox->GetClientData()); + }); + appCheckboxContainer->Add(checkbox, 0, wxALL, 5); + } + + auto anyOtherCheckbox = new wxCheckBox(this, wxID_ANY, _("Any other"), wxDefaultPosition, wxDefaultSize, 0); + anyOtherCheckbox->SetValue(settings.anyOtherEnabled); + anyOtherCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) { + bool isChecked = event.IsChecked(); + auto settings = utils::getSettings(); + settings.anyOtherEnabled = isChecked; + utils::saveSettings(settings); + }); + + appCheckboxContainer->Add(anyOtherCheckbox, 0, wxALL, 5); + + enabledAppsContainer->Add(appCheckboxContainer, 1, wxEXPAND, 5); + + mainContainer->Add(enabledAppsContainer, 0, 0, 5); + // enabled apps end + + // LastFM start + auto lastfmDivider = new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL); + mainContainer->Add(lastfmDivider, 0, wxEXPAND | wxALL, 5); + + wxBoxSizer* lastFMContainer; + lastFMContainer = new wxBoxSizer(wxHORIZONTAL); + + auto lastfmText = new wxStaticText(this, wxID_ANY, _("LastFM:"), wxDefaultPosition, wxDefaultSize, 0); + lastfmText->Wrap(-1); + lastFMContainer->Add(lastfmText, 0, wxALL, 5); + + wxBoxSizer* lastfmSettingsContainer; + lastfmSettingsContainer = new wxBoxSizer(wxVERTICAL); + + auto lastfmEnabledCheckbox = new wxCheckBox(this, wxID_ANY, _("Enabled"), wxDefaultPosition, wxDefaultSize, 0); + lastfmEnabledCheckbox->SetValue(settings.lastfm.enabled); + lastfmEnabledCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) { + bool isChecked = event.IsChecked(); + auto settings = utils::getSettings(); + settings.lastfm.enabled = isChecked; + utils::saveSettings(settings); + }); + lastfmSettingsContainer->Add(lastfmEnabledCheckbox, 0, wxALIGN_CENTER | wxALL, 5); + lastFMContainer->Add(lastfmSettingsContainer, 1, wxEXPAND, 5); + + auto usernameInput = + new wxTextCtrlWithPlaceholder(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0); + usernameInput->SetPlaceholderText(_("Username")); + usernameInput->SetValue(settings.lastfm.username); + usernameInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) { + auto settings = utils::getSettings(); + std::string data = event.GetString().ToStdString(); + settings.lastfm.username = data; + utils::saveSettings(settings); + }); + auto passwordInput = new wxTextCtrlWithPlaceholder(this, wxID_ANY, wxEmptyString, wxDefaultPosition, + wxDefaultSize, wxTE_PASSWORD); + passwordInput->SetPlaceholderText(_("Password")); + passwordInput->SetValue(settings.lastfm.password); + passwordInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) { + auto settings = utils::getSettings(); + std::string data = event.GetString().ToStdString(); + settings.lastfm.password = data; + utils::saveSettings(settings); + }); + auto apikeyInput = + new wxTextCtrlWithPlaceholder(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0); + apikeyInput->SetPlaceholderText(_("API-Key")); + apikeyInput->SetValue(settings.lastfm.api_key); + apikeyInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) { + auto settings = utils::getSettings(); + std::string data = event.GetString().ToStdString(); + settings.lastfm.api_key = data; + utils::saveSettings(settings); + }); + auto apisecretInput = new wxTextCtrlWithPlaceholder(this, wxID_ANY, wxEmptyString, wxDefaultPosition, + wxDefaultSize, wxTE_PASSWORD); + apisecretInput->SetPlaceholderText(_("API-Secret")); + apisecretInput->SetValue(settings.lastfm.api_secret); + apisecretInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) { + auto settings = utils::getSettings(); + std::string data = event.GetString().ToStdString(); + settings.lastfm.api_secret = data; + utils::saveSettings(settings); + }); + + auto checkButton = new wxButton(this, wxID_ANY, _("Check credentials")); + checkButton->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { initLastFM(true); }); + mainContainer->Add(lastFMContainer, 0, 0, 5); + + mainContainer->Add(usernameInput, 0, wxEXPAND | wxALL, 5); + mainContainer->Add(passwordInput, 0, wxEXPAND | wxALL, 5); + mainContainer->Add(apikeyInput, 0, wxEXPAND | wxALL, 5); + mainContainer->Add(apisecretInput, 0, wxEXPAND | wxALL, 5); + mainContainer->Add(checkButton, 0, wxEXPAND | wxALL, 10); + + // Last FM End + + // settings start + auto appsDivider = new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL); + mainContainer->Add(appsDivider, 0, wxEXPAND | wxALL, 5); + + wxBoxSizer* settingsContainer; + settingsContainer = new wxBoxSizer(wxHORIZONTAL); + + auto startupText = new wxStaticText(this, wxID_ANY, _("Startup:"), wxDefaultPosition, wxDefaultSize, 0); + startupText->Wrap(-1); + settingsContainer->Add(startupText, 0, wxALL, 5); + + auto autostartCheckbox = + new wxCheckBox(this, wxID_ANY, _("Launch at login"), wxDefaultPosition, wxDefaultSize, 0); + autostartCheckbox->SetValue(settings.autoStart); + autostartCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) { + bool isChecked = event.IsChecked(); + auto settings = utils::getSettings(); + settings.autoStart = isChecked; + backend::toggleAutostart(isChecked); + utils::saveSettings(settings); + }); + + settingsContainer->Add(autostartCheckbox, 0, wxALL, 5); + mainContainer->Add(settingsContainer, 0, wxEXPAND, 5); + // settings end + this->SetSizerAndFit(mainContainer); + + wxSize currentSize = this->GetSize(); + this->SetSize(size.GetWidth(), currentSize.GetHeight()); + this->Layout(); + + this->Centre(wxBOTH); + } +}; +class PlayerLink : public wxApp { +public: + virtual bool OnInit() override { + if (!backend::init()) { + wxMessageBox(_("Error initializing platform backend!"), _("PlayerLink"), wxOK | wxICON_ERROR); + return false; + } + + if (wxSystemSettings::GetAppearance().IsSystemDark()) // To support the native dark mode on windows 10 and up + this->SetAppearance(wxAppBase::Appearance::Dark); + + wxInitAllImageHandlers(); + wxIcon icon = utils::loadIconFromMemory(icon_png, icon_png_size); + PlayerLinkFrame* frame = new PlayerLinkFrame(nullptr, icon, wxID_ANY, _("PlayerLink")); + trayIcon = new PlayerLinkIcon(frame); + frame->Bind(wxEVT_CLOSE_WINDOW, [=](wxCloseEvent& event) { + if (event.CanVeto()) { + frame->Hide(); + event.Veto(); + } else + std::exit(0); + }); + + trayIcon->SetIcon(icon, _("PlayerLink")); + return true; + } + +private: + PlayerLinkIcon* trayIcon; +}; + +wxIMPLEMENT_APP_NO_MAIN(PlayerLink); + +int main(int argc, char** argv) { + std::thread rpcThread(handleRPCTasks); + rpcThread.detach(); + std::thread mediaThread(handleMediaTasks); + mediaThread.detach(); + return wxEntry(argc, argv); } \ No newline at end of file diff --git a/vendor/wxWidgets b/vendor/wxWidgets index 12b09a5..138937b 160000 --- a/vendor/wxWidgets +++ b/vendor/wxWidgets @@ -1 +1 @@ -Subproject commit 12b09a5e5ea76a1a0c27b769e821b37d803a4cb7 +Subproject commit 138937b7775c117b57f55374a0c507a35a1102f6