Compare commits
No commits in common. "main" and "v1.2" have entirely different histories.
|
@ -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:
|
||||||
|
|
|
@ -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.
|
BIN
img/linux.png
Before Width: | Height: | Size: 263 KiB After Width: | Height: | Size: 42 KiB |
BIN
img/macos.png
Before Width: | Height: | Size: 692 KiB After Width: | Height: | Size: 135 KiB |
BIN
img/windows.png
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 285 KiB |
BIN
osx/icon.icns
BIN
rsrc/icon.png
Before Width: | Height: | Size: 817 KiB After Width: | Height: | Size: 285 KiB |
Before Width: | Height: | Size: 34 KiB |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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
|
||||||
}
|
}
|
|
@ -30,7 +30,6 @@ NSString *executeCommand(NSString *command, NSArray *arguments) {
|
||||||
|
|
||||||
NSPipe *pipe = [NSPipe pipe];
|
NSPipe *pipe = [NSPipe pipe];
|
||||||
task.standardOutput = pipe;
|
task.standardOutput = pipe;
|
||||||
task.standardError = [NSPipe pipe];
|
|
||||||
[task launch];
|
[task launch];
|
||||||
|
|
||||||
NSData *data = [[pipe fileHandleForReading] readDataToEndOfFile];
|
NSData *data = [[pipe fileHandleForReading] readDataToEndOfFile];
|
||||||
|
|
|
@ -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,55 +145,57 @@ 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;
|
|
||||||
|
|
||||||
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;
|
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() {
|
bool backend::init() {
|
||||||
|
|
|
@ -44,17 +44,13 @@ 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>();
|
|
||||||
|
|
||||||
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) {
|
||||||
|
|
496
src/main.cpp
|
@ -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;
|
wxMessageBox(_("Error authenticating at LastFM!"), _("PlayerLink"), wxOK | wxICON_ERROR);
|
||||||
lastfm = nullptr;
|
else if (checkMode)
|
||||||
if (checkMode)
|
|
||||||
wxMessageBox(_("Error authenticating at LastFM!"), _("PlayerLink"), wxOK | wxICON_ERROR);
|
|
||||||
} 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();
|
||||||
|
@ -151,7 +143,7 @@ void handleMediaTasks() {
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string odesliUrl = utils::getOdesliURL(songInfo);
|
std::string odesliUrl = utils::getOdesliURL(songInfo);
|
||||||
if (settings.odesli && songInfo.artworkURL != "") {
|
if(settings.odesli && songInfo.artworkURL != "") {
|
||||||
activity.button2name = "Show on Song.link";
|
activity.button2name = "Show on Song.link";
|
||||||
activity.button2link = odesliUrl.c_str();
|
activity.button2link = odesliUrl.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
|
|
||||||
|
|
||||||
this->SetSizerAndFit(frameSizer);
|
// settings end
|
||||||
|
this->SetSizerAndFit(mainContainer);
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,17 +144,13 @@ 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) {
|
ret.artworkURL = results[0]["artworkUrl100"].get<std::string>();
|
||||||
ret.artworkURL = results[0]["artworkUrl100"].get<std::string>();
|
ret.trackId = results[0]["trackId"].get<int64_t>();
|
||||||
ret.trackId = results[0]["trackId"].get<int64_t>();
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
} catch (...) {
|
|
||||||
return ret;
|
|
||||||
}
|
}
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 6bc42071f109083178ae824898ca4b3cb40a8307
|
Subproject commit ca0091c80054e640cbc772160ec594d2ca361db3
|
BIN
win/icon.ico
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 260 KiB |