Compare commits

..

No commits in common. "7d4bc6dd898a43dd71a4ec91618654e48b59840e" and "c35416c9283ce4e3dda3e22077d3c9651205ca70" have entirely different histories.

24 changed files with 317 additions and 774 deletions

View File

@ -10,7 +10,7 @@ on:
jobs: jobs:
build-linux: build-linux:
runs-on: ubuntu-22.04 runs-on: ubuntu-20.04
steps: steps:
- name: Checkout code - name: Checkout code
@ -46,7 +46,7 @@ jobs:
run: cmake -B build -S . -DCMAKE_BUILD_TYPE=Release run: cmake -B build -S . -DCMAKE_BUILD_TYPE=Release
- name: Build the project - name: Build the project
run: cmake --build build --config Release --target PlayerLink --parallel $(nproc) run: cmake --build build --config Release
- name: Download linuxdeploy - name: Download linuxdeploy
run: | run: |
@ -66,63 +66,6 @@ jobs:
name: PlayerLink-AppImage name: PlayerLink-AppImage
path: ./*.AppImage path: ./*.AppImage
build-linux-arm64:
runs-on: ubuntu-22.04-arm
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
submodules: recursive
fetch-depth: 0
- name: Set up CMake
uses: lukka/get-cmake@latest
with:
cmakeVersion: '3.22.0'
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential \
libssl-dev \
libx11-dev \
libxext-dev \
libxrandr-dev \
libxinerama-dev \
libxcursor-dev \
zlib1g-dev \
libglu1-mesa-dev \
libgtk-3-dev \
libwayland-dev \
fuse
sudo modprobe fuse
- name: Configure with CMake
run: cmake -B build -S . -DCMAKE_BUILD_TYPE=Release
- name: Build the project
run: cmake --build build --config Release --target PlayerLink --parallel $(nproc)
- name: Download linuxdeploy
run: |
wget "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-aarch64.AppImage"
wget "https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh"
chmod +x linuxdeploy-aarch64.AppImage linuxdeploy-plugin-gtk.sh
sudo mv linuxdeploy-aarch64.AppImage /usr/local/bin/linuxdeploy
- name: Create AppImage
run: |
cp -r build/PlayerLink linux/AppDir/usr/bin/
linuxdeploy --appdir=linux/AppDir --plugin gtk --output appimage
- name: Upload AppImage artifact
uses: actions/upload-artifact@v4
with:
name: PlayerLink-AppImage-ARM64
path: ./*.AppImage
build-windows: build-windows:
runs-on: windows-latest runs-on: windows-latest
@ -142,47 +85,12 @@ jobs:
run: cmake -B build -S . -DCMAKE_BUILD_TYPE=Release run: cmake -B build -S . -DCMAKE_BUILD_TYPE=Release
- name: Build the project - name: Build the project
run: cmake --build build --config Release --target PlayerLink run: cmake --build build --config Release
- name: Upload Windows artifact - name: Upload Windows artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: PlayerLink-Windows-AMD64 name: PlayerLink-Windows-Executable
path: build/Release/*
build-windows-arm64:
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
submodules: recursive
fetch-depth: 0
- name: Setup right windows sdk
uses: GuillaumeFalourd/setup-windows10-sdk-action@v2.4
with:
sdk-version: 26100
- name: Set up CMake
uses: lukka/get-cmake@latest
with:
cmakeVersion: '3.22.0'
- name: Configure with CMake for ARM64
run: cmake -B build -S . -DCMAKE_SYSTEM_VERSION="10.0.26100.0" -DCMAKE_BUILD_TYPE=Release -A ARM64
- name: Build the project for ARM64
run: cmake --build build --config Release --target PlayerLink
- name: Rename ARM64 executable
run: Rename-Item build\Release\PlayerLink.exe PlayerLink-arm64.exe
- name: Upload Windows ARM64 artifact
uses: actions/upload-artifact@v4
with:
name: PlayerLink-Windows-ARM64
path: build/Release/* path: build/Release/*
build-macos: build-macos:
@ -204,7 +112,7 @@ jobs:
run: cmake -B build -S . -DCMAKE_BUILD_TYPE=Release run: cmake -B build -S . -DCMAKE_BUILD_TYPE=Release
- name: Build the project - name: Build the project
run: cmake --build build --config Release --target PlayerLink --parallel $(sysctl -n hw.physicalcpu) run: cmake --build build --config Release
- name: Create DMG package - name: Create DMG package
run: | run: |
@ -220,7 +128,7 @@ jobs:
create-release: create-release:
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
needs: [build-linux, build-linux-arm64, build-windows, build-windows-arm64, build-macos] needs: [build-linux, build-windows, build-macos]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Download artifacts - name: Download artifacts
@ -240,7 +148,6 @@ jobs:
release_name: Release ${{ github.ref_name }} release_name: Release ${{ github.ref_name }}
draft: false draft: false
prerelease: false prerelease: false
- name: Upload AppImage - name: Upload AppImage
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1
env: env:
@ -251,16 +158,6 @@ jobs:
asset_name: PlayerLink-x86_64.AppImage asset_name: PlayerLink-x86_64.AppImage
asset_content_type: application/octet-stream asset_content_type: application/octet-stream
- name: Upload ARM64 AppImage
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./release-assets/PlayerLink-aarch64.AppImage
asset_name: PlayerLink-aarch64.AppImage
asset_content_type: application/octet-stream
- name: Upload Windows Executable - name: Upload Windows Executable
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1
env: env:
@ -271,16 +168,6 @@ jobs:
asset_name: PlayerLink.exe asset_name: PlayerLink.exe
asset_content_type: application/octet-stream asset_content_type: application/octet-stream
- name: Upload Windows ARM64 Executable
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./release-assets/PlayerLink-arm64.exe
asset_name: PlayerLink-arm64.exe
asset_content_type: application/octet-stream
- name: Upload macOS DMG - name: Upload macOS DMG
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1
env: env:

View File

@ -1,11 +1,11 @@
cmake_minimum_required (VERSION 3.12) cmake_minimum_required (VERSION 3.8)
include("cmake/create_resources.cmake") include("cmake/create_resources.cmake")
file(GLOB_RECURSE SOURCES "src/*.cpp") file(GLOB_RECURSE SOURCES "src/*.cpp")
#enable objective c support on mac os, needed for wxwidgets and compile for both intel macs and apple sillicon macs #enable objective c support on mac os, needed for wxwidgets and compile for both intel macs and apple sillicon macs
if(APPLE) if(APPLE)
list(APPEND SOURCES "src/backends/darwin.mm" ${CMAKE_SOURCE_DIR}/osx/icon.icns ${CMAKE_SOURCE_DIR}/osx/MediaRemote.js) list(APPEND SOURCES "src/backends/darwin.mm" ${CMAKE_SOURCE_DIR}/osx/icon.icns)
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "" FORCE) set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "" FORCE)
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "" FORCE) set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "" FORCE)
project ("PlayerLink" LANGUAGES C CXX OBJCXX) project ("PlayerLink" LANGUAGES C CXX OBJCXX)
@ -40,7 +40,6 @@ elseif(APPLE)
endif() endif()
set_target_properties(PlayerLink PROPERTIES MACOSX_BUNDLE TRUE) set_target_properties(PlayerLink PROPERTIES MACOSX_BUNDLE TRUE)
set_source_files_properties(${CMAKE_SOURCE_DIR}/osx/icon.icns PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") set_source_files_properties(${CMAKE_SOURCE_DIR}/osx/icon.icns PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
set_source_files_properties(${CMAKE_SOURCE_DIR}/osx/MediaRemote.js PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
set_target_properties(PlayerLink PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/osx/Info.plist) set_target_properties(PlayerLink PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/osx/Info.plist)
elseif(UNIX AND NOT APPLE) elseif(UNIX AND NOT APPLE)
list(APPEND LIBRARIES dbus) list(APPEND LIBRARIES dbus)

View File

@ -80,13 +80,13 @@ An example on how to add custom apps to the config can be found [here](./setting
4. Build the project :) 4. Build the project :)
```bash ```bash
# for a release build # for a release build
cmake --build build --config Release --target PlayerLink cmake --build build --config Release
# for a debug build # for a debug build
cmake --build build --target PlayerLink cmake --build build
``` ```
## Contributing ## Contributing
This repository is open for contributions. You can view the current roadmap [here](https://github.com/EinTim23/PlayerLink/projects) or implement your own features and then open a pull request. Please keep your code as consistent and clean as possible. This repository is open for contributions. You can view the current roadmap [here](https://github.com/EinTim23/PlayerLink/projects) or implement your own features and then open a pull request. Please keep your code as consistent and clean as possible.
## Credits ## Credits
This project was heavily inspired by [Alexandra Göttlicher's MusicRPC](https://github.com/kaethchen/MusicRPC) and her project may provide a better experience when being on Mac OS only. This project was heavily inspired by [Alexandra Aurora's MusicRPC](https://github.com/AlexandraAurora/MusicRPC) and her project may provide a better experience when being on Mac OS only.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 692 KiB

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 285 KiB

View File

@ -1,33 +0,0 @@
ObjC.import('Foundation');
try {
const frameworkPath = '/System/Library/PrivateFrameworks/MediaRemote.framework';
const framework = $.NSBundle.bundleWithPath($(frameworkPath));
framework.load
const MRNowPlayingRequest = $.NSClassFromString('MRNowPlayingRequest');
const playerPath = MRNowPlayingRequest.localNowPlayingPlayerPath;
const bundleID = ObjC.unwrap(playerPath.client.bundleIdentifier);
const nowPlayingItem = MRNowPlayingRequest.localNowPlayingItem;
const info = nowPlayingItem.nowPlayingInfo;
const title = info.valueForKey('kMRMediaRemoteNowPlayingInfoTitle');
const album = info.valueForKey('kMRMediaRemoteNowPlayingInfoAlbum');
const artist = info.valueForKey('kMRMediaRemoteNowPlayingInfoArtist');
const duration = info.valueForKey('kMRMediaRemoteNowPlayingInfoDuration');
const playbackStatus = info.valueForKey('kMRMediaRemoteNowPlayingInfoPlaybackRate');
const elapsed = info.valueForKey('kMRMediaRemoteNowPlayingInfoElapsedTime');
JSON.stringify({
title: ObjC.unwrap(title),
album: ObjC.unwrap(album),
artist: ObjC.unwrap(artist),
duration: ObjC.unwrap(duration),
playbackStatus: ObjC.unwrap(playbackStatus),
elapsed: ObjC.unwrap(elapsed),
player: ObjC.unwrap(bundleID)
});
} catch (error) {
JSON.stringify({ player: 'none', error: error.toString() });
}

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 817 KiB

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
<path d="M21.731 2.269a2.625 2.625 0 0 0-3.712 0l-1.157 1.157 3.712 3.712 1.157-1.157a2.625 2.625 0 0 0 0-3.712ZM19.513 8.199l-3.712-3.712-12.15 12.15a5.25 5.25 0 0 0-1.32 2.214l-.8 2.685a.75.75 0 0 0 .933.933l2.685-.8a5.25 5.25 0 0 0 2.214-1.32L19.513 8.2Z" />
</svg>

Before

Width:  |  Height:  |  Size: 367 B

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
<path fill-rule="evenodd" d="M14 3a2 2 0 1 0-4 0v7H3a2 2 0 1 0 0 4h7v7a2 2 0 1 0 4 0v-7h7a2 2 0 1 0 0-4h-7V3z" clip-rule="evenodd" />
</svg>

Before

Width:  |  Height:  |  Size: 239 B

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
<path fill-rule="evenodd" d="M16.5 4.478v.227a48.816 48.816 0 0 1 3.878.512.75.75 0 1 1-.256 1.478l-.209-.035-1.005 13.07a3 3 0 0 1-2.991 2.77H8.084a3 3 0 0 1-2.991-2.77L4.087 6.66l-.209.035a.75.75 0 0 1-.256-1.478A48.567 48.567 0 0 1 7.5 4.705v-.227c0-1.564 1.213-2.9 2.816-2.951a52.662 52.662 0 0 1 3.369 0c1.603.051 2.815 1.387 2.815 2.951Zm-6.136-1.452a51.196 51.196 0 0 1 3.273 0C14.39 3.05 15 3.684 15 4.478v.113a49.488 49.488 0 0 0-6 0v-.113c0-.794.609-1.428 1.364-1.452Zm-.355 5.945a.75.75 0 1 0-1.5.058l.347 9a.75.75 0 1 0 1.499-.058l-.346-9Zm5.48.058a.75.75 0 1 0-1.498-.058l-.347 9a.75.75 0 0 0 1.5.058l.345-9Z" clip-rule="evenodd" />
</svg>

Before

Width:  |  Height:  |  Size: 751 B

View File

@ -5,34 +5,13 @@
"client_id": "1245257414715113573", "client_id": "1245257414715113573",
"enabled": true, "enabled": true,
"name": "Spotify", "name": "Spotify",
"type": 2,
"process_names": [ "process_names": [
"org.mpris.MediaPlayer2.spotify", "org.mpris.MediaPlayer2.spotify",
"com.spotify.client", "com.spotify.client",
"Spotify.exe" "Spotify.exe"
], ],
"search_endpoint": "https://open.spotify.com/search/" "search_endpoint": "https://open.spotify.com/search/"
},
{
"client_id": "1337188104829665340",
"enabled": true,
"name": "Apple Music",
"type": 2,
"process_names": [
"com.apple.Music",
"AppleInc.AppleMusicWin_nzyj5cx40ttqa!APP",
"AppleMusic.exe"
],
"search_endpoint": "https://music.apple.com/search?term="
} }
], ],
"lastfm": {
"api_key": "",
"api_secret": "",
"enabled": false,
"password": "",
"username": ""
},
"odesli": true,
"autostart": false "autostart": false
} }

View File

@ -1,11 +1,9 @@
#include <Foundation/NSObjCRuntime.h>
#ifdef __APPLE__ #ifdef __APPLE__
#include <AppKit/AppKit.h> #include <AppKit/AppKit.h>
#include <Cocoa/Cocoa.h> #include <Cocoa/Cocoa.h>
#include <Foundation/Foundation.h> #include <Foundation/Foundation.h>
#include <dispatch/dispatch.h> #include <dispatch/dispatch.h>
#include <filesystem> #include <filesystem>
#include <nlohmann-json/single_include/nlohmann/json.hpp>
#include <fstream> #include <fstream>
#include "../MediaRemote.hpp" #include "../MediaRemote.hpp"
@ -18,61 +16,7 @@ void hideDockIcon(bool shouldHide) {
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
} }
NSString *getFilePathFromBundle(NSString *fileName, NSString *fileType) {
NSString *filePath = [[NSBundle mainBundle] pathForResource:fileName ofType:fileType];
return filePath;
}
NSString *executeCommand(NSString *command, NSArray *arguments) {
NSTask *task = [[NSTask alloc] init];
task.launchPath = command;
task.arguments = arguments;
NSPipe *pipe = [NSPipe pipe];
task.standardOutput = pipe;
task.standardError = [NSPipe pipe];
[task launch];
NSData *data = [[pipe fileHandleForReading] readDataToEndOfFile];
NSString *output = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
[task waitUntilExit];
return output;
}
std::shared_ptr<MediaInfo> backend::getMediaInformation() { std::shared_ptr<MediaInfo> backend::getMediaInformation() {
// apple decided to prevent apps not signed by them to use media remote, so we use an apple script instead. But that script only works on Sonoma or newer and the other one is arguably better, so keep the old method as well
if (@available(macOS 15.0, *)) {
static NSString *script = getFilePathFromBundle(@"MediaRemote", @"js");
NSString *output = executeCommand(@"/usr/bin/osascript", @[ @"-l", @"JavaScript", script ]);
nlohmann::json j = nlohmann::json::parse([output UTF8String]);
std::string appName = j["player"].get<std::string>();
if (appName == "none")
return nullptr;
bool paused = j["playbackStatus"].get<int>() == 0;
std::string songTitle = j["title"].get<std::string>();
std::string songAlbum = j["album"].get<std::string>();
std::string songArtist = j["artist"].get<std::string>();
int64_t elapsedTimeMs = 0;
int64_t durationMs = 0;
try {
double durationNumber = j["duration"].get<double>();
durationMs = static_cast<int64_t>(durationNumber * 1000);
double elapsedTimeNumber = j["elapsed"].get<double>();
elapsedTimeMs = static_cast<int64_t>(elapsedTimeNumber * 1000);
} catch (...) {
}
return std::make_shared<MediaInfo>(paused, songTitle, songArtist, songAlbum, appName, "", durationMs,
elapsedTimeMs);
} else {
__block NSString *appName = nil; __block NSString *appName = nil;
__block NSDictionary *playingInfo = nil; __block NSDictionary *playingInfo = nil;
@ -132,7 +76,6 @@ std::shared_ptr<MediaInfo> backend::getMediaInformation() {
return std::make_shared<MediaInfo>(paused, songTitle, songArtist, songAlbum, appNameString, thumbnailData, return std::make_shared<MediaInfo>(paused, songTitle, songArtist, songAlbum, appNameString, thumbnailData,
durationMs, elapsedTimeMs); durationMs, elapsedTimeMs);
} }
}
std::filesystem::path backend::getConfigDirectory() { std::filesystem::path backend::getConfigDirectory() {
std::filesystem::path configDirectoryPath = std::getenv("HOME"); std::filesystem::path configDirectoryPath = std::getenv("HOME");

View File

@ -35,6 +35,59 @@ std::string toStdString(winrt::hstring& in) {
return result; 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) { bool CreateShortcut(std::string source, std::string target) {
CoInitialize(nullptr); CoInitialize(nullptr);
WCHAR src[MAX_PATH]; WCHAR src[MAX_PATH];
@ -92,15 +145,13 @@ std::shared_ptr<MediaInfo> backend::getMediaInformation() {
return nullptr; return nullptr;
auto playbackInfo = currentSession.GetPlaybackInfo(); auto playbackInfo = currentSession.GetPlaybackInfo();
try {
auto mediaProperties = currentSession.TryGetMediaPropertiesAsync().get(); auto mediaProperties = currentSession.TryGetMediaPropertiesAsync().get();
auto timelineInformation = currentSession.GetTimelineProperties(); auto timelineInformation = currentSession.GetTimelineProperties();
if (!mediaProperties) if (!mediaProperties)
return nullptr; return nullptr;
auto endTime = std::chrono::duration_cast<std::chrono::milliseconds>(timelineInformation.EndTime()).count(); auto endTime = std::chrono::duration_cast<std::chrono::milliseconds>(timelineInformation.EndTime()).count();
auto elapsedTime = auto elapsedTime = std::chrono::duration_cast<std::chrono::milliseconds>(timelineInformation.Position()).count();
std::chrono::duration_cast<std::chrono::milliseconds>(timelineInformation.Position()).count();
auto thumbnail = mediaProperties.Thumbnail(); auto thumbnail = mediaProperties.Thumbnail();
std::string thumbnailData = ""; std::string thumbnailData = "";
@ -134,13 +185,17 @@ std::shared_ptr<MediaInfo> backend::getMediaInformation() {
std::string modelId = toStdString(currentSession.SourceAppUserModelId()); 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>( return std::make_shared<MediaInfo>(
playbackInfo.PlaybackStatus() == GlobalSystemMediaTransportControlsSessionPlaybackStatus::Paused, playbackInfo.PlaybackStatus() == GlobalSystemMediaTransportControlsSessionPlaybackStatus::Paused,
toStdString(mediaProperties.Title()), std::move(artist), std::move(albumName), std::move(modelId), toStdString(mediaProperties.Title()), std::move(artist), std::move(albumName), std::move(modelId),
std::move(thumbnailData), endTime, elapsedTime); std::move(thumbnailData), endTime, elapsedTime);
} catch (...) {
return nullptr;
}
} }
bool backend::init() { bool backend::init() {

View File

@ -44,7 +44,6 @@ public:
parameters["api_sig"] = getApiSignature(parameters); parameters["api_sig"] = getApiSignature(parameters);
std::string postBody = utils::getURLEncodedPostBody(parameters); std::string postBody = utils::getURLEncodedPostBody(parameters);
std::string response = utils::httpRequest(api_base, "POST", postBody); std::string response = utils::httpRequest(api_base, "POST", postBody);
try {
auto j = nlohmann::json::parse(response); auto j = nlohmann::json::parse(response);
if (j.contains("error")) if (j.contains("error"))
return j["error"].get<LASTFM_STATUS>(); return j["error"].get<LASTFM_STATUS>();
@ -52,9 +51,6 @@ public:
session_token = j["session"]["key"].get<std::string>(); session_token = j["session"]["key"].get<std::string>();
authenticated = true; authenticated = true;
return LASTFM_STATUS::SUCCESS; return LASTFM_STATUS::SUCCESS;
} catch (...) {
return LASTFM_STATUS::UNKNOWN_ERROR;
}
} }
LASTFM_STATUS scrobble(std::string artist, std::string track) { LASTFM_STATUS scrobble(std::string artist, std::string track) {

View File

@ -3,18 +3,15 @@
#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/hyperlink.h>
#include <wx/wx.h> #include <wx/wx.h>
#include <chrono> #include <chrono>
#include <cstddef>
#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"
#include "wx/sizer.h"
std::string lastPlayingSong = ""; std::string lastPlayingSong = "";
std::string lastMediaSource = ""; std::string lastMediaSource = "";
@ -51,22 +48,17 @@ void initLastFM(bool checkMode = false) {
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)
delete lastfm;
lastfm = nullptr;
if (checkMode)
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();
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));
if (!lastfm)
initLastFM();
auto mediaInformation = backend::getMediaInformation(); auto mediaInformation = backend::getMediaInformation();
auto settings = utils::getSettings(); auto settings = utils::getSettings();
if (!mediaInformation) { if (!mediaInformation) {
@ -93,6 +85,14 @@ void handleMediaTasks() {
if (shouldContinue) if (shouldContinue)
continue; 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; std::string currentMediaSource = mediaInformation->playbackSource;
if (currentMediaSource != lastMediaSource) { if (currentMediaSource != lastMediaSource) {
@ -102,14 +102,6 @@ void handleMediaTasks() {
auto app = utils::getApp(lastMediaSource); auto app = utils::getApp(lastMediaSource);
if (lastPlayingSong.find(mediaInformation->songTitle + mediaInformation->songArtist +
mediaInformation->songAlbum) == std::string::npos &&
lastfm && app.enabled)
lastfm->scrobble(mediaInformation->songArtist, mediaInformation->songTitle);
lastPlayingSong = currentlyPlayingSong;
currentSongTitle = mediaInformation->songArtist + " - " + mediaInformation->songTitle;
if (!app.enabled) { if (!app.enabled) {
Discord_ClearPresence(); Discord_ClearPresence();
continue; continue;
@ -118,7 +110,7 @@ void handleMediaTasks() {
std::string activityState = "by " + mediaInformation->songArtist; std::string activityState = "by " + mediaInformation->songArtist;
DiscordRichPresence activity{}; DiscordRichPresence activity{};
activity.type = app.type; 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();
@ -159,202 +151,27 @@ void handleMediaTasks() {
Discord_UpdatePresence(&activity); Discord_UpdatePresence(&activity);
} }
} }
void SetWindowIcon(wxTopLevelWindow* win) {
const wxIcon icon = utils::loadIconFromMemory(icon_png, icon_png_size);
win->SetIcon(icon);
}
class AboutDialog : public wxDialog {
public:
AboutDialog(wxWindow* parent)
: wxDialog(parent, wxID_ANY, _("About PlayerLink"), wxDefaultPosition, wxDefaultSize,
wxDEFAULT_DIALOG_STYLE & ~wxRESIZE_BORDER) {
SetWindowIcon(this);
wxBoxSizer* mainSizer = new wxBoxSizer(wxVERTICAL);
wxStaticText* label = new wxStaticText(this, wxID_ANY, _("Made with <3 by EinTim"));
label->Wrap(300);
mainSizer->Add(label, 0, wxALL | wxALIGN_CENTER, 10);
wxStaticText* copyrightText = new wxStaticText(this, wxID_ANY, "© 2024-2025 EinTim. All rights reserved.");
copyrightText->Wrap(300);
mainSizer->Add(copyrightText, 0, wxALL | wxALIGN_CENTER, 10);
wxStaticText* creditsText =
new wxStaticText(this, wxID_ANY,
_("Credits:\n- Developer: EinTim\n- Inspiration: Alexandra Göttlicher\n- Icons from: "
"heroicons.com\n- App Icon from: localcc\n- Open source "
"projects used in this:\n wxWidgets, libcurl, libdbus, mbedtls, nlohmann-json."));
creditsText->Wrap(300);
mainSizer->Add(creditsText, 0, wxALL | wxALIGN_CENTER, 10);
wxHyperlinkCtrl* link = new wxHyperlinkCtrl(this, wxID_ANY, _("Visit my website"), _("https://eintim.dev"));
mainSizer->Add(link, 0, wxALL | wxALIGN_CENTER, 10);
wxButton* okButton = new wxButton(this, wxID_OK, _("OK"));
mainSizer->Add(okButton, 0, wxALL | wxALIGN_CENTER, 10);
this->SetSizerAndFit(mainSizer);
this->CenterOnScreen();
}
};
class EditAppDialog : public wxDialog {
public:
EditAppDialog(wxWindow* parent, const wxString title, utils::App* app)
: wxDialog(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize,
wxDEFAULT_DIALOG_STYLE & ~wxRESIZE_BORDER) {
SetWindowIcon(this);
Bind(wxEVT_CLOSE_WINDOW, &EditAppDialog::OnClose, this);
wxBoxSizer* mainSizer = new wxBoxSizer(wxVERTICAL);
wxFlexGridSizer* formSizer = new wxFlexGridSizer(2, 0, 5);
// Make the second column growable (the one with the text controls)
formSizer->AddGrowableCol(1, 1);
// Application name
formSizer->Add(new wxStaticText(this, wxID_ANY, _("Application name:")), 0, wxALL | wxALIGN_CENTER_VERTICAL, 5);
auto nameInput = new wxTextCtrl(this, wxID_ANY);
nameInput->SetValue(app->appName);
nameInput->SetHint(_("Example: Apple Music"));
nameInput->Bind(wxEVT_TEXT,
[this, app](wxCommandEvent& event) { app->appName = event.GetString().ToStdString(); });
formSizer->Add(nameInput, 1, wxALL | wxEXPAND, 5);
// Client ID
formSizer->Add(new wxStaticText(this, wxID_ANY, _("Discord client id:")), 0, wxALL | wxALIGN_CENTER_VERTICAL,
5);
auto clientIdInput = new wxTextCtrl(this, wxID_ANY);
clientIdInput->SetHint(_("Example: 1337188104829665340"));
clientIdInput->SetValue(app->clientId);
clientIdInput->Bind(wxEVT_TEXT,
[this, app](wxCommandEvent& event) { app->clientId = event.GetString().ToStdString(); });
formSizer->Add(clientIdInput, 1, wxALL | wxEXPAND, 5);
// Search endpoint
formSizer->Add(new wxStaticText(this, wxID_ANY, _("Search endpoint:")), 0, wxALL | wxALIGN_CENTER_VERTICAL, 5);
auto searchEndpointInput = new wxTextCtrl(this, wxID_ANY);
searchEndpointInput->SetValue(app->searchEndpoint);
searchEndpointInput->SetHint(_("Search endpoint: https://music.apple.com/search?term="));
searchEndpointInput->Bind(
wxEVT_TEXT, [this, app](wxCommandEvent& event) { app->searchEndpoint = event.GetString().ToStdString(); });
formSizer->Add(searchEndpointInput, 1, wxALL | wxEXPAND, 5);
// Dropdown
formSizer->Add(new wxStaticText(this, wxID_ANY, "Activity Type:"), 0, wxALL | wxALIGN_CENTER_VERTICAL, 5);
wxString choices[] = {_("Listening"), _("Watching"), _("Playing")};
wxChoice* activityChoice = new wxChoice(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 3, choices);
switch (app->type) {
case ActivityType::PLAYING:
activityChoice->SetSelection(2);
break;
case ActivityType::LISTENING:
activityChoice->SetSelection(0);
break;
case ActivityType::WATCHING:
activityChoice->SetSelection(1);
break;
default:
activityChoice->SetSelection(0);
}
activityChoice->Bind(wxEVT_CHOICE, [activityChoice, app](wxCommandEvent& event) {
const std::map<wxString, ActivityType> typeMap = {
{_("Listening"), ActivityType::LISTENING},
{_("Watching"), ActivityType::WATCHING},
{_("Playing"), ActivityType::PLAYING},
};
app->type = typeMap.at(event.GetString());
});
formSizer->Add(activityChoice, 1, wxALL | wxEXPAND, 5);
mainSizer->Add(formSizer, 0, wxEXPAND);
// Process names group
formSizer->Add(new wxStaticText(this, wxID_ANY, _("Process names:")), 0, wxALL | wxALIGN_CENTER_VERTICAL, 5);
wxBoxSizer* processBox = new wxBoxSizer(wxVERTICAL);
wxListBox* listBox = new wxListBox(this, wxID_ANY);
for (auto& process : app->processNames) {
listBox->Append(process);
}
if (app->processNames.size() == 0 && lastMediaSource != "") {
listBox->Append(lastMediaSource);
app->processNames.push_back(lastMediaSource);
}
processBox->Add(listBox, 1, wxALL | wxEXPAND, 5);
// Add input + buttons
wxBoxSizer* addSizer = new wxBoxSizer(wxHORIZONTAL);
auto processNameInput =
new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(250, wxDefaultSize.GetHeight()), 0);
processNameInput->SetHint(_("Process name"));
const auto delete_button_texture = utils::loadSettingsIcon(trash_svg, trash_svg_size);
const auto add_button_texture = utils::loadSettingsIcon(plus_svg, plus_svg_size);
wxBitmapButton* addButton = new wxBitmapButton(this, wxID_ANY, add_button_texture);
addButton->SetToolTip(_("Add process to list"));
wxBitmapButton* removeButton = new wxBitmapButton(this, wxID_ANY, delete_button_texture);
removeButton->SetToolTip(_("Remove process from list"));
addSizer->Add(processNameInput, 1, wxALL | wxEXPAND, 5);
addSizer->Add(addButton, 0, wxALL, 5);
addSizer->Add(removeButton, 0, wxALL, 5);
processBox->Add(addSizer, 0, wxEXPAND);
mainSizer->Add(processBox, 1, wxALL | wxEXPAND);
SetSizerAndFit(mainSizer);
Centre();
// Bind events
addButton->Bind(wxEVT_BUTTON, [processNameInput, listBox, app](wxCommandEvent& event) {
wxString name = processNameInput->GetValue().Trim();
if (!name.IsEmpty()) {
listBox->Append(name);
app->processNames.push_back(name.ToStdString());
processNameInput->Clear();
}
});
removeButton->Bind(wxEVT_BUTTON, [listBox, app](wxCommandEvent& event) {
int selection = listBox->GetSelection();
if (selection != wxNOT_FOUND) {
app->processNames.erase(std::find(app->processNames.begin(), app->processNames.end(),
listBox->GetString(selection).ToStdString()));
listBox->Delete(selection);
}
});
wxBoxSizer* buttonSizer = new wxBoxSizer(wxHORIZONTAL);
wxButton* okButton = new wxButton(this, wxID_OK, _("Save"));
wxButton* cancelButton = new wxButton(this, wxID_CANCEL, _("Cancel"));
buttonSizer->Add(okButton, 0, wxALL | wxALIGN_CENTER, 10);
buttonSizer->Add(cancelButton, 0, wxALL | wxALIGN_CENTER, 10);
mainSizer->Add(buttonSizer, 0, wxALL | wxALIGN_CENTER);
this->SetSizerAndFit(mainSizer);
this->CenterOnParent();
}
private:
void OnClose(wxCloseEvent& event) { EndModal(wxID_CANCEL); }
};
class PlayerLinkIcon : public wxTaskBarIcon { class PlayerLinkIcon : public wxTaskBarIcon {
public: public:
PlayerLinkIcon(wxFrame* s) : settingsFrame(s), aboutDlg(nullptr) {} PlayerLinkIcon(wxFrame* s) : settingsFrame(s) {}
void OnMenuOpen(wxCommandEvent& evt) {
settingsFrame->Show(true);
settingsFrame->Raise();
}
void OnCopyOdesliURL(wxCommandEvent& evt) { utils::copyToClipboard(utils::getOdesliURL(songInfo)); }
void OnMenuExit(wxCommandEvent& evt) { settingsFrame->Close(true); }
void OnMenuAbout(wxCommandEvent& evt) {
wxMessageBox(_("Made with <3 by EinTim"), _("PlayerLink"), wxOK | wxICON_INFORMATION);
}
protected: protected:
virtual wxMenu* CreatePopupMenu() override { virtual wxMenu* CreatePopupMenu() override {
wxMenu* menu = new wxMenu; wxMenu* menu = new wxMenu;
menu->Append(10004, currentSongTitle == "" ? _("Not Playing") : currentSongTitle); menu->Append(10004, currentSongTitle == "" ? _("Not Playing") : wxString::FromUTF8(currentSongTitle));
menu->Enable(10004, false); menu->Enable(10004, false);
menu->AppendSeparator(); menu->AppendSeparator();
menu->Append(10005, _("Copy Odesli URL")); menu->Append(10005, _("Copy Odesli URL"));
@ -372,73 +189,115 @@ protected:
} }
private: private:
void OnMenuOpen(wxCommandEvent& evt) {
settingsFrame->Show(true);
settingsFrame->Raise();
}
void OnCopyOdesliURL(wxCommandEvent& evt) { utils::copyToClipboard(utils::getOdesliURL(songInfo)); }
void OnMenuExit(wxCommandEvent& evt) { settingsFrame->Close(true); }
void OnMenuAbout(wxCommandEvent& evt) {
aboutDlg.Show(true);
aboutDlg.Raise();
}
wxFrame* settingsFrame; wxFrame* settingsFrame;
AboutDialog aboutDlg; };
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),
isPassword((style & wxTE_PASSWORD) != 0) {
Bind(wxEVT_SET_FOCUS, &wxTextCtrlWithPlaceholder::OnFocus, this);
Bind(wxEVT_KILL_FOCUS, &wxTextCtrlWithPlaceholder::OnBlur, this);
}
void SetPlaceholderText(const wxString& p) {
placeholder = p;
if (GetValue().IsEmpty() || showPlaceholder)
UpdatePlaceholder();
}
protected:
void OnFocus(wxFocusEvent& event) {
if (showPlaceholder && GetValue() == placeholder) {
Clear();
if (isPassword)
SetStyleToPassword();
}
showPlaceholder = false;
event.Skip();
}
void OnBlur(wxFocusEvent& event) {
if (GetValue().IsEmpty()) {
showPlaceholder = true;
UpdatePlaceholder();
}
event.Skip();
}
private:
wxString placeholder;
bool showPlaceholder;
bool isPassword;
void UpdatePlaceholder() {
if (isPassword)
SetStyleToNormal();
SetValue(placeholder);
}
void SetStyleToPassword() { SetWindowStyle(GetWindowStyle() | wxTE_PASSWORD); }
void SetStyleToNormal() { SetWindowStyle(GetWindowStyle() & ~wxTE_PASSWORD); }
}; };
class PlayerLinkFrame : public wxFrame { class PlayerLinkFrame : public wxFrame {
public: public:
PlayerLinkFrame(wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = wxEmptyString, PlayerLinkFrame(wxWindow* parent, wxIcon& icon, wxWindowID id = wxID_ANY, const wxString& title = wxEmptyString,
const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize(300, 200), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize(300, 200),
long style = wxDEFAULT_FRAME_STYLE & ~wxRESIZE_BORDER & ~wxMAXIMIZE_BOX) long style = wxDEFAULT_FRAME_STYLE & ~wxRESIZE_BORDER & ~wxMAXIMIZE_BOX)
: wxFrame(parent, id, title, pos, size, style) { : wxFrame(parent, id, title, pos, size, style) {
this->SetSizeHints(wxDefaultSize, wxDefaultSize); this->SetSizeHints(wxDefaultSize, wxDefaultSize);
SetWindowIcon(this); this->SetIcon(icon);
auto mainContainer = new wxBoxSizer(wxVERTICAL); auto mainContainer = new wxBoxSizer(wxVERTICAL);
wxPanel* panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL);
panel->SetSizer(mainContainer);
wxBoxSizer* frameSizer = new wxBoxSizer(wxVERTICAL);
frameSizer->Add(panel, 1, wxEXPAND);
// header start // header start
auto settingsText = new wxStaticText(panel, wxID_ANY, _("Settings"), wxDefaultPosition, wxDefaultSize, 0); auto settingsText = new wxStaticText(this, wxID_ANY, _("Settings"), wxDefaultPosition, wxDefaultSize, 0);
settingsText->Wrap(-1); settingsText->Wrap(-1);
mainContainer->Add(settingsText, 0, wxALIGN_CENTER | wxALL, 5); mainContainer->Add(settingsText, 0, wxALIGN_CENTER | wxALL, 5);
// header end // header end
// enabled apps start // enabled apps start
auto settingsDivider = new wxStaticLine(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL); auto settingsDivider = new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL);
mainContainer->Add(settingsDivider, 0, wxEXPAND | wxALL, 5); mainContainer->Add(settingsDivider, 0, wxEXPAND | wxALL, 5);
wxBoxSizer* enabledAppsContainer; wxBoxSizer* enabledAppsContainer;
enabledAppsContainer = new wxBoxSizer(wxHORIZONTAL); enabledAppsContainer = new wxBoxSizer(wxHORIZONTAL);
auto enabledAppsText = auto enabledAppsText =
new wxStaticText(panel, wxID_ANY, _("Enabled Apps:"), wxDefaultPosition, wxDefaultSize, 0); new wxStaticText(this, wxID_ANY, _("Enabled Apps:"), wxDefaultPosition, wxDefaultSize, 0);
enabledAppsText->Wrap(-1); enabledAppsText->Wrap(-1);
enabledAppsContainer->Add(enabledAppsText, 0, wxALL, 5); enabledAppsContainer->Add(enabledAppsText, 0, wxALL, 5);
const auto edit_button_texture = utils::loadSettingsIcon(pencil_svg, pencil_svg_size); wxBoxSizer* appCheckboxContainer;
const auto delete_button_texture = utils::loadSettingsIcon(trash_svg, trash_svg_size); appCheckboxContainer = new wxBoxSizer(wxVERTICAL);
const auto add_button_texture = utils::loadSettingsIcon(plus_svg, plus_svg_size);
wxBoxSizer* appCheckboxContainer = new wxBoxSizer(wxVERTICAL);
wxBoxSizer* vSizer = new wxBoxSizer(wxVERTICAL);
enabledAppsContainer->Add(vSizer, 0, wxEXPAND);
auto settings = utils::getSettings(); auto settings = utils::getSettings();
for (const auto& app : settings.apps) { for (auto app : settings.apps) {
addCheckboxToContainer(panel, appCheckboxContainer, frameSizer, size, delete_button_texture, auto checkbox = new wxCheckBox(this, wxID_ANY, app.appName, wxDefaultPosition, wxDefaultSize, 0);
edit_button_texture, app); 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);
} }
wxBoxSizer* checkboxRowSizer = new wxBoxSizer(wxHORIZONTAL); auto anyOtherCheckbox = new wxCheckBox(this, wxID_ANY, _("Any other"), wxDefaultPosition, wxDefaultSize, 0);
auto anyOtherCheckbox = new wxCheckBox(panel, wxID_ANY, _("Any other"), wxDefaultPosition, wxDefaultSize, 0);
anyOtherCheckbox->SetValue(settings.anyOtherEnabled); anyOtherCheckbox->SetValue(settings.anyOtherEnabled);
anyOtherCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) { anyOtherCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) {
bool isChecked = event.IsChecked(); bool isChecked = event.IsChecked();
@ -447,50 +306,28 @@ public:
utils::saveSettings(settings); utils::saveSettings(settings);
}); });
wxBitmapButton* addButton = new wxBitmapButton(panel, wxID_ANY, add_button_texture); appCheckboxContainer->Add(anyOtherCheckbox, 0, wxALL, 5);
addButton->SetToolTip(_("Add application"));
addButton->Bind(wxEVT_BUTTON, [this, panel, frameSizer, size, delete_button_texture, edit_button_texture,
appCheckboxContainer](wxCommandEvent& event) {
utils::App* app = new utils::App();
EditAppDialog dlg{this, _("Add new application"), app};
if (dlg.ShowModal() == wxID_OK) {
auto settings = utils::getSettings();
settings.apps.push_back(*app);
utils::saveSettings(settings);
addCheckboxToContainer(panel, appCheckboxContainer, frameSizer, size, delete_button_texture,
edit_button_texture, *app);
this->SetSizerAndFit(frameSizer);
wxSize currentSize = this->GetSize();
this->SetSize(size.GetWidth(), currentSize.GetHeight());
this->Layout();
} else
delete app;
});
checkboxRowSizer->Add(anyOtherCheckbox, 1, wxALL | wxALIGN_CENTER_VERTICAL); enabledAppsContainer->Add(appCheckboxContainer, 1, wxEXPAND, 5);
checkboxRowSizer->Add(addButton, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, 5);
vSizer->Add(appCheckboxContainer, 1, wxEXPAND, 5);
vSizer->Add(checkboxRowSizer, 1, wxALL, 5);
mainContainer->Add(enabledAppsContainer, 0, 0, 5); mainContainer->Add(enabledAppsContainer, 0, 0, 5);
// enabled apps end // enabled apps end
// LastFM start // LastFM start
auto lastfmDivider = new wxStaticLine(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL); auto lastfmDivider = new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL);
mainContainer->Add(lastfmDivider, 0, wxEXPAND | wxALL, 5); mainContainer->Add(lastfmDivider, 0, wxEXPAND | wxALL, 5);
wxBoxSizer* lastFMContainer; wxBoxSizer* lastFMContainer;
lastFMContainer = new wxBoxSizer(wxHORIZONTAL); lastFMContainer = new wxBoxSizer(wxHORIZONTAL);
auto lastfmText = new wxStaticText(panel, wxID_ANY, _("LastFM:"), wxDefaultPosition, wxDefaultSize, 0); auto lastfmText = new wxStaticText(this, wxID_ANY, _("LastFM:"), wxDefaultPosition, wxDefaultSize, 0);
lastfmText->Wrap(-1); lastfmText->Wrap(-1);
lastFMContainer->Add(lastfmText, 0, wxALL, 5); lastFMContainer->Add(lastfmText, 0, wxALL, 5);
wxBoxSizer* lastfmSettingsContainer; wxBoxSizer* lastfmSettingsContainer;
lastfmSettingsContainer = new wxBoxSizer(wxVERTICAL); lastfmSettingsContainer = new wxBoxSizer(wxVERTICAL);
auto lastfmEnabledCheckbox = new wxCheckBox(panel, wxID_ANY, _("Enabled"), wxDefaultPosition, wxDefaultSize, 0); auto lastfmEnabledCheckbox = new wxCheckBox(this, wxID_ANY, _("Enabled"), wxDefaultPosition, wxDefaultSize, 0);
lastfmEnabledCheckbox->SetValue(settings.lastfm.enabled); lastfmEnabledCheckbox->SetValue(settings.lastfm.enabled);
lastfmEnabledCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) { lastfmEnabledCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) {
bool isChecked = event.IsChecked(); bool isChecked = event.IsChecked();
@ -501,8 +338,9 @@ public:
lastfmSettingsContainer->Add(lastfmEnabledCheckbox, 0, wxALIGN_CENTER | wxALL, 5); lastfmSettingsContainer->Add(lastfmEnabledCheckbox, 0, wxALIGN_CENTER | wxALL, 5);
lastFMContainer->Add(lastfmSettingsContainer, 1, wxEXPAND, 5); lastFMContainer->Add(lastfmSettingsContainer, 1, wxEXPAND, 5);
auto usernameInput = new wxTextCtrl(panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0); auto usernameInput =
usernameInput->SetHint(_("Username")); new wxTextCtrlWithPlaceholder(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0);
usernameInput->SetPlaceholderText(_("Username"));
usernameInput->SetValue(settings.lastfm.username); usernameInput->SetValue(settings.lastfm.username);
usernameInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) { usernameInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) {
auto settings = utils::getSettings(); auto settings = utils::getSettings();
@ -510,9 +348,9 @@ public:
settings.lastfm.username = data; settings.lastfm.username = data;
utils::saveSettings(settings); utils::saveSettings(settings);
}); });
auto passwordInput = auto passwordInput = new wxTextCtrlWithPlaceholder(this, wxID_ANY, wxEmptyString, wxDefaultPosition,
new wxTextCtrl(panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_PASSWORD); wxDefaultSize, wxTE_PASSWORD);
passwordInput->SetHint(_("Password")); passwordInput->SetPlaceholderText(_("Password"));
passwordInput->SetValue(settings.lastfm.password); passwordInput->SetValue(settings.lastfm.password);
passwordInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) { passwordInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) {
auto settings = utils::getSettings(); auto settings = utils::getSettings();
@ -520,8 +358,9 @@ public:
settings.lastfm.password = data; settings.lastfm.password = data;
utils::saveSettings(settings); utils::saveSettings(settings);
}); });
auto apikeyInput = new wxTextCtrl(panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0); auto apikeyInput =
apikeyInput->SetHint(_("API-Key")); new wxTextCtrlWithPlaceholder(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0);
apikeyInput->SetPlaceholderText(_("API-Key"));
apikeyInput->SetValue(settings.lastfm.api_key); apikeyInput->SetValue(settings.lastfm.api_key);
apikeyInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) { apikeyInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) {
auto settings = utils::getSettings(); auto settings = utils::getSettings();
@ -529,9 +368,9 @@ public:
settings.lastfm.api_key = data; settings.lastfm.api_key = data;
utils::saveSettings(settings); utils::saveSettings(settings);
}); });
auto apisecretInput = auto apisecretInput = new wxTextCtrlWithPlaceholder(this, wxID_ANY, wxEmptyString, wxDefaultPosition,
new wxTextCtrl(panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_PASSWORD); wxDefaultSize, wxTE_PASSWORD);
apisecretInput->SetHint(_("API-Secret")); apisecretInput->SetPlaceholderText(_("API-Secret"));
apisecretInput->SetValue(settings.lastfm.api_secret); apisecretInput->SetValue(settings.lastfm.api_secret);
apisecretInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) { apisecretInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) {
auto settings = utils::getSettings(); auto settings = utils::getSettings();
@ -540,7 +379,7 @@ public:
utils::saveSettings(settings); utils::saveSettings(settings);
}); });
auto checkButton = new wxButton(panel, wxID_ANY, _("Check credentials")); auto checkButton = new wxButton(this, wxID_ANY, _("Check credentials"));
checkButton->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { initLastFM(true); }); checkButton->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { initLastFM(true); });
mainContainer->Add(lastFMContainer, 0, 0, 5); mainContainer->Add(lastFMContainer, 0, 0, 5);
@ -553,7 +392,7 @@ public:
// Last FM End // Last FM End
// settings start // settings start
auto appsDivider = new wxStaticLine(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL); auto appsDivider = new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL);
mainContainer->Add(appsDivider, 0, wxEXPAND | wxALL, 5); mainContainer->Add(appsDivider, 0, wxEXPAND | wxALL, 5);
wxBoxSizer* settingsContainer; wxBoxSizer* settingsContainer;
@ -562,12 +401,12 @@ public:
wxBoxSizer* startupContainer; wxBoxSizer* startupContainer;
startupContainer = new wxBoxSizer(wxHORIZONTAL); startupContainer = new wxBoxSizer(wxHORIZONTAL);
auto startupText = new wxStaticText(panel, wxID_ANY, _("Startup:"), wxDefaultPosition, wxDefaultSize, 0); auto startupText = new wxStaticText(this, wxID_ANY, _("Startup:"), wxDefaultPosition, wxDefaultSize, 0);
startupText->Wrap(-1); startupText->Wrap(-1);
startupContainer->Add(startupText, 0, wxALL, 5); startupContainer->Add(startupText, 0, wxALL, 5);
auto autostartCheckbox = auto autostartCheckbox =
new wxCheckBox(panel, wxID_ANY, _("Launch at login"), wxDefaultPosition, wxDefaultSize, 0); new wxCheckBox(this, wxID_ANY, _("Launch at login"), wxDefaultPosition, wxDefaultSize, 0);
autostartCheckbox->SetValue(settings.autoStart); autostartCheckbox->SetValue(settings.autoStart);
autostartCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) { autostartCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) {
bool isChecked = event.IsChecked(); bool isChecked = event.IsChecked();
@ -578,7 +417,7 @@ public:
}); });
auto odesliCheckbox = auto odesliCheckbox =
new wxCheckBox(panel, wxID_ANY, _("Odesli integration"), wxDefaultPosition, wxDefaultSize, 0); new wxCheckBox(this, wxID_ANY, _("Odesli integration"), wxDefaultPosition, wxDefaultSize, 0);
odesliCheckbox->SetValue(settings.odesli); odesliCheckbox->SetValue(settings.odesli);
odesliCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) { odesliCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) {
bool isChecked = event.IsChecked(); bool isChecked = event.IsChecked();
@ -591,80 +430,17 @@ public:
settingsContainer->Add(odesliCheckbox, 0, wxALL, 5); settingsContainer->Add(odesliCheckbox, 0, wxALL, 5);
startupContainer->Add(settingsContainer); startupContainer->Add(settingsContainer);
mainContainer->Add(startupContainer, 0, wxEXPAND, 5); mainContainer->Add(startupContainer, 0, wxEXPAND, 5);
// settings end // settings end
this->SetSizerAndFit(mainContainer);
this->SetSizerAndFit(frameSizer);
wxSize currentSize = this->GetSize(); wxSize currentSize = this->GetSize();
this->SetSize(size.GetWidth(), currentSize.GetHeight()); this->SetSize(size.GetWidth(), currentSize.GetHeight());
this->Layout(); this->Layout();
this->Centre(wxBOTH); this->Centre(wxBOTH);
panel->SetFocus();
}
private:
void addCheckboxToContainer(wxPanel* panel, wxBoxSizer* container, wxBoxSizer* frameSizer, const wxSize& size,
const wxBitmap& delete_button_texture, const wxBitmap& edit_button_texture,
const utils::App& app) {
wxBoxSizer* checkboxRowSizer = new wxBoxSizer(wxHORIZONTAL);
auto checkbox = new wxCheckBox(panel, 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());
});
wxBitmapButton* editButton = new wxBitmapButton(panel, wxID_ANY, edit_button_texture);
editButton->SetToolTip(_("Edit application"));
editButton->Bind(wxEVT_BUTTON, [this, checkbox](wxCommandEvent& event) {
utils::App* appData = static_cast<utils::App*>(checkbox->GetClientData());
utils::App backupApp = *appData;
EditAppDialog dlg{this, _("Edit application") + " " + appData->appName, appData};
if (dlg.ShowModal() == wxID_OK) {
auto settings = utils::getSettings();
for (auto& app : settings.apps) {
if (app == backupApp)
app = *appData;
}
utils::saveSettings(settings);
checkbox->SetLabelText(appData->appName);
this->Layout();
}
});
wxBitmapButton* deleteButton = new wxBitmapButton(panel, wxID_ANY, delete_button_texture);
deleteButton->SetToolTip(_("Delete application"));
deleteButton->Bind(wxEVT_BUTTON,
[this, checkboxRowSizer, container, frameSizer, checkbox, size](wxCommandEvent& event) {
utils::App* appData = static_cast<utils::App*>(checkbox->GetClientData());
auto settings = utils::getSettings();
settings.apps.erase(std::find(settings.apps.begin(), settings.apps.end(), *appData));
utils::saveSettings(settings);
container->Detach(checkboxRowSizer);
this->CallAfter([this, checkboxRowSizer, frameSizer, size]() {
checkboxRowSizer->Clear(true);
this->SetSizerAndFit(frameSizer);
wxSize currentSize = this->GetSize();
this->SetSize(size.GetWidth(), currentSize.GetHeight());
this->Layout();
});
});
checkboxRowSizer->Add(checkbox, 1, wxALL | wxALIGN_CENTER_VERTICAL);
checkboxRowSizer->Add(editButton, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, 5);
checkboxRowSizer->Add(deleteButton, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, 5);
container->Add(checkboxRowSizer, 0, wxALL, 5);
} }
}; };
class PlayerLink : public wxApp { class PlayerLink : public wxApp {
public: public:
virtual bool OnInit() override { virtual bool OnInit() override {
@ -677,8 +453,8 @@ public:
this->SetAppearance(wxAppBase::Appearance::Dark); this->SetAppearance(wxAppBase::Appearance::Dark);
wxInitAllImageHandlers(); wxInitAllImageHandlers();
wxIcon tray_icon = utils::loadIconFromMemory(menubar_icon_png, menubar_icon_png_size); wxIcon icon = utils::loadIconFromMemory(icon_png, icon_png_size);
PlayerLinkFrame* frame = new PlayerLinkFrame(nullptr, wxID_ANY, _("PlayerLink")); PlayerLinkFrame* frame = new PlayerLinkFrame(nullptr, icon, wxID_ANY, _("PlayerLink"));
trayIcon = new PlayerLinkIcon(frame); trayIcon = new PlayerLinkIcon(frame);
frame->Bind(wxEVT_CLOSE_WINDOW, [=](wxCloseEvent& event) { frame->Bind(wxEVT_CLOSE_WINDOW, [=](wxCloseEvent& event) {
if (event.CanVeto()) { if (event.CanVeto()) {
@ -688,7 +464,7 @@ public:
std::exit(0); std::exit(0);
}); });
trayIcon->SetIcon(tray_icon, _("PlayerLink")); trayIcon->SetIcon(icon, _("PlayerLink"));
return true; return true;
} }

View File

@ -4,6 +4,7 @@
#include <wx/mstream.h> #include <wx/mstream.h>
#include <wx/wx.h> #include <wx/wx.h>
#include <wx/clipbrd.h> #include <wx/clipbrd.h>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <nlohmann-json/single_include/nlohmann/json.hpp> #include <nlohmann-json/single_include/nlohmann/json.hpp>
@ -20,14 +21,10 @@
namespace utils { namespace utils {
struct App { struct App {
bool enabled; bool enabled;
int type;
std::string appName; std::string appName;
std::string clientId; std::string clientId;
std::string searchEndpoint; std::string searchEndpoint;
std::vector<std::string> processNames; std::vector<std::string> processNames;
bool operator==(const App& other) const {
return appName == other.appName && clientId == other.clientId && type == other.type;
}
}; };
struct LastFMSettings { struct LastFMSettings {
@ -59,57 +56,18 @@ namespace utils {
} }
} }
inline wxBitmap loadImageFromMemory(const unsigned char* data, size_t size, int width = 0, int height = 0) { inline wxIcon loadIconFromMemory(const unsigned char* data, size_t size) {
wxMemoryInputStream stream(data, size); wxMemoryInputStream stream(data, size);
wxImage img(stream, wxBITMAP_TYPE_PNG); wxImage img(stream, wxBITMAP_TYPE_PNG);
if (img.IsOk()) { if (img.IsOk()) {
if (width != 0 || height != 0)
img.Rescale(width, height, wxIMAGE_QUALITY_HIGH);
wxBitmap bmp(img); wxBitmap bmp(img);
return bmp; wxIcon icon;
icon.CopyFromBitmap(bmp);
return icon;
} }
return wxNullBitmap; return wxNullIcon;
} }
inline wxIcon loadIconFromMemory(const unsigned char* data, size_t size, int width = 0, int height = 0) {
wxIcon icn{};
icn.CopyFromBitmap(loadImageFromMemory(data, size, width, height));
return icn;
}
inline wxBitmap loadColoredSVG(const unsigned char* svg, const unsigned int svg_size, const wxSize& size,
const wxColour& color) {
const std::string defaultColor = "currentColor";
std::string svg_data = std::string((const char*)svg, svg_size);
size_t start_pos = svg_data.find(defaultColor);
if (start_pos != std::string::npos)
svg_data.replace(start_pos, defaultColor.length(), color.GetAsString(wxC2S_HTML_SYNTAX));
wxBitmapBundle bundle = wxBitmapBundle::FromSVG(svg_data.c_str(), size);
if (!bundle.IsOk())
return wxNullBitmap;
wxBitmap bmp = bundle.GetBitmap(size);
if (!bmp.IsOk())
return wxNullBitmap;
return bmp;
}
inline wxBitmap loadSettingsIcon(const unsigned char* svg, const unsigned int svg_size,
const wxSize& size = wxSize(16, 16)) {
return loadColoredSVG(
svg, svg_size, size,
wxSystemSettings::GetAppearance().IsSystemDark() ? wxColor(255, 255, 255, 255) : wxColor(0, 0, 0, 255));
}
inline std::string toLower(const std::string& str) {
std::string lowerStr = str;
std::transform(lowerStr.begin(), lowerStr.end(), lowerStr.begin(),
[](unsigned char c) { return std::tolower(c); });
return lowerStr;
}
inline bool caseInsensitiveMatch(const std::string& a, const std::string& b) { return toLower(a) == toLower(b); }
inline std::string ltrim(std::string& s) { inline std::string ltrim(std::string& s) {
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { return !std::isspace(ch); })); s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { return !std::isspace(ch); }));
return s; return s;
@ -186,7 +144,6 @@ namespace utils {
SongInfo ret{}; SongInfo ret{};
std::string response = std::string response =
httpRequest("https://itunes.apple.com/search?media=music&entity=song&term=" + urlEncode(query)); httpRequest("https://itunes.apple.com/search?media=music&entity=song&term=" + urlEncode(query));
try {
nlohmann::json j = nlohmann::json::parse(response); nlohmann::json j = nlohmann::json::parse(response);
auto results = j["results"]; auto results = j["results"];
if (results.size() > 0) { if (results.size() > 0) {
@ -194,9 +151,6 @@ namespace utils {
ret.trackId = results[0]["trackId"].get<int64_t>(); ret.trackId = results[0]["trackId"].get<int64_t>();
} }
return ret; return ret;
} catch (...) {
return ret;
}
} }
inline std::string getOdesliURL(SongInfo& song) { inline std::string getOdesliURL(SongInfo& song) {
@ -270,7 +224,7 @@ namespace utils {
appJson["client_id"] = app.clientId; appJson["client_id"] = app.clientId;
appJson["search_endpoint"] = app.searchEndpoint; appJson["search_endpoint"] = app.searchEndpoint;
appJson["enabled"] = app.enabled; appJson["enabled"] = app.enabled;
appJson["type"] = app.type;
for (const auto& processName : app.processNames) appJson["process_names"].push_back(processName); for (const auto& processName : app.processNames) appJson["process_names"].push_back(processName);
j["apps"].push_back(appJson); j["apps"].push_back(appJson);
@ -315,9 +269,8 @@ namespace utils {
a.clientId = app.value("client_id", ""); a.clientId = app.value("client_id", "");
a.searchEndpoint = app.value("search_endpoint", ""); a.searchEndpoint = app.value("search_endpoint", "");
a.enabled = app.value("enabled", false); a.enabled = app.value("enabled", false);
a.type = app.value("type", 2);
for (const auto& process : app.value("process_names", nlohmann::json())) a.processNames.push_back(process.get<std::string>()); for (const auto& process : app["process_names"]) a.processNames.push_back(process.get<std::string>());
ret.apps.push_back(a); ret.apps.push_back(a);
} }
@ -330,7 +283,7 @@ namespace utils {
auto settings = getSettings(); auto settings = getSettings();
for (auto app : settings.apps) { for (auto app : settings.apps) {
for (auto procName : app.processNames) { for (auto procName : app.processNames) {
if (caseInsensitiveMatch(procName, processName)) if (procName == processName)
return app; return app;
} }
} }
@ -338,7 +291,6 @@ namespace utils {
a.clientId = DEFAULT_CLIENT_ID; a.clientId = DEFAULT_CLIENT_ID;
a.appName = DEFAULT_APP_NAME; a.appName = DEFAULT_APP_NAME;
a.enabled = settings.anyOtherEnabled; a.enabled = settings.anyOtherEnabled;
a.type = 2; // Default to listening
a.searchEndpoint = ""; a.searchEndpoint = "";
return a; return a;
} }

View File

@ -19,8 +19,6 @@ set(wxBUILD_SHARED OFF)
set(wxBUILD_MONOLITHIC ON) set(wxBUILD_MONOLITHIC ON)
set(wxUSE_GUI ON) set(wxUSE_GUI ON)
set(wxUSE_WEBVIEW OFF) set(wxUSE_WEBVIEW OFF)
set(wxUSE_UNICODE_UTF8 ON)
set(wxUSE_UTF8_LOCALE_ONLY ON)
add_subdirectory("wxWidgets") add_subdirectory("wxWidgets")
if(UNIX AND NOT APPLE) if(UNIX AND NOT APPLE)
add_subdirectory("dbus") add_subdirectory("dbus")

2
vendor/discord-rpc vendored

@ -1 +1 @@
Subproject commit 6bc42071f109083178ae824898ca4b3cb40a8307 Subproject commit e86d7a81de7a33323e2038182ab53a26c69f7880

2
vendor/wxWidgets vendored

@ -1 +1 @@
Subproject commit d7a696de4c301948f02dc5d7979f867f07f9d684 Subproject commit 138937b7775c117b57f55374a0c507a35a1102f6

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 260 KiB