- Fix: settings window not auto focussing on mac os
- Optimized the windows backend - Updated dependencies
This commit is contained in:
parent
4a68eba106
commit
d13231e84e
|
@ -1,10 +1,11 @@
|
|||
BasedOnStyle: Google
|
||||
UseTab: Never
|
||||
IndentWidth: 4
|
||||
TabWidth: 4
|
||||
BreakBeforeBraces: Attach
|
||||
AllowShortIfStatementsOnASingleLine: false
|
||||
IndentCaseLabels: false
|
||||
AccessModifierOffset: -4
|
||||
ColumnLimit: 120
|
||||
NamespaceIndentation: All
|
||||
BasedOnStyle: Google
|
||||
UseTab: Never
|
||||
IndentWidth: 4
|
||||
TabWidth: 4
|
||||
BreakBeforeBraces: Attach
|
||||
AllowShortIfStatementsOnASingleLine: false
|
||||
IndentCaseLabels: false
|
||||
AccessModifierOffset: -4
|
||||
ColumnLimit: 120
|
||||
NamespaceIndentation: All
|
||||
SortIncludes: false
|
|
@ -1,198 +1,208 @@
|
|||
#ifdef _WIN32
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <objbase.h>
|
||||
#include <psapi.h>
|
||||
#include <shlobj.h>
|
||||
#include <windows.h>
|
||||
#include <appmodel.h>
|
||||
#include <winrt/windows.foundation.h>
|
||||
#include <winrt/windows.foundation.metadata.h>
|
||||
#include <winrt/windows.media.control.h>
|
||||
#include <winrt/windows.storage.streams.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <codecvt>
|
||||
#include <filesystem>
|
||||
|
||||
#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<std::codecvt_utf8<wchar_t>> 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<void**>(&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<void**>(&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<MediaInfo> 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<std::chrono::milliseconds>(timelineInformation.EndTime()).count();
|
||||
auto elapsedTime = std::chrono::duration_cast<std::chrono::milliseconds>(timelineInformation.Position()).count();
|
||||
|
||||
auto thumbnail = mediaProperties.Thumbnail();
|
||||
std::string thumbnailData = "";
|
||||
|
||||
if (thumbnail) {
|
||||
auto stream = thumbnail.OpenReadAsync().get();
|
||||
size_t size = static_cast<size_t>(stream.Size());
|
||||
|
||||
DataReader reader(stream);
|
||||
reader.LoadAsync(static_cast<uint32_t>(size)).get();
|
||||
|
||||
std::vector<uint8_t> 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<MediaInfo>(
|
||||
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 <objbase.h>
|
||||
#include <psapi.h>
|
||||
#include <shlobj.h>
|
||||
#include <windows.h>
|
||||
#include <appmodel.h>
|
||||
#include <winrt/windows.foundation.h>
|
||||
#include <winrt/windows.foundation.metadata.h>
|
||||
#include <winrt/windows.media.control.h>
|
||||
#include <winrt/windows.storage.streams.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <codecvt>
|
||||
#include <filesystem>
|
||||
|
||||
#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<int>(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<void**>(&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<void**>(&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<MediaInfo> 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<std::chrono::milliseconds>(timelineInformation.EndTime()).count();
|
||||
auto elapsedTime = std::chrono::duration_cast<std::chrono::milliseconds>(timelineInformation.Position()).count();
|
||||
|
||||
auto thumbnail = mediaProperties.Thumbnail();
|
||||
std::string thumbnailData = "";
|
||||
|
||||
if (thumbnail) {
|
||||
auto stream = thumbnail.OpenReadAsync().get();
|
||||
size_t size = static_cast<size_t>(stream.Size());
|
||||
|
||||
DataReader reader(stream);
|
||||
reader.LoadAsync(static_cast<uint32_t>(size)).get();
|
||||
|
||||
std::vector<uint8_t> 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<MediaInfo>(
|
||||
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
|
||||
|
|
867
src/main.cpp
867
src/main.cpp
|
@ -1,433 +1,436 @@
|
|||
#include <discord-rpc/discord_rpc.h>
|
||||
#include <wx/image.h>
|
||||
#include <wx/mstream.h>
|
||||
#include <wx/statline.h>
|
||||
#include <wx/taskbar.h>
|
||||
#include <wx/wx.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
#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<utils::App*>(checkbox->GetClientData());
|
||||
appData->enabled = isChecked;
|
||||
utils::saveSettings(appData);
|
||||
});
|
||||
checkbox->Bind(wxEVT_DESTROY, [checkbox](wxWindowDestroyEvent&) {
|
||||
delete static_cast<utils::App*>(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 <discord-rpc/discord_rpc.h>
|
||||
#include <wx/image.h>
|
||||
#include <wx/mstream.h>
|
||||
#include <wx/statline.h>
|
||||
#include <wx/taskbar.h>
|
||||
#include <wx/wx.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
#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<utils::App*>(checkbox->GetClientData());
|
||||
appData->enabled = isChecked;
|
||||
utils::saveSettings(appData);
|
||||
});
|
||||
checkbox->Bind(wxEVT_DESTROY, [checkbox](wxWindowDestroyEvent&) {
|
||||
delete static_cast<utils::App*>(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);
|
||||
}
|
|
@ -1 +1 @@
|
|||
Subproject commit 12b09a5e5ea76a1a0c27b769e821b37d803a4cb7
|
||||
Subproject commit 138937b7775c117b57f55374a0c507a35a1102f6
|
Loading…
Reference in New Issue