- Fix: settings window not auto focussing on mac os

- Optimized the windows backend
- Updated dependencies
This commit is contained in:
EinTim23 2025-01-11 15:50:39 +01:00
parent 4a68eba106
commit d13231e84e
4 changed files with 655 additions and 641 deletions

View File

@ -1,10 +1,11 @@
BasedOnStyle: Google BasedOnStyle: Google
UseTab: Never UseTab: Never
IndentWidth: 4 IndentWidth: 4
TabWidth: 4 TabWidth: 4
BreakBeforeBraces: Attach BreakBeforeBraces: Attach
AllowShortIfStatementsOnASingleLine: false AllowShortIfStatementsOnASingleLine: false
IndentCaseLabels: false IndentCaseLabels: false
AccessModifierOffset: -4 AccessModifierOffset: -4
ColumnLimit: 120 ColumnLimit: 120
NamespaceIndentation: All NamespaceIndentation: All
SortIncludes: false

View File

@ -1,198 +1,208 @@
#ifdef _WIN32 #ifdef _WIN32
#define WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN
#include <objbase.h> #include <objbase.h>
#include <psapi.h> #include <psapi.h>
#include <shlobj.h> #include <shlobj.h>
#include <windows.h> #include <windows.h>
#include <appmodel.h> #include <appmodel.h>
#include <winrt/windows.foundation.h> #include <winrt/windows.foundation.h>
#include <winrt/windows.foundation.metadata.h> #include <winrt/windows.foundation.metadata.h>
#include <winrt/windows.media.control.h> #include <winrt/windows.media.control.h>
#include <winrt/windows.storage.streams.h> #include <winrt/windows.storage.streams.h>
#include <chrono> #include <chrono>
#include <codecvt> #include <codecvt>
#include <filesystem> #include <filesystem>
#include "../backend.hpp" #include "../backend.hpp"
#include "../utils.hpp" #include "../utils.hpp"
using namespace winrt; using namespace winrt;
using namespace Windows::Media::Control; using namespace Windows::Media::Control;
using namespace Windows::Storage::Streams; using namespace Windows::Storage::Streams;
#define EM_DASH "\xE2\x80\x94" #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::string toStdString(winrt::hstring in) { const wchar_t* wideStr = in.c_str();
std::wstring_convert<std::codecvt_utf8<wchar_t>> converter; int wideStrLen = static_cast<int>(in.size());
return converter.to_bytes(in.c_str()); int bufferSize = WideCharToMultiByte(CP_UTF8, 0, wideStr, wideStrLen, nullptr, 0, nullptr, nullptr);
} if (bufferSize <= 0)
return "";
std::string getAppModelIdOfProcess(HANDLE hProc) {
UINT32 length = 0; std::string result(bufferSize, 0);
LONG rc = GetApplicationUserModelId(hProc, &length, NULL);
if (rc != ERROR_INSUFFICIENT_BUFFER) WideCharToMultiByte(CP_UTF8, 0, wideStr, wideStrLen, result.data(), bufferSize, nullptr, nullptr);
return "";
return result;
PWSTR fullName = (PWSTR)malloc(length * sizeof(*fullName)); }
if (!fullName)
return ""; std::string getAppModelIdOfProcess(HANDLE hProc) {
UINT32 length = 0;
rc = GetApplicationUserModelId(hProc, &length, fullName); LONG rc = GetApplicationUserModelId(hProc, &length, NULL);
if (rc != ERROR_SUCCESS) { if (rc != ERROR_INSUFFICIENT_BUFFER)
free(fullName); return "";
return "";
} PWSTR fullName = (PWSTR)malloc(length * sizeof(*fullName));
std::string name = toStdString(fullName); if (!fullName)
free(fullName); return "";
return name;
} rc = GetApplicationUserModelId(hProc, &length, fullName);
if (rc != ERROR_SUCCESS) {
std::string getProcessNameFromAppModelId(std::string appModelId) { free(fullName);
DWORD processes[1024]; return "";
DWORD cbNeeded; }
winrt::hstring wideName = fullName;
if (!EnumProcesses(processes, sizeof(processes), &cbNeeded)) std::string name = toStdString(wideName);
return ""; free(fullName);
return name;
unsigned int processCount = cbNeeded / sizeof(DWORD); }
for (DWORD i = 0; i < processCount; i++) { std::string getProcessNameFromAppModelId(std::string appModelId) {
DWORD processID = processes[i]; DWORD processes[1024];
DWORD cbNeeded;
HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processID);
if (hProcess) { if (!EnumProcesses(processes, sizeof(processes), &cbNeeded))
std::string modelid = getAppModelIdOfProcess(hProcess); return "";
if (modelid != appModelId) { unsigned int processCount = cbNeeded / sizeof(DWORD);
CloseHandle(hProcess);
continue; for (DWORD i = 0; i < processCount; i++) {
} DWORD processID = processes[i];
char exeName[MAX_PATH]{}; HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processID);
DWORD size = MAX_PATH; if (hProcess) {
QueryFullProcessImageNameA(hProcess, 0, exeName, &size); std::string modelid = getAppModelIdOfProcess(hProcess);
std::filesystem::path exePath = exeName;
CloseHandle(hProcess); if (modelid != appModelId) {
return exePath.filename().string(); CloseHandle(hProcess);
} continue;
} }
return "";
} char exeName[MAX_PATH]{};
DWORD size = MAX_PATH;
bool CreateShortcut(std::string source, std::string target) { QueryFullProcessImageNameA(hProcess, 0, exeName, &size);
CoInitialize(nullptr); std::filesystem::path exePath = exeName;
WCHAR src[MAX_PATH]; CloseHandle(hProcess);
IShellLinkW* pShellLink = nullptr; return exePath.filename().string();
HRESULT hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_IShellLink, }
reinterpret_cast<void**>(&pShellLink)); }
return "";
if (SUCCEEDED(hr) && pShellLink) { }
MultiByteToWideChar(CP_ACP, 0, source.c_str(), -1, src, MAX_PATH);
pShellLink->SetPath(src); bool CreateShortcut(std::string source, std::string target) {
CoInitialize(nullptr);
IPersistFile* pPersistFile = nullptr; WCHAR src[MAX_PATH];
hr = pShellLink->QueryInterface(IID_IPersistFile, reinterpret_cast<void**>(&pPersistFile)); IShellLinkW* pShellLink = nullptr;
HRESULT hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_IShellLink,
if (SUCCEEDED(hr) && pPersistFile) { reinterpret_cast<void**>(&pShellLink));
WCHAR dst[MAX_PATH];
MultiByteToWideChar(CP_ACP, 0, target.c_str(), -1, dst, MAX_PATH); if (SUCCEEDED(hr) && pShellLink) {
hr = pPersistFile->Save(dst, TRUE); MultiByteToWideChar(CP_ACP, 0, source.c_str(), -1, src, MAX_PATH);
pPersistFile->Release(); pShellLink->SetPath(src);
}
IPersistFile* pPersistFile = nullptr;
pShellLink->Release(); hr = pShellLink->QueryInterface(IID_IPersistFile, reinterpret_cast<void**>(&pPersistFile));
}
if (SUCCEEDED(hr) && pPersistFile) {
CoUninitialize(); WCHAR dst[MAX_PATH];
return SUCCEEDED(hr); MultiByteToWideChar(CP_ACP, 0, target.c_str(), -1, dst, MAX_PATH);
} hr = pPersistFile->Save(dst, TRUE);
pPersistFile->Release();
std::filesystem::path backend::getConfigDirectory() { }
std::filesystem::path configDirectoryPath = std::getenv("APPDATA");
configDirectoryPath = configDirectoryPath / "PlayerLink"; pShellLink->Release();
return configDirectoryPath; }
}
CoUninitialize();
bool backend::toggleAutostart(bool enabled) { return SUCCEEDED(hr);
std::filesystem::path shortcutPath = std::getenv("APPDATA"); }
shortcutPath = shortcutPath / "Microsoft" / "Windows" / "Start Menu" / "Programs" / "Startup";
std::filesystem::create_directories(shortcutPath); std::filesystem::path backend::getConfigDirectory() {
shortcutPath = shortcutPath / "PlayerLink.lnk"; std::filesystem::path configDirectoryPath = std::getenv("APPDATA");
configDirectoryPath = configDirectoryPath / "PlayerLink";
if (!enabled && std::filesystem::exists(shortcutPath)) { return configDirectoryPath;
std::filesystem::remove(shortcutPath); }
return true;
} bool backend::toggleAutostart(bool enabled) {
char binaryPath[MAX_PATH]{}; std::filesystem::path shortcutPath = std::getenv("APPDATA");
GetModuleFileNameA(NULL, binaryPath, MAX_PATH); shortcutPath = shortcutPath / "Microsoft" / "Windows" / "Start Menu" / "Programs" / "Startup";
bool result = CreateShortcut(binaryPath, shortcutPath.string()); std::filesystem::create_directories(shortcutPath);
return result; shortcutPath = shortcutPath / "PlayerLink.lnk";
}
if (!enabled && std::filesystem::exists(shortcutPath)) {
std::shared_ptr<MediaInfo> backend::getMediaInformation() { std::filesystem::remove(shortcutPath);
auto sessionManager = GlobalSystemMediaTransportControlsSessionManager::RequestAsync().get(); return true;
auto currentSession = sessionManager.GetCurrentSession(); }
if (!currentSession) char binaryPath[MAX_PATH]{};
return nullptr; GetModuleFileNameA(NULL, binaryPath, MAX_PATH);
bool result = CreateShortcut(binaryPath, shortcutPath.string());
auto playbackInfo = currentSession.GetPlaybackInfo(); return result;
auto mediaProperties = currentSession.TryGetMediaPropertiesAsync().get(); }
auto timelineInformation = currentSession.GetTimelineProperties();
if (!mediaProperties) std::shared_ptr<MediaInfo> backend::getMediaInformation() {
return nullptr; static auto sessionManager = GlobalSystemMediaTransportControlsSessionManager::RequestAsync().get();
auto currentSession = sessionManager.GetCurrentSession();
auto endTime = std::chrono::duration_cast<std::chrono::milliseconds>(timelineInformation.EndTime()).count(); if (!currentSession)
auto elapsedTime = std::chrono::duration_cast<std::chrono::milliseconds>(timelineInformation.Position()).count(); return nullptr;
auto thumbnail = mediaProperties.Thumbnail(); auto playbackInfo = currentSession.GetPlaybackInfo();
std::string thumbnailData = ""; auto mediaProperties = currentSession.TryGetMediaPropertiesAsync().get();
auto timelineInformation = currentSession.GetTimelineProperties();
if (thumbnail) { if (!mediaProperties)
auto stream = thumbnail.OpenReadAsync().get(); return nullptr;
size_t size = static_cast<size_t>(stream.Size());
auto endTime = std::chrono::duration_cast<std::chrono::milliseconds>(timelineInformation.EndTime()).count();
DataReader reader(stream); auto elapsedTime = std::chrono::duration_cast<std::chrono::milliseconds>(timelineInformation.Position()).count();
reader.LoadAsync(static_cast<uint32_t>(size)).get();
auto thumbnail = mediaProperties.Thumbnail();
std::vector<uint8_t> buffer(size); std::string thumbnailData = "";
reader.ReadBytes(buffer);
reader.Close(); if (thumbnail) {
auto stream = thumbnail.OpenReadAsync().get();
thumbnailData = std::string(buffer.begin(), buffer.end()); size_t size = static_cast<size_t>(stream.Size());
stream.Close();
} DataReader reader(stream);
reader.LoadAsync(static_cast<uint32_t>(size)).get();
std::string artist = toStdString(mediaProperties.Artist());
std::string albumName = toStdString(mediaProperties.AlbumTitle()); std::vector<uint8_t> buffer(size);
if (artist == "") reader.ReadBytes(buffer);
artist = toStdString(mediaProperties.AlbumArtist()); // Needed for some apps reader.Close();
if (artist.find(EM_DASH) != std::string::npos) { thumbnailData = std::string(buffer.begin(), buffer.end());
albumName = artist.substr(artist.find(EM_DASH) + 3); stream.Close();
artist = artist.substr(0, artist.find(EM_DASH)); }
utils::trim(artist);
utils::trim(albumName); std::string artist = toStdString(mediaProperties.Artist());
} std::string albumName = toStdString(mediaProperties.AlbumTitle());
if (artist == "")
std::string modelId = toStdString(currentSession.SourceAppUserModelId()); artist = toStdString(mediaProperties.AlbumArtist()); // Needed for some apps
// I do know that this is disgusting, but for some reason microsoft decided to switch out the exe name with the if (artist.find(EM_DASH) != std::string::npos) {
// ApplicationUserModelId in some version of windows 11. So we check if it's an exe name, if not we are on some albumName = artist.substr(artist.find(EM_DASH) + 3);
// newer windows version and need to get the exe name from the model id. We cannot directly work with the model id artist = artist.substr(0, artist.find(EM_DASH));
// because it's unique per machine and therefore would mess up configs with preconfigured apps. utils::trim(artist);
if (modelId.find(".exe") == std::string::npos) utils::trim(albumName);
modelId = getProcessNameFromAppModelId(modelId); }
return std::make_shared<MediaInfo>( std::string modelId = toStdString(currentSession.SourceAppUserModelId());
playbackInfo.PlaybackStatus() == GlobalSystemMediaTransportControlsSessionPlaybackStatus::Paused,
toStdString(mediaProperties.Title()), artist, albumName, modelId, thumbnailData, endTime, elapsedTime); // 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
bool backend::init() { // because it's unique per machine and therefore would mess up configs with preconfigured apps.
return winrt::Windows::Foundation::Metadata::ApiInformation::IsTypePresent( if (modelId.find(".exe") == std::string::npos)
L"Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager"); modelId = getProcessNameFromAppModelId(modelId);
}
return std::make_shared<MediaInfo>(
#undef EM_DASH playbackInfo.PlaybackStatus() == GlobalSystemMediaTransportControlsSessionPlaybackStatus::Paused,
#endif 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

View File

@ -1,433 +1,436 @@
#include <discord-rpc/discord_rpc.h> #include <discord-rpc/discord_rpc.h>
#include <wx/image.h> #include <wx/image.h>
#include <wx/mstream.h> #include <wx/mstream.h>
#include <wx/statline.h> #include <wx/statline.h>
#include <wx/taskbar.h> #include <wx/taskbar.h>
#include <wx/wx.h> #include <wx/wx.h>
#include <chrono> #include <chrono>
#include <thread> #include <thread>
#include "backend.hpp" #include "backend.hpp"
#include "lastfm.hpp" #include "lastfm.hpp"
#include "rsrc.hpp" #include "rsrc.hpp"
#include "utils.hpp" #include "utils.hpp"
std::string lastPlayingSong = ""; std::string lastPlayingSong = "";
std::string lastMediaSource = ""; std::string lastMediaSource = "";
std::string currentSongTitle = ""; std::string currentSongTitle = "";
LastFM* lastfm = nullptr; LastFM* lastfm = nullptr;
void handleRPCTasks() { void handleRPCTasks() {
while (true) { while (true) {
while (true) { while (true) {
DiscordEventHandlers discordHandler{}; DiscordEventHandlers discordHandler{};
auto app = utils::getApp(lastMediaSource); auto app = utils::getApp(lastMediaSource);
Discord_Initialize(app.clientId.c_str(), &discordHandler); Discord_Initialize(app.clientId.c_str(), &discordHandler);
if (Discord_IsConnected()) if (Discord_IsConnected())
break; break;
std::this_thread::sleep_for(std::chrono::seconds(1)); std::this_thread::sleep_for(std::chrono::seconds(1));
} }
while (true) { while (true) {
Discord_RunCallbacks(); Discord_RunCallbacks();
if (!Discord_IsConnected()) if (!Discord_IsConnected())
break; break;
std::this_thread::sleep_for(std::chrono::seconds(1)); std::this_thread::sleep_for(std::chrono::seconds(1));
} }
Discord_Shutdown(); Discord_Shutdown();
} }
} }
void initLastFM(bool checkMode = false) { void initLastFM(bool checkMode = false) {
if (lastfm) if (lastfm)
delete lastfm; delete lastfm;
auto settings = utils::getSettings(); auto settings = utils::getSettings();
if (!settings.lastfm.enabled && !checkMode) if (!settings.lastfm.enabled && !checkMode)
return; return;
lastfm = new LastFM(settings.lastfm.username, settings.lastfm.password, settings.lastfm.api_key, lastfm = new LastFM(settings.lastfm.username, settings.lastfm.password, settings.lastfm.api_key,
settings.lastfm.api_secret); settings.lastfm.api_secret);
LastFM::LASTFM_STATUS status = lastfm->authenticate(); LastFM::LASTFM_STATUS status = lastfm->authenticate();
if (status) if (status)
wxMessageBox(_("Error authenticating at LastFM!"), _("PlayerLink"), wxOK | wxICON_ERROR); wxMessageBox(_("Error authenticating at LastFM!"), _("PlayerLink"), wxOK | wxICON_ERROR);
else if (checkMode) else if (checkMode)
wxMessageBox(_("The LastFM authentication was successful."), _("PlayerLink"), wxOK | wxICON_INFORMATION); wxMessageBox(_("The LastFM authentication was successful."), _("PlayerLink"), wxOK | wxICON_INFORMATION);
} }
void handleMediaTasks() { void handleMediaTasks() {
initLastFM(); initLastFM();
int64_t lastMs = 0; int64_t lastMs = 0;
while (true) { while (true) {
std::this_thread::sleep_for(std::chrono::seconds(1)); std::this_thread::sleep_for(std::chrono::seconds(1));
auto mediaInformation = backend::getMediaInformation(); auto mediaInformation = backend::getMediaInformation();
if (!mediaInformation) { if (!mediaInformation) {
currentSongTitle = ""; currentSongTitle = "";
Discord_ClearPresence(); // Nothing is playing rn, clear presence Discord_ClearPresence(); // Nothing is playing rn, clear presence
continue; continue;
} }
if (mediaInformation->paused) { if (mediaInformation->paused) {
lastMs = 0; lastMs = 0;
currentSongTitle = ""; currentSongTitle = "";
Discord_ClearPresence(); Discord_ClearPresence();
continue; continue;
} }
std::string currentlyPlayingSong = mediaInformation->songTitle + mediaInformation->songArtist + std::string currentlyPlayingSong = mediaInformation->songTitle + mediaInformation->songArtist +
mediaInformation->songAlbum + std::to_string(mediaInformation->songDuration); mediaInformation->songAlbum + std::to_string(mediaInformation->songDuration);
int64_t currentMs = mediaInformation->songElapsedTime; int64_t currentMs = mediaInformation->songElapsedTime;
bool shouldContinue = bool shouldContinue =
currentlyPlayingSong == lastPlayingSong && (lastMs <= currentMs) && (lastMs + 3000 >= currentMs); currentlyPlayingSong == lastPlayingSong && (lastMs <= currentMs) && (lastMs + 3000 >= currentMs);
lastMs = currentMs; lastMs = currentMs;
if (shouldContinue) if (shouldContinue)
continue; continue;
if (lastPlayingSong.find(mediaInformation->songTitle + mediaInformation->songArtist + if (lastPlayingSong.find(mediaInformation->songTitle + mediaInformation->songArtist +
mediaInformation->songAlbum) == std::string::npos && mediaInformation->songAlbum) == std::string::npos &&
lastfm) lastfm)
lastfm->scrobble(mediaInformation->songArtist, mediaInformation->songTitle); lastfm->scrobble(mediaInformation->songArtist, mediaInformation->songTitle);
lastPlayingSong = currentlyPlayingSong; lastPlayingSong = currentlyPlayingSong;
currentSongTitle = mediaInformation->songArtist + " - " + mediaInformation->songTitle; currentSongTitle = mediaInformation->songArtist + " - " + mediaInformation->songTitle;
std::string currentMediaSource = mediaInformation->playbackSource; std::string currentMediaSource = mediaInformation->playbackSource;
if (currentMediaSource != lastMediaSource) { if (currentMediaSource != lastMediaSource) {
lastMediaSource = currentMediaSource; lastMediaSource = currentMediaSource;
Discord_Shutdown(); Discord_Shutdown();
} // reinitialize with new client id } // reinitialize with new client id
auto app = utils::getApp(lastMediaSource); auto app = utils::getApp(lastMediaSource);
if (!app.enabled) { if (!app.enabled) {
Discord_ClearPresence(); Discord_ClearPresence();
continue; continue;
} }
std::string serviceName = app.appName; std::string serviceName = app.appName;
std::string activityState = "by " + mediaInformation->songArtist; std::string activityState = "by " + mediaInformation->songArtist;
DiscordRichPresence activity{}; DiscordRichPresence activity{};
activity.type = ActivityType::LISTENING; activity.type = ActivityType::LISTENING;
activity.details = mediaInformation->songTitle.c_str(); activity.details = mediaInformation->songTitle.c_str();
activity.state = activityState.c_str(); activity.state = activityState.c_str();
activity.smallImageText = serviceName.c_str(); activity.smallImageText = serviceName.c_str();
std::string artworkURL = utils::getArtworkURL(mediaInformation->songTitle + " " + mediaInformation->songArtist + std::string artworkURL = utils::getArtworkURL(mediaInformation->songTitle + " " + mediaInformation->songArtist +
" " + mediaInformation->songAlbum); " " + mediaInformation->songAlbum);
activity.smallImageKey = "appicon"; activity.smallImageKey = "appicon";
if (artworkURL == "") { if (artworkURL == "") {
activity.smallImageKey = ""; activity.smallImageKey = "";
activity.largeImageKey = "appicon"; activity.largeImageKey = "appicon";
} else { } else {
activity.largeImageKey = artworkURL.c_str(); activity.largeImageKey = artworkURL.c_str();
} }
activity.largeImageText = mediaInformation->songAlbum.c_str(); activity.largeImageText = mediaInformation->songAlbum.c_str();
if (mediaInformation->songDuration != 0) { if (mediaInformation->songDuration != 0) {
int64_t remainingTime = mediaInformation->songDuration - mediaInformation->songElapsedTime; int64_t remainingTime = mediaInformation->songDuration - mediaInformation->songElapsedTime;
activity.startTimestamp = time(nullptr) - (mediaInformation->songElapsedTime / 1000); activity.startTimestamp = time(nullptr) - (mediaInformation->songElapsedTime / 1000);
activity.endTimestamp = time(nullptr) + (remainingTime / 1000); activity.endTimestamp = time(nullptr) + (remainingTime / 1000);
} }
std::string endpointURL = app.searchEndpoint; std::string endpointURL = app.searchEndpoint;
std::string searchQuery = mediaInformation->songTitle + " " + mediaInformation->songArtist; std::string searchQuery = mediaInformation->songTitle + " " + mediaInformation->songArtist;
std::string buttonName = "Search on " + serviceName; std::string buttonName = "Search on " + serviceName;
std::string buttonText = endpointURL + utils::urlEncode(searchQuery); std::string buttonText = endpointURL + utils::urlEncode(searchQuery);
if (endpointURL != "") { if (endpointURL != "") {
activity.button1name = buttonName.c_str(); activity.button1name = buttonName.c_str();
activity.button1link = buttonText.c_str(); activity.button1link = buttonText.c_str();
} }
Discord_UpdatePresence(&activity); Discord_UpdatePresence(&activity);
} }
} }
class PlayerLinkIcon : public wxTaskBarIcon { class PlayerLinkIcon : public wxTaskBarIcon {
public: public:
PlayerLinkIcon(wxFrame* s) : settingsFrame(s) {} PlayerLinkIcon(wxFrame* s) : settingsFrame(s) {}
void OnMenuOpen(wxCommandEvent& evt) { settingsFrame->Show(true); } void OnMenuOpen(wxCommandEvent& evt) {
settingsFrame->Show(true);
void OnMenuExit(wxCommandEvent& evt) { settingsFrame->Close(true); } settingsFrame->Raise();
}
void OnMenuAbout(wxCommandEvent& evt) {
wxMessageBox(_("Made with <3 by EinTim"), _("PlayerLink"), wxOK | wxICON_INFORMATION); void OnMenuExit(wxCommandEvent& evt) { settingsFrame->Close(true); }
}
void OnMenuAbout(wxCommandEvent& evt) {
protected: wxMessageBox(_("Made with <3 by EinTim"), _("PlayerLink"), wxOK | wxICON_INFORMATION);
virtual wxMenu* CreatePopupMenu() override { }
wxMenu* menu = new wxMenu;
menu->Append(10004, currentSongTitle == "" ? _("Not Playing") : wxString::FromUTF8(currentSongTitle)); protected:
menu->Enable(10004, false); virtual wxMenu* CreatePopupMenu() override {
menu->AppendSeparator(); wxMenu* menu = new wxMenu;
menu->Append(10001, _("Settings")); menu->Append(10004, currentSongTitle == "" ? _("Not Playing") : wxString::FromUTF8(currentSongTitle));
menu->Append(10003, _("About PlayerLink")); menu->Enable(10004, false);
menu->AppendSeparator(); menu->AppendSeparator();
menu->Append(10002, _("Quit PlayerLink...")); menu->Append(10001, _("Settings"));
Bind(wxEVT_MENU, &PlayerLinkIcon::OnMenuOpen, this, 10001); menu->Append(10003, _("About PlayerLink"));
Bind(wxEVT_MENU, &PlayerLinkIcon::OnMenuExit, this, 10002); menu->AppendSeparator();
Bind(wxEVT_MENU, &PlayerLinkIcon::OnMenuAbout, this, 10003); menu->Append(10002, _("Quit PlayerLink..."));
return menu; Bind(wxEVT_MENU, &PlayerLinkIcon::OnMenuOpen, this, 10001);
} Bind(wxEVT_MENU, &PlayerLinkIcon::OnMenuExit, this, 10002);
Bind(wxEVT_MENU, &PlayerLinkIcon::OnMenuAbout, this, 10003);
private: return menu;
wxFrame* settingsFrame; }
};
private:
class wxTextCtrlWithPlaceholder : public wxTextCtrl { wxFrame* settingsFrame;
public: };
wxTextCtrlWithPlaceholder(wxWindow* parent, wxWindowID id, const wxString& value,
const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, class wxTextCtrlWithPlaceholder : public wxTextCtrl {
long style = 0, const wxValidator& validator = wxDefaultValidator, public:
const wxString& name = "textCtrl") wxTextCtrlWithPlaceholder(wxWindow* parent, wxWindowID id, const wxString& value,
: wxTextCtrl(parent, id, value, pos, size, style, validator, name), placeholder(""), showPlaceholder(true) { const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize,
Bind(wxEVT_SET_FOCUS, &wxTextCtrlWithPlaceholder::OnFocus, this); long style = 0, const wxValidator& validator = wxDefaultValidator,
Bind(wxEVT_KILL_FOCUS, &wxTextCtrlWithPlaceholder::OnBlur, this); const wxString& name = "textCtrl")
} : wxTextCtrl(parent, id, value, pos, size, style, validator, name), placeholder(""), showPlaceholder(true) {
Bind(wxEVT_SET_FOCUS, &wxTextCtrlWithPlaceholder::OnFocus, this);
void SetPlaceholderText(const wxString& p) { Bind(wxEVT_KILL_FOCUS, &wxTextCtrlWithPlaceholder::OnBlur, this);
placeholder = p; }
SetValue(placeholder);
Refresh(); void SetPlaceholderText(const wxString& p) {
} placeholder = p;
SetValue(placeholder);
protected: Refresh();
void OnFocus(wxFocusEvent& event) { }
if (GetValue() == placeholder)
Clear(); protected:
void OnFocus(wxFocusEvent& event) {
showPlaceholder = false; if (GetValue() == placeholder)
event.Skip(); Clear();
}
showPlaceholder = false;
void OnBlur(wxFocusEvent& event) { event.Skip();
if (GetValue().IsEmpty() || GetValue() == "") { }
SetValue(placeholder);
showPlaceholder = true; void OnBlur(wxFocusEvent& event) {
} if (GetValue().IsEmpty() || GetValue() == "") {
event.Skip(); SetValue(placeholder);
} showPlaceholder = true;
}
private: event.Skip();
wxString placeholder; }
bool showPlaceholder;
}; private:
wxString placeholder;
class PlayerLinkFrame : public wxFrame { bool showPlaceholder;
public: };
PlayerLinkFrame(wxWindow* parent, wxIcon& icon, wxWindowID id = wxID_ANY, const wxString& title = wxEmptyString,
const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize(300, 200), class PlayerLinkFrame : public wxFrame {
long style = wxDEFAULT_FRAME_STYLE & ~wxRESIZE_BORDER & ~wxMAXIMIZE_BOX) public:
: wxFrame(parent, id, title, pos, size, style) { PlayerLinkFrame(wxWindow* parent, wxIcon& icon, wxWindowID id = wxID_ANY, const wxString& title = wxEmptyString,
this->SetSizeHints(wxDefaultSize, wxDefaultSize); const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize(300, 200),
this->SetIcon(icon); long style = wxDEFAULT_FRAME_STYLE & ~wxRESIZE_BORDER & ~wxMAXIMIZE_BOX)
: wxFrame(parent, id, title, pos, size, style) {
auto mainContainer = new wxBoxSizer(wxVERTICAL); this->SetSizeHints(wxDefaultSize, wxDefaultSize);
// header start this->SetIcon(icon);
auto settingsText = new wxStaticText(this, wxID_ANY, _("Settings"), wxDefaultPosition, wxDefaultSize, 0);
settingsText->Wrap(-1); auto mainContainer = new wxBoxSizer(wxVERTICAL);
mainContainer->Add(settingsText, 0, wxALIGN_CENTER | wxALL, 5); // header start
// header end auto settingsText = new wxStaticText(this, wxID_ANY, _("Settings"), wxDefaultPosition, wxDefaultSize, 0);
settingsText->Wrap(-1);
// enabled apps start mainContainer->Add(settingsText, 0, wxALIGN_CENTER | wxALL, 5);
auto settingsDivider = new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL); // header end
mainContainer->Add(settingsDivider, 0, wxEXPAND | wxALL, 5);
// enabled apps start
wxBoxSizer* enabledAppsContainer; auto settingsDivider = new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL);
enabledAppsContainer = new wxBoxSizer(wxHORIZONTAL); mainContainer->Add(settingsDivider, 0, wxEXPAND | wxALL, 5);
auto enabledAppsText = wxBoxSizer* enabledAppsContainer;
new wxStaticText(this, wxID_ANY, _("Enabled Apps:"), wxDefaultPosition, wxDefaultSize, 0); enabledAppsContainer = new wxBoxSizer(wxHORIZONTAL);
enabledAppsText->Wrap(-1);
enabledAppsContainer->Add(enabledAppsText, 0, wxALL, 5); auto enabledAppsText =
new wxStaticText(this, wxID_ANY, _("Enabled Apps:"), wxDefaultPosition, wxDefaultSize, 0);
wxBoxSizer* appCheckboxContainer; enabledAppsText->Wrap(-1);
appCheckboxContainer = new wxBoxSizer(wxVERTICAL); enabledAppsContainer->Add(enabledAppsText, 0, wxALL, 5);
auto settings = utils::getSettings(); wxBoxSizer* appCheckboxContainer;
appCheckboxContainer = new wxBoxSizer(wxVERTICAL);
for (auto app : settings.apps) {
auto checkbox = new wxCheckBox(this, wxID_ANY, _(app.appName), wxDefaultPosition, wxDefaultSize, 0); auto settings = utils::getSettings();
checkbox->SetValue(app.enabled);
checkbox->SetClientData(new utils::App(app)); for (auto app : settings.apps) {
checkbox->Bind(wxEVT_CHECKBOX, [checkbox](wxCommandEvent& event) { auto checkbox = new wxCheckBox(this, wxID_ANY, app.appName, wxDefaultPosition, wxDefaultSize, 0);
bool isChecked = checkbox->IsChecked(); checkbox->SetValue(app.enabled);
utils::App* appData = static_cast<utils::App*>(checkbox->GetClientData()); checkbox->SetClientData(new utils::App(app));
appData->enabled = isChecked; checkbox->Bind(wxEVT_CHECKBOX, [checkbox](wxCommandEvent& event) {
utils::saveSettings(appData); bool isChecked = checkbox->IsChecked();
}); utils::App* appData = static_cast<utils::App*>(checkbox->GetClientData());
checkbox->Bind(wxEVT_DESTROY, [checkbox](wxWindowDestroyEvent&) { appData->enabled = isChecked;
delete static_cast<utils::App*>(checkbox->GetClientData()); utils::saveSettings(appData);
}); });
appCheckboxContainer->Add(checkbox, 0, wxALL, 5); checkbox->Bind(wxEVT_DESTROY, [checkbox](wxWindowDestroyEvent&) {
} delete static_cast<utils::App*>(checkbox->GetClientData());
});
auto anyOtherCheckbox = new wxCheckBox(this, wxID_ANY, _("Any other"), wxDefaultPosition, wxDefaultSize, 0); appCheckboxContainer->Add(checkbox, 0, wxALL, 5);
anyOtherCheckbox->SetValue(settings.anyOtherEnabled); }
anyOtherCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) {
bool isChecked = event.IsChecked(); auto anyOtherCheckbox = new wxCheckBox(this, wxID_ANY, _("Any other"), wxDefaultPosition, wxDefaultSize, 0);
auto settings = utils::getSettings(); anyOtherCheckbox->SetValue(settings.anyOtherEnabled);
settings.anyOtherEnabled = isChecked; anyOtherCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) {
utils::saveSettings(settings); bool isChecked = event.IsChecked();
}); auto settings = utils::getSettings();
settings.anyOtherEnabled = isChecked;
appCheckboxContainer->Add(anyOtherCheckbox, 0, wxALL, 5); utils::saveSettings(settings);
});
enabledAppsContainer->Add(appCheckboxContainer, 1, wxEXPAND, 5);
appCheckboxContainer->Add(anyOtherCheckbox, 0, wxALL, 5);
mainContainer->Add(enabledAppsContainer, 0, 0, 5);
// enabled apps end enabledAppsContainer->Add(appCheckboxContainer, 1, wxEXPAND, 5);
// LastFM start mainContainer->Add(enabledAppsContainer, 0, 0, 5);
auto lastfmDivider = new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL); // enabled apps end
mainContainer->Add(lastfmDivider, 0, wxEXPAND | wxALL, 5);
// LastFM start
wxBoxSizer* lastFMContainer; auto lastfmDivider = new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL);
lastFMContainer = new wxBoxSizer(wxHORIZONTAL); mainContainer->Add(lastfmDivider, 0, wxEXPAND | wxALL, 5);
auto lastfmText = new wxStaticText(this, wxID_ANY, _("LastFM:"), wxDefaultPosition, wxDefaultSize, 0); wxBoxSizer* lastFMContainer;
lastfmText->Wrap(-1); lastFMContainer = new wxBoxSizer(wxHORIZONTAL);
lastFMContainer->Add(lastfmText, 0, wxALL, 5);
auto lastfmText = new wxStaticText(this, wxID_ANY, _("LastFM:"), wxDefaultPosition, wxDefaultSize, 0);
wxBoxSizer* lastfmSettingsContainer; lastfmText->Wrap(-1);
lastfmSettingsContainer = new wxBoxSizer(wxVERTICAL); lastFMContainer->Add(lastfmText, 0, wxALL, 5);
auto lastfmEnabledCheckbox = new wxCheckBox(this, wxID_ANY, _("Enabled"), wxDefaultPosition, wxDefaultSize, 0); wxBoxSizer* lastfmSettingsContainer;
lastfmEnabledCheckbox->SetValue(settings.lastfm.enabled); lastfmSettingsContainer = new wxBoxSizer(wxVERTICAL);
lastfmEnabledCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) {
bool isChecked = event.IsChecked(); auto lastfmEnabledCheckbox = new wxCheckBox(this, wxID_ANY, _("Enabled"), wxDefaultPosition, wxDefaultSize, 0);
auto settings = utils::getSettings(); lastfmEnabledCheckbox->SetValue(settings.lastfm.enabled);
settings.lastfm.enabled = isChecked; lastfmEnabledCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) {
utils::saveSettings(settings); bool isChecked = event.IsChecked();
}); auto settings = utils::getSettings();
lastfmSettingsContainer->Add(lastfmEnabledCheckbox, 0, wxALIGN_CENTER | wxALL, 5); settings.lastfm.enabled = isChecked;
lastFMContainer->Add(lastfmSettingsContainer, 1, wxEXPAND, 5); utils::saveSettings(settings);
});
auto usernameInput = lastfmSettingsContainer->Add(lastfmEnabledCheckbox, 0, wxALIGN_CENTER | wxALL, 5);
new wxTextCtrlWithPlaceholder(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0); lastFMContainer->Add(lastfmSettingsContainer, 1, wxEXPAND, 5);
usernameInput->SetPlaceholderText(_("Username"));
usernameInput->SetValue(settings.lastfm.username); auto usernameInput =
usernameInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) { new wxTextCtrlWithPlaceholder(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0);
auto settings = utils::getSettings(); usernameInput->SetPlaceholderText(_("Username"));
std::string data = event.GetString().ToStdString(); usernameInput->SetValue(settings.lastfm.username);
settings.lastfm.username = data; usernameInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) {
utils::saveSettings(settings); auto settings = utils::getSettings();
}); std::string data = event.GetString().ToStdString();
auto passwordInput = new wxTextCtrlWithPlaceholder(this, wxID_ANY, wxEmptyString, wxDefaultPosition, settings.lastfm.username = data;
wxDefaultSize, wxTE_PASSWORD); utils::saveSettings(settings);
passwordInput->SetPlaceholderText(_("Password")); });
passwordInput->SetValue(settings.lastfm.password); auto passwordInput = new wxTextCtrlWithPlaceholder(this, wxID_ANY, wxEmptyString, wxDefaultPosition,
passwordInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) { wxDefaultSize, wxTE_PASSWORD);
auto settings = utils::getSettings(); passwordInput->SetPlaceholderText(_("Password"));
std::string data = event.GetString().ToStdString(); passwordInput->SetValue(settings.lastfm.password);
settings.lastfm.password = data; passwordInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) {
utils::saveSettings(settings); auto settings = utils::getSettings();
}); std::string data = event.GetString().ToStdString();
auto apikeyInput = settings.lastfm.password = data;
new wxTextCtrlWithPlaceholder(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0); utils::saveSettings(settings);
apikeyInput->SetPlaceholderText(_("API-Key")); });
apikeyInput->SetValue(settings.lastfm.api_key); auto apikeyInput =
apikeyInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) { new wxTextCtrlWithPlaceholder(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0);
auto settings = utils::getSettings(); apikeyInput->SetPlaceholderText(_("API-Key"));
std::string data = event.GetString().ToStdString(); apikeyInput->SetValue(settings.lastfm.api_key);
settings.lastfm.api_key = data; apikeyInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) {
utils::saveSettings(settings); auto settings = utils::getSettings();
}); std::string data = event.GetString().ToStdString();
auto apisecretInput = new wxTextCtrlWithPlaceholder(this, wxID_ANY, wxEmptyString, wxDefaultPosition, settings.lastfm.api_key = data;
wxDefaultSize, wxTE_PASSWORD); utils::saveSettings(settings);
apisecretInput->SetPlaceholderText(_("API-Secret")); });
apisecretInput->SetValue(settings.lastfm.api_secret); auto apisecretInput = new wxTextCtrlWithPlaceholder(this, wxID_ANY, wxEmptyString, wxDefaultPosition,
apisecretInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) { wxDefaultSize, wxTE_PASSWORD);
auto settings = utils::getSettings(); apisecretInput->SetPlaceholderText(_("API-Secret"));
std::string data = event.GetString().ToStdString(); apisecretInput->SetValue(settings.lastfm.api_secret);
settings.lastfm.api_secret = data; apisecretInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) {
utils::saveSettings(settings); auto settings = utils::getSettings();
}); std::string data = event.GetString().ToStdString();
settings.lastfm.api_secret = data;
auto checkButton = new wxButton(this, wxID_ANY, _("Check credentials")); utils::saveSettings(settings);
checkButton->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { initLastFM(true); }); });
mainContainer->Add(lastFMContainer, 0, 0, 5);
auto checkButton = new wxButton(this, wxID_ANY, _("Check credentials"));
mainContainer->Add(usernameInput, 0, wxEXPAND | wxALL, 5); checkButton->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { initLastFM(true); });
mainContainer->Add(passwordInput, 0, wxEXPAND | wxALL, 5); mainContainer->Add(lastFMContainer, 0, 0, 5);
mainContainer->Add(apikeyInput, 0, wxEXPAND | wxALL, 5);
mainContainer->Add(apisecretInput, 0, wxEXPAND | wxALL, 5); mainContainer->Add(usernameInput, 0, wxEXPAND | wxALL, 5);
mainContainer->Add(checkButton, 0, wxEXPAND | wxALL, 10); mainContainer->Add(passwordInput, 0, wxEXPAND | wxALL, 5);
mainContainer->Add(apikeyInput, 0, wxEXPAND | wxALL, 5);
// Last FM End mainContainer->Add(apisecretInput, 0, wxEXPAND | wxALL, 5);
mainContainer->Add(checkButton, 0, wxEXPAND | wxALL, 10);
// settings start
auto appsDivider = new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL); // Last FM End
mainContainer->Add(appsDivider, 0, wxEXPAND | wxALL, 5);
// settings start
wxBoxSizer* settingsContainer; auto appsDivider = new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL);
settingsContainer = new wxBoxSizer(wxHORIZONTAL); mainContainer->Add(appsDivider, 0, wxEXPAND | wxALL, 5);
auto startupText = new wxStaticText(this, wxID_ANY, _("Startup:"), wxDefaultPosition, wxDefaultSize, 0); wxBoxSizer* settingsContainer;
startupText->Wrap(-1); settingsContainer = new wxBoxSizer(wxHORIZONTAL);
settingsContainer->Add(startupText, 0, wxALL, 5);
auto startupText = new wxStaticText(this, wxID_ANY, _("Startup:"), wxDefaultPosition, wxDefaultSize, 0);
auto autostartCheckbox = startupText->Wrap(-1);
new wxCheckBox(this, wxID_ANY, _("Launch at login"), wxDefaultPosition, wxDefaultSize, 0); settingsContainer->Add(startupText, 0, wxALL, 5);
autostartCheckbox->SetValue(settings.autoStart);
autostartCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) { auto autostartCheckbox =
bool isChecked = event.IsChecked(); new wxCheckBox(this, wxID_ANY, _("Launch at login"), wxDefaultPosition, wxDefaultSize, 0);
auto settings = utils::getSettings(); autostartCheckbox->SetValue(settings.autoStart);
settings.autoStart = isChecked; autostartCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) {
backend::toggleAutostart(isChecked); bool isChecked = event.IsChecked();
utils::saveSettings(settings); auto settings = utils::getSettings();
}); settings.autoStart = isChecked;
backend::toggleAutostart(isChecked);
settingsContainer->Add(autostartCheckbox, 0, wxALL, 5); utils::saveSettings(settings);
mainContainer->Add(settingsContainer, 0, wxEXPAND, 5); });
// settings end
this->SetSizerAndFit(mainContainer); settingsContainer->Add(autostartCheckbox, 0, wxALL, 5);
mainContainer->Add(settingsContainer, 0, wxEXPAND, 5);
wxSize currentSize = this->GetSize(); // settings end
this->SetSize(size.GetWidth(), currentSize.GetHeight()); this->SetSizerAndFit(mainContainer);
this->Layout();
wxSize currentSize = this->GetSize();
this->Centre(wxBOTH); this->SetSize(size.GetWidth(), currentSize.GetHeight());
} this->Layout();
};
class PlayerLink : public wxApp { this->Centre(wxBOTH);
public: }
virtual bool OnInit() override { };
if (!backend::init()) { class PlayerLink : public wxApp {
wxMessageBox(_("Error initializing platform backend!"), _("PlayerLink"), wxOK | wxICON_ERROR); public:
return false; virtual bool OnInit() override {
} if (!backend::init()) {
wxMessageBox(_("Error initializing platform backend!"), _("PlayerLink"), wxOK | wxICON_ERROR);
if (wxSystemSettings::GetAppearance().IsSystemDark()) // To support the native dark mode on windows 10 and up return false;
this->SetAppearance(wxAppBase::Appearance::Dark); }
wxInitAllImageHandlers(); if (wxSystemSettings::GetAppearance().IsSystemDark()) // To support the native dark mode on windows 10 and up
wxIcon icon = utils::loadIconFromMemory(icon_png, icon_png_size); this->SetAppearance(wxAppBase::Appearance::Dark);
PlayerLinkFrame* frame = new PlayerLinkFrame(nullptr, icon, wxID_ANY, _("PlayerLink"));
trayIcon = new PlayerLinkIcon(frame); wxInitAllImageHandlers();
frame->Bind(wxEVT_CLOSE_WINDOW, [=](wxCloseEvent& event) { wxIcon icon = utils::loadIconFromMemory(icon_png, icon_png_size);
if (event.CanVeto()) { PlayerLinkFrame* frame = new PlayerLinkFrame(nullptr, icon, wxID_ANY, _("PlayerLink"));
frame->Hide(); trayIcon = new PlayerLinkIcon(frame);
event.Veto(); frame->Bind(wxEVT_CLOSE_WINDOW, [=](wxCloseEvent& event) {
} else if (event.CanVeto()) {
std::exit(0); frame->Hide();
}); event.Veto();
} else
trayIcon->SetIcon(icon, _("PlayerLink")); std::exit(0);
return true; });
}
trayIcon->SetIcon(icon, _("PlayerLink"));
private: return true;
PlayerLinkIcon* trayIcon; }
};
private:
wxIMPLEMENT_APP_NO_MAIN(PlayerLink); PlayerLinkIcon* trayIcon;
};
int main(int argc, char** argv) {
std::thread rpcThread(handleRPCTasks); wxIMPLEMENT_APP_NO_MAIN(PlayerLink);
rpcThread.detach();
std::thread mediaThread(handleMediaTasks); int main(int argc, char** argv) {
mediaThread.detach(); std::thread rpcThread(handleRPCTasks);
return wxEntry(argc, argv); rpcThread.detach();
std::thread mediaThread(handleMediaTasks);
mediaThread.detach();
return wxEntry(argc, argv);
} }

2
vendor/wxWidgets vendored

@ -1 +1 @@
Subproject commit 12b09a5e5ea76a1a0c27b769e821b37d803a4cb7 Subproject commit 138937b7775c117b57f55374a0c507a35a1102f6