Compare commits
26 Commits
c35416c928
...
7d4bc6dd89
Author | SHA1 | Date |
---|---|---|
|
7d4bc6dd89 | |
|
fa29020cea | |
|
36be257462 | |
|
958def7527 | |
|
28a198983c | |
|
374a95e68d | |
|
49e1bb0262 | |
|
cfecd99858 | |
|
c805dfe829 | |
|
c3bf12a997 | |
|
5428f57891 | |
|
66a25e62f2 | |
|
6807fe00fa | |
|
4e57f272a1 | |
|
7fc1ac5d5d | |
|
a6f5d3277b | |
|
563080643f | |
|
90607d2f5d | |
|
cd5656de63 | |
|
c53f02b35c | |
|
dbbec3b027 | |
|
bccf4c6100 | |
|
349058c32d | |
|
9821b6a294 | |
|
8fb4af3a49 | |
|
191fd2c375 |
|
@ -10,7 +10,7 @@ on:
|
|||
|
||||
jobs:
|
||||
build-linux:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
@ -46,7 +46,7 @@ jobs:
|
|||
run: cmake -B build -S . -DCMAKE_BUILD_TYPE=Release
|
||||
|
||||
- name: Build the project
|
||||
run: cmake --build build --config Release
|
||||
run: cmake --build build --config Release --target PlayerLink --parallel $(nproc)
|
||||
|
||||
- name: Download linuxdeploy
|
||||
run: |
|
||||
|
@ -66,6 +66,63 @@ jobs:
|
|||
name: PlayerLink-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:
|
||||
runs-on: windows-latest
|
||||
|
||||
|
@ -85,12 +142,47 @@ jobs:
|
|||
run: cmake -B build -S . -DCMAKE_BUILD_TYPE=Release
|
||||
|
||||
- name: Build the project
|
||||
run: cmake --build build --config Release
|
||||
run: cmake --build build --config Release --target PlayerLink
|
||||
|
||||
- name: Upload Windows artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: PlayerLink-Windows-Executable
|
||||
name: PlayerLink-Windows-AMD64
|
||||
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/*
|
||||
|
||||
build-macos:
|
||||
|
@ -112,7 +204,7 @@ jobs:
|
|||
run: cmake -B build -S . -DCMAKE_BUILD_TYPE=Release
|
||||
|
||||
- name: Build the project
|
||||
run: cmake --build build --config Release
|
||||
run: cmake --build build --config Release --target PlayerLink --parallel $(sysctl -n hw.physicalcpu)
|
||||
|
||||
- name: Create DMG package
|
||||
run: |
|
||||
|
@ -128,7 +220,7 @@ jobs:
|
|||
|
||||
create-release:
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
needs: [build-linux, build-windows, build-macos]
|
||||
needs: [build-linux, build-linux-arm64, build-windows, build-windows-arm64, build-macos]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
|
@ -148,6 +240,7 @@ jobs:
|
|||
release_name: Release ${{ github.ref_name }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
- name: Upload AppImage
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
|
@ -158,6 +251,16 @@ jobs:
|
|||
asset_name: PlayerLink-x86_64.AppImage
|
||||
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
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
|
@ -168,6 +271,16 @@ jobs:
|
|||
asset_name: PlayerLink.exe
|
||||
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
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
cmake_minimum_required (VERSION 3.8)
|
||||
cmake_minimum_required (VERSION 3.12)
|
||||
include("cmake/create_resources.cmake")
|
||||
|
||||
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
|
||||
if(APPLE)
|
||||
list(APPEND SOURCES "src/backends/darwin.mm" ${CMAKE_SOURCE_DIR}/osx/icon.icns)
|
||||
list(APPEND SOURCES "src/backends/darwin.mm" ${CMAKE_SOURCE_DIR}/osx/icon.icns ${CMAKE_SOURCE_DIR}/osx/MediaRemote.js)
|
||||
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "" FORCE)
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "" FORCE)
|
||||
project ("PlayerLink" LANGUAGES C CXX OBJCXX)
|
||||
|
@ -40,6 +40,7 @@ elseif(APPLE)
|
|||
endif()
|
||||
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/MediaRemote.js PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
|
||||
set_target_properties(PlayerLink PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/osx/Info.plist)
|
||||
elseif(UNIX AND NOT APPLE)
|
||||
list(APPEND LIBRARIES dbus)
|
||||
|
|
|
@ -80,13 +80,13 @@ An example on how to add custom apps to the config can be found [here](./setting
|
|||
4. Build the project :)
|
||||
```bash
|
||||
# for a release build
|
||||
cmake --build build --config Release
|
||||
cmake --build build --config Release --target PlayerLink
|
||||
# for a debug build
|
||||
cmake --build build
|
||||
cmake --build build --target PlayerLink
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
## Credits
|
||||
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.
|
||||
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.
|
BIN
img/linux.png
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 263 KiB |
BIN
img/macos.png
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 692 KiB |
BIN
img/windows.png
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 285 KiB After Width: | Height: | Size: 41 KiB |
|
@ -0,0 +1,33 @@
|
|||
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() });
|
||||
}
|
BIN
osx/icon.icns
BIN
rsrc/icon.png
Before Width: | Height: | Size: 285 KiB After Width: | Height: | Size: 817 KiB |
After Width: | Height: | Size: 34 KiB |
|
@ -0,0 +1,3 @@
|
|||
<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>
|
After Width: | Height: | Size: 367 B |
|
@ -0,0 +1,3 @@
|
|||
<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>
|
After Width: | Height: | Size: 239 B |
|
@ -0,0 +1,3 @@
|
|||
<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>
|
After Width: | Height: | Size: 751 B |
|
@ -5,13 +5,34 @@
|
|||
"client_id": "1245257414715113573",
|
||||
"enabled": true,
|
||||
"name": "Spotify",
|
||||
"type": 2,
|
||||
"process_names": [
|
||||
"org.mpris.MediaPlayer2.spotify",
|
||||
"com.spotify.client",
|
||||
"Spotify.exe"
|
||||
],
|
||||
"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
|
||||
}
|
|
@ -1,9 +1,11 @@
|
|||
#include <Foundation/NSObjCRuntime.h>
|
||||
#ifdef __APPLE__
|
||||
#include <AppKit/AppKit.h>
|
||||
#include <Cocoa/Cocoa.h>
|
||||
#include <Foundation/Foundation.h>
|
||||
#include <dispatch/dispatch.h>
|
||||
#include <filesystem>
|
||||
#include <nlohmann-json/single_include/nlohmann/json.hpp>
|
||||
#include <fstream>
|
||||
|
||||
#include "../MediaRemote.hpp"
|
||||
|
@ -16,65 +18,120 @@ void hideDockIcon(bool shouldHide) {
|
|||
[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() {
|
||||
__block NSString *appName = nil;
|
||||
__block NSDictionary *playingInfo = nil;
|
||||
// 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]);
|
||||
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
std::string appName = j["player"].get<std::string>();
|
||||
if (appName == "none")
|
||||
return nullptr;
|
||||
|
||||
dispatch_group_enter(group);
|
||||
MRMediaRemoteGetNowPlayingApplicationPID(dispatch_get_main_queue(), ^(pid_t pid) {
|
||||
if (pid > 0) {
|
||||
NSRunningApplication *app = [NSRunningApplication runningApplicationWithProcessIdentifier:pid];
|
||||
if (app)
|
||||
appName = [[app.bundleIdentifier copy] retain];
|
||||
}
|
||||
dispatch_group_leave(group);
|
||||
});
|
||||
bool paused = j["playbackStatus"].get<int>() == 0;
|
||||
|
||||
dispatch_group_enter(group);
|
||||
MRMediaRemoteGetNowPlayingInfo(dispatch_get_main_queue(), ^(CFDictionaryRef result) {
|
||||
if (result)
|
||||
playingInfo = [[(__bridge NSDictionary *)result copy] retain];
|
||||
dispatch_group_leave(group);
|
||||
});
|
||||
std::string songTitle = j["title"].get<std::string>();
|
||||
|
||||
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
|
||||
dispatch_release(group);
|
||||
if (appName == nil || playingInfo == nil)
|
||||
return nullptr;
|
||||
std::string songAlbum = j["album"].get<std::string>();
|
||||
|
||||
bool paused = [playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoPlaybackRate] intValue] == 0;
|
||||
std::string songArtist = j["artist"].get<std::string>();
|
||||
|
||||
NSString *title = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoTitle];
|
||||
std::string songTitle = title ? [title UTF8String] : "";
|
||||
int64_t elapsedTimeMs = 0;
|
||||
int64_t durationMs = 0;
|
||||
try {
|
||||
double durationNumber = j["duration"].get<double>();
|
||||
durationMs = static_cast<int64_t>(durationNumber * 1000);
|
||||
|
||||
NSString *album = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoAlbum];
|
||||
std::string songAlbum = album ? [album UTF8String] : "";
|
||||
double elapsedTimeNumber = j["elapsed"].get<double>();
|
||||
elapsedTimeMs = static_cast<int64_t>(elapsedTimeNumber * 1000);
|
||||
} catch (...) {
|
||||
}
|
||||
|
||||
NSString *artist = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoArtist];
|
||||
std::string songArtist = artist ? [artist UTF8String] : "";
|
||||
return std::make_shared<MediaInfo>(paused, songTitle, songArtist, songAlbum, appName, "", durationMs,
|
||||
elapsedTimeMs);
|
||||
} else {
|
||||
__block NSString *appName = nil;
|
||||
__block NSDictionary *playingInfo = nil;
|
||||
|
||||
NSData *artworkData = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoArtworkData];
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
|
||||
std::string thumbnailData;
|
||||
if (artworkData)
|
||||
thumbnailData = std::string((const char *)[artworkData bytes], [artworkData length]);
|
||||
dispatch_group_enter(group);
|
||||
MRMediaRemoteGetNowPlayingApplicationPID(dispatch_get_main_queue(), ^(pid_t pid) {
|
||||
if (pid > 0) {
|
||||
NSRunningApplication *app = [NSRunningApplication runningApplicationWithProcessIdentifier:pid];
|
||||
if (app)
|
||||
appName = [[app.bundleIdentifier copy] retain];
|
||||
}
|
||||
dispatch_group_leave(group);
|
||||
});
|
||||
|
||||
NSNumber *elapsedTimeNumber = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoElapsedTime];
|
||||
dispatch_group_enter(group);
|
||||
MRMediaRemoteGetNowPlayingInfo(dispatch_get_main_queue(), ^(CFDictionaryRef result) {
|
||||
if (result)
|
||||
playingInfo = [[(__bridge NSDictionary *)result copy] retain];
|
||||
dispatch_group_leave(group);
|
||||
});
|
||||
|
||||
NSNumber *durationNumber = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoDuration];
|
||||
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
|
||||
dispatch_release(group);
|
||||
if (appName == nil || playingInfo == nil)
|
||||
return nullptr;
|
||||
|
||||
int64_t elapsedTimeMs = elapsedTimeNumber ? static_cast<int64_t>([elapsedTimeNumber doubleValue] * 1000) : 0;
|
||||
bool paused = [playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoPlaybackRate] intValue] == 0;
|
||||
|
||||
int64_t durationMs = durationNumber ? static_cast<int64_t>([durationNumber doubleValue] * 1000) : 0;
|
||||
NSString *title = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoTitle];
|
||||
std::string songTitle = title ? [title UTF8String] : "";
|
||||
|
||||
std::string appNameString = appName.UTF8String;
|
||||
NSString *album = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoAlbum];
|
||||
std::string songAlbum = album ? [album UTF8String] : "";
|
||||
|
||||
[appName release];
|
||||
[playingInfo release];
|
||||
return std::make_shared<MediaInfo>(paused, songTitle, songArtist, songAlbum, appNameString, thumbnailData,
|
||||
durationMs, elapsedTimeMs);
|
||||
NSString *artist = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoArtist];
|
||||
std::string songArtist = artist ? [artist UTF8String] : "";
|
||||
|
||||
NSData *artworkData = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoArtworkData];
|
||||
|
||||
std::string thumbnailData;
|
||||
if (artworkData)
|
||||
thumbnailData = std::string((const char *)[artworkData bytes], [artworkData length]);
|
||||
|
||||
NSNumber *elapsedTimeNumber = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoElapsedTime];
|
||||
|
||||
NSNumber *durationNumber = playingInfo[(__bridge NSString *)kMRMediaRemoteNowPlayingInfoDuration];
|
||||
|
||||
int64_t elapsedTimeMs = elapsedTimeNumber ? static_cast<int64_t>([elapsedTimeNumber doubleValue] * 1000) : 0;
|
||||
|
||||
int64_t durationMs = durationNumber ? static_cast<int64_t>([durationNumber doubleValue] * 1000) : 0;
|
||||
|
||||
std::string appNameString = appName.UTF8String;
|
||||
|
||||
[appName release];
|
||||
[playingInfo release];
|
||||
return std::make_shared<MediaInfo>(paused, songTitle, songArtist, songAlbum, appNameString, thumbnailData,
|
||||
durationMs, elapsedTimeMs);
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::path backend::getConfigDirectory() {
|
||||
|
@ -88,14 +145,14 @@ bool backend::toggleAutostart(bool enabled) {
|
|||
launchAgentPath = launchAgentPath / "Library" / "LaunchAgents";
|
||||
std::filesystem::create_directories(launchAgentPath);
|
||||
launchAgentPath = launchAgentPath / "PlayerLink.plist";
|
||||
|
||||
|
||||
if (!enabled && std::filesystem::exists(launchAgentPath)) {
|
||||
std::filesystem::remove(launchAgentPath);
|
||||
return true;
|
||||
}
|
||||
NSString *binaryPath = [[[NSProcessInfo processInfo] arguments][0] stringByStandardizingPath];
|
||||
|
||||
//I would also like to use std::format here, but well I also want to support older mac os versions.
|
||||
// I would also like to use std::format here, but well I also want to support older mac os versions.
|
||||
std::string formattedPlist =
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" "
|
||||
"\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n <dict>\n\n "
|
||||
|
|
|
@ -35,59 +35,6 @@ std::string toStdString(winrt::hstring& in) {
|
|||
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];
|
||||
|
@ -145,57 +92,55 @@ std::shared_ptr<MediaInfo> backend::getMediaInformation() {
|
|||
return nullptr;
|
||||
|
||||
auto playbackInfo = currentSession.GetPlaybackInfo();
|
||||
auto mediaProperties = currentSession.TryGetMediaPropertiesAsync().get();
|
||||
auto timelineInformation = currentSession.GetTimelineProperties();
|
||||
if (!mediaProperties)
|
||||
try {
|
||||
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());
|
||||
|
||||
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);
|
||||
} catch (...) {
|
||||
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() {
|
||||
|
|
|
@ -44,13 +44,17 @@ public:
|
|||
parameters["api_sig"] = getApiSignature(parameters);
|
||||
std::string postBody = utils::getURLEncodedPostBody(parameters);
|
||||
std::string response = utils::httpRequest(api_base, "POST", postBody);
|
||||
auto j = nlohmann::json::parse(response);
|
||||
if (j.contains("error"))
|
||||
return j["error"].get<LASTFM_STATUS>();
|
||||
try {
|
||||
auto j = nlohmann::json::parse(response);
|
||||
if (j.contains("error"))
|
||||
return j["error"].get<LASTFM_STATUS>();
|
||||
|
||||
session_token = j["session"]["key"].get<std::string>();
|
||||
authenticated = true;
|
||||
return LASTFM_STATUS::SUCCESS;
|
||||
session_token = j["session"]["key"].get<std::string>();
|
||||
authenticated = true;
|
||||
return LASTFM_STATUS::SUCCESS;
|
||||
} catch (...) {
|
||||
return LASTFM_STATUS::UNKNOWN_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
LASTFM_STATUS scrobble(std::string artist, std::string track) {
|
||||
|
|
494
src/main.cpp
|
@ -3,15 +3,18 @@
|
|||
#include <wx/mstream.h>
|
||||
#include <wx/statline.h>
|
||||
#include <wx/taskbar.h>
|
||||
#include <wx/hyperlink.h>
|
||||
#include <wx/wx.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <thread>
|
||||
|
||||
#include "backend.hpp"
|
||||
#include "lastfm.hpp"
|
||||
#include "rsrc.hpp"
|
||||
#include "utils.hpp"
|
||||
#include "wx/sizer.h"
|
||||
|
||||
std::string lastPlayingSong = "";
|
||||
std::string lastMediaSource = "";
|
||||
|
@ -48,17 +51,22 @@ void initLastFM(bool checkMode = false) {
|
|||
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)
|
||||
if (status) {
|
||||
delete lastfm;
|
||||
lastfm = nullptr;
|
||||
if (checkMode)
|
||||
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));
|
||||
if (!lastfm)
|
||||
initLastFM();
|
||||
|
||||
auto mediaInformation = backend::getMediaInformation();
|
||||
auto settings = utils::getSettings();
|
||||
if (!mediaInformation) {
|
||||
|
@ -85,14 +93,6 @@ void handleMediaTasks() {
|
|||
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) {
|
||||
|
@ -102,6 +102,14 @@ void handleMediaTasks() {
|
|||
|
||||
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) {
|
||||
Discord_ClearPresence();
|
||||
continue;
|
||||
|
@ -110,7 +118,7 @@ void handleMediaTasks() {
|
|||
|
||||
std::string activityState = "by " + mediaInformation->songArtist;
|
||||
DiscordRichPresence activity{};
|
||||
activity.type = ActivityType::LISTENING;
|
||||
activity.type = app.type;
|
||||
activity.details = mediaInformation->songTitle.c_str();
|
||||
activity.state = activityState.c_str();
|
||||
activity.smallImageText = serviceName.c_str();
|
||||
|
@ -143,7 +151,7 @@ void handleMediaTasks() {
|
|||
}
|
||||
|
||||
std::string odesliUrl = utils::getOdesliURL(songInfo);
|
||||
if(settings.odesli && songInfo.artworkURL != "") {
|
||||
if (settings.odesli && songInfo.artworkURL != "") {
|
||||
activity.button2name = "Show on Song.link";
|
||||
activity.button2link = odesliUrl.c_str();
|
||||
}
|
||||
|
@ -151,27 +159,202 @@ void handleMediaTasks() {
|
|||
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 {
|
||||
public:
|
||||
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);
|
||||
}
|
||||
PlayerLinkIcon(wxFrame* s) : settingsFrame(s), aboutDlg(nullptr) {}
|
||||
|
||||
protected:
|
||||
virtual wxMenu* CreatePopupMenu() override {
|
||||
wxMenu* menu = new wxMenu;
|
||||
menu->Append(10004, currentSongTitle == "" ? _("Not Playing") : wxString::FromUTF8(currentSongTitle));
|
||||
menu->Append(10004, currentSongTitle == "" ? _("Not Playing") : currentSongTitle);
|
||||
menu->Enable(10004, false);
|
||||
menu->AppendSeparator();
|
||||
menu->Append(10005, _("Copy Odesli URL"));
|
||||
|
@ -189,115 +372,73 @@ protected:
|
|||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
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); }
|
||||
AboutDialog aboutDlg;
|
||||
};
|
||||
|
||||
class PlayerLinkFrame : public wxFrame {
|
||||
public:
|
||||
PlayerLinkFrame(wxWindow* parent, wxIcon& icon, wxWindowID id = wxID_ANY, const wxString& title = wxEmptyString,
|
||||
PlayerLinkFrame(wxWindow* parent, 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);
|
||||
SetWindowIcon(this);
|
||||
|
||||
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
|
||||
auto settingsText = new wxStaticText(this, wxID_ANY, _("Settings"), wxDefaultPosition, wxDefaultSize, 0);
|
||||
auto settingsText = new wxStaticText(panel, 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);
|
||||
auto settingsDivider = new wxStaticLine(panel, 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);
|
||||
new wxStaticText(panel, wxID_ANY, _("Enabled Apps:"), wxDefaultPosition, wxDefaultSize, 0);
|
||||
enabledAppsText->Wrap(-1);
|
||||
enabledAppsContainer->Add(enabledAppsText, 0, wxALL, 5);
|
||||
|
||||
wxBoxSizer* appCheckboxContainer;
|
||||
appCheckboxContainer = new wxBoxSizer(wxVERTICAL);
|
||||
const auto edit_button_texture = utils::loadSettingsIcon(pencil_svg, pencil_svg_size);
|
||||
const auto delete_button_texture = utils::loadSettingsIcon(trash_svg, trash_svg_size);
|
||||
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();
|
||||
|
||||
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);
|
||||
for (const auto& app : settings.apps) {
|
||||
addCheckboxToContainer(panel, appCheckboxContainer, frameSizer, size, delete_button_texture,
|
||||
edit_button_texture, app);
|
||||
}
|
||||
|
||||
auto anyOtherCheckbox = new wxCheckBox(this, wxID_ANY, _("Any other"), wxDefaultPosition, wxDefaultSize, 0);
|
||||
wxBoxSizer* checkboxRowSizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto anyOtherCheckbox = new wxCheckBox(panel, wxID_ANY, _("Any other"), wxDefaultPosition, wxDefaultSize, 0);
|
||||
anyOtherCheckbox->SetValue(settings.anyOtherEnabled);
|
||||
anyOtherCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) {
|
||||
bool isChecked = event.IsChecked();
|
||||
|
@ -306,28 +447,50 @@ public:
|
|||
utils::saveSettings(settings);
|
||||
});
|
||||
|
||||
appCheckboxContainer->Add(anyOtherCheckbox, 0, wxALL, 5);
|
||||
wxBitmapButton* addButton = new wxBitmapButton(panel, wxID_ANY, add_button_texture);
|
||||
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;
|
||||
});
|
||||
|
||||
enabledAppsContainer->Add(appCheckboxContainer, 1, wxEXPAND, 5);
|
||||
checkboxRowSizer->Add(anyOtherCheckbox, 1, wxALL | wxALIGN_CENTER_VERTICAL);
|
||||
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);
|
||||
// enabled apps end
|
||||
|
||||
// LastFM start
|
||||
auto lastfmDivider = new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL);
|
||||
auto lastfmDivider = new wxStaticLine(panel, 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);
|
||||
auto lastfmText = new wxStaticText(panel, 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);
|
||||
auto lastfmEnabledCheckbox = new wxCheckBox(panel, wxID_ANY, _("Enabled"), wxDefaultPosition, wxDefaultSize, 0);
|
||||
lastfmEnabledCheckbox->SetValue(settings.lastfm.enabled);
|
||||
lastfmEnabledCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) {
|
||||
bool isChecked = event.IsChecked();
|
||||
|
@ -338,9 +501,8 @@ public:
|
|||
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"));
|
||||
auto usernameInput = new wxTextCtrl(panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0);
|
||||
usernameInput->SetHint(_("Username"));
|
||||
usernameInput->SetValue(settings.lastfm.username);
|
||||
usernameInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) {
|
||||
auto settings = utils::getSettings();
|
||||
|
@ -348,9 +510,9 @@ public:
|
|||
settings.lastfm.username = data;
|
||||
utils::saveSettings(settings);
|
||||
});
|
||||
auto passwordInput = new wxTextCtrlWithPlaceholder(this, wxID_ANY, wxEmptyString, wxDefaultPosition,
|
||||
wxDefaultSize, wxTE_PASSWORD);
|
||||
passwordInput->SetPlaceholderText(_("Password"));
|
||||
auto passwordInput =
|
||||
new wxTextCtrl(panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_PASSWORD);
|
||||
passwordInput->SetHint(_("Password"));
|
||||
passwordInput->SetValue(settings.lastfm.password);
|
||||
passwordInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) {
|
||||
auto settings = utils::getSettings();
|
||||
|
@ -358,9 +520,8 @@ public:
|
|||
settings.lastfm.password = data;
|
||||
utils::saveSettings(settings);
|
||||
});
|
||||
auto apikeyInput =
|
||||
new wxTextCtrlWithPlaceholder(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0);
|
||||
apikeyInput->SetPlaceholderText(_("API-Key"));
|
||||
auto apikeyInput = new wxTextCtrl(panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, 0);
|
||||
apikeyInput->SetHint(_("API-Key"));
|
||||
apikeyInput->SetValue(settings.lastfm.api_key);
|
||||
apikeyInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) {
|
||||
auto settings = utils::getSettings();
|
||||
|
@ -368,9 +529,9 @@ public:
|
|||
settings.lastfm.api_key = data;
|
||||
utils::saveSettings(settings);
|
||||
});
|
||||
auto apisecretInput = new wxTextCtrlWithPlaceholder(this, wxID_ANY, wxEmptyString, wxDefaultPosition,
|
||||
wxDefaultSize, wxTE_PASSWORD);
|
||||
apisecretInput->SetPlaceholderText(_("API-Secret"));
|
||||
auto apisecretInput =
|
||||
new wxTextCtrl(panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_PASSWORD);
|
||||
apisecretInput->SetHint(_("API-Secret"));
|
||||
apisecretInput->SetValue(settings.lastfm.api_secret);
|
||||
apisecretInput->Bind(wxEVT_TEXT, [this](wxCommandEvent& event) {
|
||||
auto settings = utils::getSettings();
|
||||
|
@ -379,7 +540,7 @@ public:
|
|||
utils::saveSettings(settings);
|
||||
});
|
||||
|
||||
auto checkButton = new wxButton(this, wxID_ANY, _("Check credentials"));
|
||||
auto checkButton = new wxButton(panel, wxID_ANY, _("Check credentials"));
|
||||
checkButton->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) { initLastFM(true); });
|
||||
mainContainer->Add(lastFMContainer, 0, 0, 5);
|
||||
|
||||
|
@ -392,7 +553,7 @@ public:
|
|||
// Last FM End
|
||||
|
||||
// settings start
|
||||
auto appsDivider = new wxStaticLine(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL);
|
||||
auto appsDivider = new wxStaticLine(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLI_HORIZONTAL);
|
||||
mainContainer->Add(appsDivider, 0, wxEXPAND | wxALL, 5);
|
||||
|
||||
wxBoxSizer* settingsContainer;
|
||||
|
@ -401,12 +562,12 @@ public:
|
|||
wxBoxSizer* startupContainer;
|
||||
startupContainer = new wxBoxSizer(wxHORIZONTAL);
|
||||
|
||||
auto startupText = new wxStaticText(this, wxID_ANY, _("Startup:"), wxDefaultPosition, wxDefaultSize, 0);
|
||||
auto startupText = new wxStaticText(panel, wxID_ANY, _("Startup:"), wxDefaultPosition, wxDefaultSize, 0);
|
||||
startupText->Wrap(-1);
|
||||
startupContainer->Add(startupText, 0, wxALL, 5);
|
||||
|
||||
auto autostartCheckbox =
|
||||
new wxCheckBox(this, wxID_ANY, _("Launch at login"), wxDefaultPosition, wxDefaultSize, 0);
|
||||
new wxCheckBox(panel, wxID_ANY, _("Launch at login"), wxDefaultPosition, wxDefaultSize, 0);
|
||||
autostartCheckbox->SetValue(settings.autoStart);
|
||||
autostartCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) {
|
||||
bool isChecked = event.IsChecked();
|
||||
|
@ -417,7 +578,7 @@ public:
|
|||
});
|
||||
|
||||
auto odesliCheckbox =
|
||||
new wxCheckBox(this, wxID_ANY, _("Odesli integration"), wxDefaultPosition, wxDefaultSize, 0);
|
||||
new wxCheckBox(panel, wxID_ANY, _("Odesli integration"), wxDefaultPosition, wxDefaultSize, 0);
|
||||
odesliCheckbox->SetValue(settings.odesli);
|
||||
odesliCheckbox->Bind(wxEVT_CHECKBOX, [](wxCommandEvent& event) {
|
||||
bool isChecked = event.IsChecked();
|
||||
|
@ -430,17 +591,80 @@ public:
|
|||
settingsContainer->Add(odesliCheckbox, 0, wxALL, 5);
|
||||
startupContainer->Add(settingsContainer);
|
||||
mainContainer->Add(startupContainer, 0, wxEXPAND, 5);
|
||||
|
||||
// settings end
|
||||
this->SetSizerAndFit(mainContainer);
|
||||
|
||||
this->SetSizerAndFit(frameSizer);
|
||||
wxSize currentSize = this->GetSize();
|
||||
this->SetSize(size.GetWidth(), currentSize.GetHeight());
|
||||
this->Layout();
|
||||
|
||||
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 {
|
||||
public:
|
||||
virtual bool OnInit() override {
|
||||
|
@ -453,8 +677,8 @@ public:
|
|||
this->SetAppearance(wxAppBase::Appearance::Dark);
|
||||
|
||||
wxInitAllImageHandlers();
|
||||
wxIcon icon = utils::loadIconFromMemory(icon_png, icon_png_size);
|
||||
PlayerLinkFrame* frame = new PlayerLinkFrame(nullptr, icon, wxID_ANY, _("PlayerLink"));
|
||||
wxIcon tray_icon = utils::loadIconFromMemory(menubar_icon_png, menubar_icon_png_size);
|
||||
PlayerLinkFrame* frame = new PlayerLinkFrame(nullptr, wxID_ANY, _("PlayerLink"));
|
||||
trayIcon = new PlayerLinkIcon(frame);
|
||||
frame->Bind(wxEVT_CLOSE_WINDOW, [=](wxCloseEvent& event) {
|
||||
if (event.CanVeto()) {
|
||||
|
@ -464,7 +688,7 @@ public:
|
|||
std::exit(0);
|
||||
});
|
||||
|
||||
trayIcon->SetIcon(icon, _("PlayerLink"));
|
||||
trayIcon->SetIcon(tray_icon, _("PlayerLink"));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
#include <wx/mstream.h>
|
||||
#include <wx/wx.h>
|
||||
#include <wx/clipbrd.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <nlohmann-json/single_include/nlohmann/json.hpp>
|
||||
|
@ -21,10 +20,14 @@
|
|||
namespace utils {
|
||||
struct App {
|
||||
bool enabled;
|
||||
int type;
|
||||
std::string appName;
|
||||
std::string clientId;
|
||||
std::string searchEndpoint;
|
||||
std::vector<std::string> processNames;
|
||||
bool operator==(const App& other) const {
|
||||
return appName == other.appName && clientId == other.clientId && type == other.type;
|
||||
}
|
||||
};
|
||||
|
||||
struct LastFMSettings {
|
||||
|
@ -56,18 +59,57 @@ namespace utils {
|
|||
}
|
||||
}
|
||||
|
||||
inline wxIcon loadIconFromMemory(const unsigned char* data, size_t size) {
|
||||
inline wxBitmap loadImageFromMemory(const unsigned char* data, size_t size, int width = 0, int height = 0) {
|
||||
wxMemoryInputStream stream(data, size);
|
||||
wxImage img(stream, wxBITMAP_TYPE_PNG);
|
||||
if (img.IsOk()) {
|
||||
if (width != 0 || height != 0)
|
||||
img.Rescale(width, height, wxIMAGE_QUALITY_HIGH);
|
||||
wxBitmap bmp(img);
|
||||
wxIcon icon;
|
||||
icon.CopyFromBitmap(bmp);
|
||||
return icon;
|
||||
return bmp;
|
||||
}
|
||||
return wxNullIcon;
|
||||
return wxNullBitmap;
|
||||
}
|
||||
|
||||
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) {
|
||||
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { return !std::isspace(ch); }));
|
||||
return s;
|
||||
|
@ -144,19 +186,23 @@ namespace utils {
|
|||
SongInfo ret{};
|
||||
std::string response =
|
||||
httpRequest("https://itunes.apple.com/search?media=music&entity=song&term=" + urlEncode(query));
|
||||
nlohmann::json j = nlohmann::json::parse(response);
|
||||
auto results = j["results"];
|
||||
if (results.size() > 0) {
|
||||
ret.artworkURL = results[0]["artworkUrl100"].get<std::string>();
|
||||
ret.trackId = results[0]["trackId"].get<int64_t>();
|
||||
try {
|
||||
nlohmann::json j = nlohmann::json::parse(response);
|
||||
auto results = j["results"];
|
||||
if (results.size() > 0) {
|
||||
ret.artworkURL = results[0]["artworkUrl100"].get<std::string>();
|
||||
ret.trackId = results[0]["trackId"].get<int64_t>();
|
||||
}
|
||||
return ret;
|
||||
} catch (...) {
|
||||
return ret;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
inline std::string getOdesliURL(SongInfo& song) {
|
||||
return std::string("https://song.link/i/" + std::to_string(song.trackId));
|
||||
}
|
||||
|
||||
|
||||
inline void saveSettings(const App* newApp) {
|
||||
nlohmann::json j;
|
||||
|
||||
|
@ -224,7 +270,7 @@ namespace utils {
|
|||
appJson["client_id"] = app.clientId;
|
||||
appJson["search_endpoint"] = app.searchEndpoint;
|
||||
appJson["enabled"] = app.enabled;
|
||||
|
||||
appJson["type"] = app.type;
|
||||
for (const auto& processName : app.processNames) appJson["process_names"].push_back(processName);
|
||||
|
||||
j["apps"].push_back(appJson);
|
||||
|
@ -269,8 +315,9 @@ namespace utils {
|
|||
a.clientId = app.value("client_id", "");
|
||||
a.searchEndpoint = app.value("search_endpoint", "");
|
||||
a.enabled = app.value("enabled", false);
|
||||
a.type = app.value("type", 2);
|
||||
|
||||
for (const auto& process : app["process_names"]) a.processNames.push_back(process.get<std::string>());
|
||||
for (const auto& process : app.value("process_names", nlohmann::json())) a.processNames.push_back(process.get<std::string>());
|
||||
|
||||
ret.apps.push_back(a);
|
||||
}
|
||||
|
@ -283,7 +330,7 @@ namespace utils {
|
|||
auto settings = getSettings();
|
||||
for (auto app : settings.apps) {
|
||||
for (auto procName : app.processNames) {
|
||||
if (procName == processName)
|
||||
if (caseInsensitiveMatch(procName, processName))
|
||||
return app;
|
||||
}
|
||||
}
|
||||
|
@ -291,6 +338,7 @@ namespace utils {
|
|||
a.clientId = DEFAULT_CLIENT_ID;
|
||||
a.appName = DEFAULT_APP_NAME;
|
||||
a.enabled = settings.anyOtherEnabled;
|
||||
a.type = 2; // Default to listening
|
||||
a.searchEndpoint = "";
|
||||
return a;
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@ set(wxBUILD_SHARED OFF)
|
|||
set(wxBUILD_MONOLITHIC ON)
|
||||
set(wxUSE_GUI ON)
|
||||
set(wxUSE_WEBVIEW OFF)
|
||||
set(wxUSE_UNICODE_UTF8 ON)
|
||||
set(wxUSE_UTF8_LOCALE_ONLY ON)
|
||||
add_subdirectory("wxWidgets")
|
||||
if(UNIX AND NOT APPLE)
|
||||
add_subdirectory("dbus")
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit e86d7a81de7a33323e2038182ab53a26c69f7880
|
||||
Subproject commit 6bc42071f109083178ae824898ca4b3cb40a8307
|
|
@ -1 +1 @@
|
|||
Subproject commit 138937b7775c117b57f55374a0c507a35a1102f6
|
||||
Subproject commit d7a696de4c301948f02dc5d7979f867f07f9d684
|
BIN
win/icon.ico
Before Width: | Height: | Size: 260 KiB After Width: | Height: | Size: 156 KiB |