Compare commits

..

1 Commits

Author SHA1 Message Date
Jonas Kvinge
c435464972 Add visualizations 2025-12-29 00:33:14 +01:00
47 changed files with 4684 additions and 35 deletions

View File

@@ -62,6 +62,7 @@ jobs:
libchromaprint-devel
fftw3-devel
libebur128-devel
projectM-devel
desktop-file-utils
update-desktop-files
appstream-glib
@@ -78,10 +79,11 @@ jobs:
qt6-base-common-devel
qt6-sql-sqlite
qt6-linguist-devel
qt6-openglwidgets-devel
gtest
gmock
sparsehash-devel
discord-rpc-devel
rapidjson-devel
- name: Install kdsingleapplication-qt6-devel
if: matrix.opensuse_version != 'leap:15.6'
run: zypper -n --gpg-auto-import-keys in kdsingleapplication-qt6-devel
@@ -200,6 +202,7 @@ jobs:
libchromaprint-devel
libebur128-devel
fftw-devel
libprojectM-devel
desktop-file-utils
libappstream-glib
hicolor-icon-theme
@@ -207,7 +210,7 @@ jobs:
gtest-devel
gmock-devel
sparsehash-devel
discord-rpc-devel
rapidjson-devel
- name: Checkout
uses: actions/checkout@v6
with:
@@ -290,6 +293,7 @@ jobs:
lib64Qt6DBus-devel
lib64Qt6Gui-devel
lib64Qt6Widgets-devel
lib64Qt6OpenGLWidgets-devel
lib64Qt6Test-devel
lib64kdsingleapplication-devel
lib64xkbcommon-devel
@@ -303,7 +307,7 @@ jobs:
appstream
appstream-util
hicolor-icon-theme
discord-rpc-devel
rapidjson
- name: Remove files
run: rm -rf /usr/lib64/qt6/lib/cmake/Qt6Sql/{Qt6QMYSQL*,Qt6QODBCD*,Qt6QPSQL*,Qt6QIBase*}
- name: Checkout
@@ -384,6 +388,7 @@ jobs:
lib64fftw-devel
lib64dbus-devel
lib64appstream-devel
lib64projectm-devel
lib64qt6core-devel
lib64qt6gui-devel
lib64qt6widgets-devel
@@ -393,13 +398,14 @@ jobs:
lib64qt6dbus-devel
lib64qt6help-devel
lib64qt6test-devel
lib64qt6openglwidgets-devel
lib64sparsehash-devel
lib64kdsingleapplication-devel
desktop-file-utils
appstream-util
hicolor-icon-theme
gtest
discord-rpc-devel
rapidjson
- name: Build and install KDSingleApplication
if: matrix.mageia_version == '9'
run: |
@@ -499,6 +505,7 @@ jobs:
qt6-tools-dev-tools
qt6-l10n-tools
rapidjson-dev
libprojectm-dev
- name: Install KDSingleApplication
if: matrix.debian_version != 'bookworm'
run: apt install -y libkdsingleapplication-qt6-dev
@@ -520,7 +527,7 @@ jobs:
- name: Create Build Environment
run: cmake -E make_directory build
- name: Configure CMake
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_WERROR=ON -DENABLE_DISCORD_RPC=OFF
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_WERROR=ON
- name: Delete build directory
run: rm -rf build
- name: make deb
@@ -595,6 +602,7 @@ jobs:
qt6-tools-dev-tools
qt6-l10n-tools
rapidjson-dev
libprojectm-dev
- name: Install KDSingleApplication
if: matrix.ubuntu_version != 'noble' && matrix.ubuntu_version != 'plucky'
run: apt install -y libkdsingleapplication-qt6-dev
@@ -616,7 +624,7 @@ jobs:
- name: Create Build Environment
run: cmake -E make_directory build
- name: Configure CMake
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_WERROR=ON -DENABLE_DISCORD_RPC=OFF
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_WERROR=ON
- name: Delete build directory
run: rm -rf build
- name: make deb
@@ -692,6 +700,8 @@ jobs:
gstreamer1.0-alsa
gstreamer1.0-pulseaudio
libkdsingleapplication-qt6-dev
rapidjson-dev
libprojectm-dev
- name: Install keyboxd
if: matrix.ubuntu_version == 'noble'
env:
@@ -711,7 +721,7 @@ jobs:
- name: Create Build Environment
run: cmake -E make_directory build
- name: Configure CMake
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_WERROR=ON -DENABLE_DISCORD_RPC=OFF
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_WERROR=ON
- name: Delete build directory
run: rm -rf build
- name: Import Ubuntu PPA GPG private key
@@ -755,7 +765,7 @@ jobs:
set -e
git config --global --add safe.directory ${GITHUB_WORKSPACE}
cmake -E make_directory build
cmake -S . -B build -DCMAKE_BUILD_TYPE="Debug" -DENABLE_DISCORD_RPC=OFF
cmake -S . -B build -DCMAKE_BUILD_TYPE="Debug"
cmake --build build --config Debug --parallel 4
@@ -775,7 +785,7 @@ jobs:
with:
usesh: true
mem: 4096
prepare: pkg_add git cmake pkgconf boost glib2 qt6-qtbase qt6-qttools sqlite gstreamer1 gstreamer1-plugins-base chromaprint libebur128 taglib libcdio libmtp gdk-pixbuf libgpod fftw3 icu4c kdsingleapplication pulseaudio sparsehash
prepare: pkg_add git cmake pkgconf boost glib2 qt6-qtbase qt6-qttools sqlite gstreamer1 gstreamer1-plugins-base chromaprint libebur128 taglib libcdio libmtp gdk-pixbuf libgpod fftw3 icu4c kdsingleapplication pulseaudio sparsehash rapidjson
run: |
set -e
export LDFLAGS="-L/usr/local/lib"

41
3rdparty/discord-rpc/CMakeLists.txt vendored Normal file
View File

@@ -0,0 +1,41 @@
set(DISCORD_RPC_SOURCES
discord_rpc.h
discord_register.h
discord_rpc.cpp
discord_rpc_connection.h
discord_rpc_connection.cpp
discord_serialization.h
discord_serialization.cpp
discord_connection.h
discord_backoff.h
discord_msg_queue.h
)
if(UNIX)
list(APPEND DISCORD_RPC_SOURCES discord_connection_unix.cpp)
if(APPLE)
list(APPEND DISCORD_RPC_SOURCES discord_register_osx.m)
add_definitions(-DDISCORD_OSX)
else()
list(APPEND DISCORD_RPC_SOURCES discord_register_linux.cpp)
add_definitions(-DDISCORD_LINUX)
endif()
endif()
if(WIN32)
list(APPEND DISCORD_RPC_SOURCES discord_connection_win.cpp discord_register_win.cpp)
add_definitions(-DDISCORD_WINDOWS)
endif()
add_library(discord-rpc STATIC ${DISCORD_RPC_SOURCES})
if(APPLE)
target_link_libraries(discord-rpc PRIVATE "-framework AppKit")
endif()
if(WIN32)
target_link_libraries(discord-rpc PRIVATE psapi advapi32)
endif()
target_include_directories(discord-rpc SYSTEM PRIVATE ${RapidJSON_INCLUDE_DIRS})
target_include_directories(discord-rpc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})

19
3rdparty/discord-rpc/LICENSE vendored Normal file
View File

@@ -0,0 +1,19 @@
Copyright 2017 Discord, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

162
3rdparty/discord-rpc/README.md vendored Normal file
View File

@@ -0,0 +1,162 @@
# Discord RPC
## Fork Notice
This library was slightly modified for Strawberry Music Player with some extra features from the new API and shared library support/more unnecessary components removed. The original repository is [here](https://github.com/discord/discord-rpc)
## Deprecation Notice
This library has been deprecated in favor of Discord's GameSDK. [Learn more here](https://discordapp.com/developers/docs/game-sdk/sdk-starter-guide)
---
This is a library for interfacing your game with a locally running Discord desktop client. It's known to work on Windows, macOS, and Linux. You can use the lib directly if you like, or use it as a guide to writing your own if it doesn't suit your game as is. PRs/feedback welcome if you have an improvement everyone might want, or can describe how this doesn't meet your needs.
Included here are some quick demos that implement the very minimal subset to show current status, and
have callbacks for where a more complete game would do more things (joining, spectating, etc).
## Documentation
The most up to date documentation for Rich Presence can always be found on our [developer site](https://discordapp.com/developers/docs/rich-presence/how-to)! If you're interested in rolling your own native implementation of Rich Presence via IPC sockets instead of using our SDK—hey, you've got free time, right?—check out the ["Hard Mode" documentation](https://github.com/discordapp/discord-rpc/blob/master/documentation/hard-mode.md).
## Basic Usage
Zeroith, you should be set up to build things because you are a game developer, right?
First, head on over to the [Discord developers site](https://discordapp.com/developers/applications/me) and make yourself an app. Keep track of `Client ID` -- you'll need it here to pass to the init function.
### Unreal Engine 4 Setup
To use the Rich Presense plugin with Unreal Engine Projects:
1. Download the latest [release](https://github.com/discordapp/discord-rpc/releases) for each operating system you are targeting and the zipped source code
2. In the source code zip, copy the UE plugin—`examples/unrealstatus/Plugins/discordrpc`—to your project's plugin directory
3. At `[YOUR_UE_PROJECT]/Plugins/discordrpc/source/ThirdParty/DiscordRpcLibrary/`, create an `Include` folder and copy `discord_rpc.h` and `discord_register.h` to it from the zip
4. Follow the steps below for each OS
5. Build your UE4 project
6. Launch the editor, and enable the Discord plugin.
#### Windows
- At `[YOUR_UE_PROJECT]/Plugins/discordrpc/source/ThirdParty/DiscordRpcLibrary/`, create a `Win64` folder
- Copy `lib/discord-rpc.lib` and `bin/discord-rpc.dll` from `[RELEASE_ZIP]/win64-dynamic` to the `Win64` folder
#### Mac
- At `[YOUR_UE_PROJECT]/Plugins/discordrpc/source/ThirdParty/DiscordRpcLibrary/`, create a `Mac` folder
- Copy `libdiscord-rpc.dylib` from `[RELEASE_ZIP]/osx-dynamic/lib` to the `Mac` folder
#### Linux
- At `[YOUR_UE_PROJECT]/Plugins/discordrpc/source/ThirdParty/DiscordRpcLibrary/`, create a `Linux` folder
- Inside, create another folder `x86_64-unknown-linux-gnu`
- Copy `libdiscord-rpc.so` from `[RELEASE_ZIP]/linux-dynamic/lib` to `Linux/x86_64-unknown-linux-gnu`
### Unity Setup
If you're a Unity developer looking to integrate Rich Presence into your game, follow this simple guide to get started towards success:
1. Download the DLLs for any platform that you need from [our releases](https://github.com/discordapp/discord-rpc/releases)
2. In your Unity project, create a `Plugins` folder inside your `Assets` folder if you don't already have one
3. Copy the file `DiscordRpc.cs` from [here](https://github.com/discordapp/discord-rpc/blob/master/examples/button-clicker/Assets/DiscordRpc.cs) into your `Assets` folder. This is basically your header file for the SDK
We've got our `Plugins` folder ready, so let's get platform-specific!
#### Windows
4. Create `x86` and `x86_64` folders inside `Assets/Plugins/`
5. Copy `discord-rpc-win/win64-dynamic/bin/discord-rpc.dll` to `Assets/Plugins/x86_64/`
6. Copy `discord-rpc-win/win32-dynamic/bin/discord-rpc.dll` to `Assets/Plugins/x86/`
7. Click on both DLLs and make sure they are targetting the correct architectures in the Unity editor properties pane
8. Done!
#### MacOS
4. Copy `discord-rpc-osx/osx-dynamic/lib/libdiscord-rpc.dylib` to `Assets/Plugins/`
5. Rename `libdiscord-rpc.dylib` to `discord-rpc.bundle`
6. Done!
#### Linux
4. Copy `discord-rpc-linux/linux-dynamic-lib/libdiscord-rpc.so` to `Assets/Plugins/`
5. Done!
You're ready to roll! For code examples on how to interact with the SDK using the `DiscordRpc.cs` header file, check out [our example](https://github.com/discordapp/discord-rpc/blob/master/examples/button-clicker/Assets/DiscordController.cs)
### From package
Download a release package for your platform(s) -- they have subdirs with various prebuilt options, select the one you need add `/include` to your compile includes, `/lib` to your linker paths, and link with `discord-rpc`. For the dynamically linked builds, you'll need to ship the associated file along with your game.
### From repo
First-eth, you'll want `CMake`. There's a few different ways to install it on your system, and you should refer to [their website](https://cmake.org/install/). Many package managers provide ways of installing CMake as well.
To make sure it's installed correctly, type `cmake --version` into your flavor of terminal/cmd. If you get a response with a version number, you're good to go!
There's a [CMake](https://cmake.org/download/) file that should be able to generate the lib for you; Sometimes I use it like this:
```sh
cd <path to discord-rpc>
mkdir build
cd build
cmake .. -DCMAKE_INSTALL_PREFIX=<path to install discord-rpc to>
cmake --build . --config Release --target install
```
There is a wrapper build script `build.py` that runs `cmake` with a few different options.
Usually, I run `build.py` to get things started, then use the generated project files as I work on things. It does depend on `click` library, so do a quick `pip install click` to make sure you have it if you want to run `build.py`.
There are some CMake options you might care about:
| flag | default | does |
| ---------------------------------------------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ENABLE_IO_THREAD` | `ON` | When enabled, we start up a thread to do io processing, if disabled you should call `Discord_UpdateConnection` yourself. |
| `USE_STATIC_CRT` | `OFF` | (Windows) Enable to statically link the CRT, avoiding requiring users install the redistributable package. (The prebuilt binaries enable this option) |
| [`BUILD_SHARED_LIBS`](https://cmake.org/cmake/help/v3.7/variable/BUILD_SHARED_LIBS.html) | `OFF` | Build library as a DLL |
| `WARNINGS_AS_ERRORS` | `OFF` | When enabled, compiles with `-Werror` (on \*nix platforms). |
## Continuous Builds
Why do we have three of these? Three times the fun!
| CI | badge |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| TravisCI | [![Build status](https://travis-ci.org/discordapp/discord-rpc.svg?branch=master)](https://travis-ci.org/discordapp/discord-rpc) |
| AppVeyor | [![Build status](https://ci.appveyor.com/api/projects/status/qvkoc0w1c4f4b8tj?svg=true)](https://ci.appveyor.com/project/crmarsh/discord-rpc) |
| Buildkite (internal) | [![Build status](https://badge.buildkite.com/e103d79d247f6776605a15246352a04b8fd83d69211b836111.svg)](https://buildkite.com/discord/discord-rpc) |
## Sample: send-presence
This is a text adventure "game" that inits/deinits the connection to Discord, and sends a presence update on each command.
## Sample: button-clicker
This is a sample [Unity](https://unity3d.com/) project that wraps a DLL version of the library, and sends presence updates when you click on a button. Run `python build.py unity` in the root directory to build the correct library files and place them in their respective folders.
## Sample: unrealstatus
This is a sample [Unreal](https://www.unrealengine.com) project that wraps the DLL version of the library with an Unreal plugin, exposes a blueprint class for interacting with it, and uses that to make a very simple UI. Run `python build.py unreal` in the root directory to build the correct library files and place them in their respective folders.
## Wrappers and Implementations
Below is a table of unofficial, community-developed wrappers for and implementations of Rich Presence in various languages. If you would like to have yours added, please make a pull request adding your repository to the table. The repository should include:
- The code
- A brief ReadMe of how to use it
- A working example
###### Rich Presence Wrappers and Implementations
| Name | Language |
| ------------------------------------------------------------------------- | --------------------------------- |
| [Discord RPC C#](https://github.com/Lachee/discord-rpc-csharp) | C# |
| [Discord RPC D](https://github.com/voidblaster/discord-rpc-d) | [D](https://dlang.org/) |
| [discord-rpc.jar](https://github.com/Vatuu/discord-rpc 'Discord-RPC.jar') | Java |
| [java-discord-rpc](https://github.com/MinnDevelopment/java-discord-rpc) | Java |
| [Discord-IPC](https://github.com/jagrosh/DiscordIPC) | Java |
| [Discord Rich Presence](https://npmjs.org/discord-rich-presence) | JavaScript |
| [drpc4k](https://github.com/Bluexin/drpc4k) | [Kotlin](https://kotlinlang.org/) |
| [lua-discordRPC](https://github.com/pfirsich/lua-discordRPC) | LuaJIT (FFI) |
| [pypresence](https://github.com/qwertyquerty/pypresence) | [Python](https://python.org/) |
| [SwordRPC](https://github.com/Azoy/SwordRPC) | [Swift](https://swift.org) |

63
3rdparty/discord-rpc/discord_backoff.h vendored Normal file
View File

@@ -0,0 +1,63 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#ifndef DISCORD_BACKOFF_H
#define DISCORD_BACKOFF_H
#include <algorithm>
#include <random>
#include <cstdint>
#include <ctime>
namespace discord_rpc {
struct Backoff {
int64_t minAmount;
int64_t maxAmount;
int64_t current;
int fails;
std::mt19937_64 randGenerator;
std::uniform_real_distribution<> randDistribution;
double rand01() { return randDistribution(randGenerator); }
Backoff(int64_t min, int64_t max)
: minAmount(min), maxAmount(max), current(min), fails(0), randGenerator(static_cast<uint64_t>(time(0))) {
}
void reset() {
fails = 0;
current = minAmount;
}
int64_t nextDelay() {
++fails;
int64_t delay = static_cast<int64_t>(static_cast<double>(current) * 2.0 * rand01());
current = std::min(current + delay, maxAmount);
return current;
}
};
} // namespace discord_rpc
#endif // DISCORD_BACKOFF_H

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#ifndef DISCORD_CONNECTION_H
#define DISCORD_CONNECTION_H
// This is to wrap the platform specific kinds of connect/read/write.
#include <cstdlib>
namespace discord_rpc {
// not really connectiony, but need per-platform
int GetProcessId();
struct BaseConnection {
static BaseConnection *Create();
static void Destroy(BaseConnection*&);
bool isOpen = false;
bool Open();
bool Close();
bool Write(const void *data, size_t length);
bool Read(void *data, size_t length);
};
} // namespace discord_rpc
#endif // DISCORD_CONNECTION_H

View File

@@ -0,0 +1,160 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#include "discord_connection.h"
#include <cerrno>
#include <fcntl.h>
#include <cstdio>
#include <cstring>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>
namespace discord_rpc {
int GetProcessId() {
return ::getpid();
}
struct BaseConnectionUnix : public BaseConnection {
int sock{ -1 };
};
static BaseConnectionUnix Connection;
static sockaddr_un PipeAddr{};
#ifdef MSG_NOSIGNAL
static int MsgFlags = MSG_NOSIGNAL;
#else
static int MsgFlags = 0;
#endif
static const char *GetTempPath() {
const char *temp = getenv("XDG_RUNTIME_DIR");
temp = temp ? temp : getenv("TMPDIR");
temp = temp ? temp : getenv("TMP");
temp = temp ? temp : getenv("TEMP");
temp = temp ? temp : "/tmp";
return temp;
}
BaseConnection *BaseConnection::Create() {
PipeAddr.sun_family = AF_UNIX;
return &Connection;
}
void BaseConnection::Destroy(BaseConnection *&c) {
auto self = reinterpret_cast<BaseConnectionUnix*>(c);
self->Close();
c = nullptr;
}
bool BaseConnection::Open() {
const char *tempPath = GetTempPath();
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
self->sock = socket(AF_UNIX, SOCK_STREAM, 0);
if (self->sock == -1) {
return false;
}
fcntl(self->sock, F_SETFL, O_NONBLOCK);
#ifdef SO_NOSIGPIPE
int optval = 1;
setsockopt(self->sock, SOL_SOCKET, SO_NOSIGPIPE, &optval, sizeof(optval));
#endif
for (int pipeNum = 0; pipeNum < 10; ++pipeNum) {
snprintf(PipeAddr.sun_path, sizeof(PipeAddr.sun_path), "%s/discord-ipc-%d", tempPath, pipeNum);
int err = connect(self->sock, reinterpret_cast<const sockaddr*>(&PipeAddr), sizeof(PipeAddr));
if (err == 0) {
self->isOpen = true;
return true;
}
}
self->Close();
return false;
}
bool BaseConnection::Close() {
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
if (self->sock == -1) {
return false;
}
close(self->sock);
self->sock = -1;
self->isOpen = false;
return true;
}
bool BaseConnection::Write(const void *data, size_t length) {
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
if (self->sock == -1) {
return false;
}
ssize_t sentBytes = send(self->sock, data, length, MsgFlags);
if (sentBytes < 0) {
Close();
}
return sentBytes == static_cast<ssize_t>(length);
}
bool BaseConnection::Read(void *data, size_t length) {
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
if (self->sock == -1) {
return false;
}
long res = recv(self->sock, data, length, MsgFlags);
if (res < 0) {
if (errno == EAGAIN) {
return false;
}
Close();
}
else if (res == 0) {
Close();
}
return static_cast<size_t>(res) == length;
}
} // namespace discord_rpc

View File

@@ -0,0 +1,160 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#include "discord_connection.h"
#define WIN32_LEAN_AND_MEAN
#define NOMCX
#define NOSERVICE
#define NOIME
#include <cassert>
#include <windows.h>
namespace discord_rpc {
int GetProcessId() {
return static_cast<int>(::GetCurrentProcessId());
}
struct BaseConnectionWin : public BaseConnection {
HANDLE pipe{ INVALID_HANDLE_VALUE };
};
static BaseConnectionWin Connection;
BaseConnection *BaseConnection::Create() {
return &Connection;
}
void BaseConnection::Destroy(BaseConnection *&c) {
auto self = reinterpret_cast<BaseConnectionWin*>(c);
self->Close();
c = nullptr;
}
bool BaseConnection::Open() {
wchar_t pipeName[]{ L"\\\\?\\pipe\\discord-ipc-0" };
const size_t pipeDigit = sizeof(pipeName) / sizeof(wchar_t) - 2;
pipeName[pipeDigit] = L'0';
auto self = reinterpret_cast<BaseConnectionWin*>(this);
for (;;) {
self->pipe = ::CreateFileW(pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
if (self->pipe != INVALID_HANDLE_VALUE) {
self->isOpen = true;
return true;
}
auto lastError = GetLastError();
if (lastError == ERROR_FILE_NOT_FOUND) {
if (pipeName[pipeDigit] < L'9') {
pipeName[pipeDigit]++;
continue;
}
}
else if (lastError == ERROR_PIPE_BUSY) {
if (!WaitNamedPipeW(pipeName, 10000)) {
return false;
}
continue;
}
return false;
}
}
bool BaseConnection::Close() {
auto self = reinterpret_cast<BaseConnectionWin*>(this);
::CloseHandle(self->pipe);
self->pipe = INVALID_HANDLE_VALUE;
self->isOpen = false;
return true;
}
bool BaseConnection::Write(const void *data, size_t length) {
if (length == 0) {
return true;
}
auto self = reinterpret_cast<BaseConnectionWin*>(this);
assert(self);
if (!self) {
return false;
}
if (self->pipe == INVALID_HANDLE_VALUE) {
return false;
}
assert(data);
if (!data) {
return false;
}
const DWORD bytesLength = static_cast<DWORD>(length);
DWORD bytesWritten = 0;
return ::WriteFile(self->pipe, data, bytesLength, &bytesWritten, nullptr) == TRUE && bytesWritten == bytesLength;
}
bool BaseConnection::Read(void *data, size_t length) {
assert(data);
if (!data) {
return false;
}
auto self = reinterpret_cast<BaseConnectionWin*>(this);
assert(self);
if (!self) {
return false;
}
if (self->pipe == INVALID_HANDLE_VALUE) {
return false;
}
DWORD bytesAvailable = 0;
if (::PeekNamedPipe(self->pipe, nullptr, 0, nullptr, &bytesAvailable, nullptr)) {
if (bytesAvailable >= length) {
DWORD bytesToRead = static_cast<DWORD>(length);
DWORD bytesRead = 0;
if (::ReadFile(self->pipe, data, bytesToRead, &bytesRead, nullptr) == TRUE) {
assert(bytesToRead == bytesRead);
return true;
}
else {
Close();
}
}
}
else {
Close();
}
return false;
}
} // namespace discord_rpc

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#ifndef DISCORD_MSG_QUEUE_H
#define DISCORD_MSG_QUEUE_H
#include <atomic>
// A simple queue. No locks, but only works with a single thread as producer and a single thread as
// a consumer. Mutex up as needed.
namespace discord_rpc {
template<typename ElementType, std::size_t QueueSize>
class MsgQueue {
ElementType queue_[QueueSize];
std::atomic_uint nextAdd_{ 0 };
std::atomic_uint nextSend_{ 0 };
std::atomic_uint pendingSends_{ 0 };
public:
MsgQueue() {}
ElementType *GetNextAddMessage() {
// if we are falling behind, bail
if (pendingSends_.load() >= QueueSize) {
return nullptr;
}
auto index = (nextAdd_++) % QueueSize;
return &queue_[index];
}
void CommitAdd() { ++pendingSends_; }
bool HavePendingSends() const { return pendingSends_.load() != 0; }
ElementType *GetNextSendMessage() {
auto index = (nextSend_++) % QueueSize;
return &queue_[index];
}
void CommitSend() { --pendingSends_; }
};
} // namespace discord_rpc
#endif // DISCORD_MSG_QUEUE_H

37
3rdparty/discord-rpc/discord_register.h vendored Normal file
View File

@@ -0,0 +1,37 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#ifndef DISCORD_REGISTER_H
#define DISCORD_REGISTER_H
#ifdef __cplusplus
extern "C" {
#endif
void Discord_Register(const char *applicationId, const char *command);
#ifdef __cplusplus
}
#endif
#endif // DISCORD_REGISTER_H

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#include "discord_rpc.h"
#include "discord_register.h"
#include <cstdio>
#include <errno.h>
#include <cstdlib>
#include <cstring>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
namespace {
static bool Mkdir(const char *path) {
int result = mkdir(path, 0755);
if (result == 0) {
return true;
}
if (errno == EEXIST) {
return true;
}
return false;
}
} // namespace
// We want to register games so we can run them from Discord client as discord-<appid>://
extern "C" void Discord_Register(const char *applicationId, const char *command) {
// Add a desktop file and update some mime handlers so that xdg-open does the right thing.
const char *home = getenv("HOME");
if (!home) {
return;
}
char exePath[1024]{};
if (!command || !command[0]) {
const ssize_t size = readlink("/proc/self/exe", exePath, sizeof(exePath));
if (size <= 0 || size >= static_cast<ssize_t>(sizeof(exePath))) {
return;
}
exePath[size] = '\0';
command = exePath;
}
constexpr char desktopFileFormat[] = "[Desktop Entry]\n"
"Name=Game %s\n"
"Exec=%s %%u\n" // note: it really wants that %u in there
"Type=Application\n"
"NoDisplay=true\n"
"Categories=Discord;Games;\n"
"MimeType=x-scheme-handler/discord-%s;\n";
char desktopFile[2048]{};
int fileLen = snprintf(desktopFile, sizeof(desktopFile), desktopFileFormat, applicationId, command, applicationId);
if (fileLen <= 0) {
return;
}
char desktopFilename[256]{};
(void)snprintf(desktopFilename, sizeof(desktopFilename), "/discord-%s.desktop", applicationId);
char desktopFilePath[1024]{};
(void)snprintf(desktopFilePath, sizeof(desktopFilePath), "%s/.local", home);
if (!Mkdir(desktopFilePath)) {
return;
}
strcat(desktopFilePath, "/share");
if (!Mkdir(desktopFilePath)) {
return;
}
strcat(desktopFilePath, "/applications");
if (!Mkdir(desktopFilePath)) {
return;
}
strcat(desktopFilePath, desktopFilename);
FILE *fp = fopen(desktopFilePath, "w");
if (fp) {
fwrite(desktopFile, 1, fileLen, fp);
fclose(fp);
}
else {
return;
}
char xdgMimeCommand[1024]{};
snprintf(xdgMimeCommand,
sizeof(xdgMimeCommand),
"xdg-mime default discord-%s.desktop x-scheme-handler/discord-%s",
applicationId,
applicationId);
if (system(xdgMimeCommand) < 0) {
fprintf(stderr, "Failed to register mime handler\n");
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#include <stdio.h>
#include <sys/stat.h>
#import <AppKit/AppKit.h>
#include "discord_register.h"
static void RegisterCommand(const char *applicationId, const char *command) {
// There does not appear to be a way to register arbitrary commands on OSX, so instead we'll save the command
// to a file in the Discord config path, and when it is needed, Discord can try to load the file there, open
// the command therein (will pass to js's window.open, so requires a url-like thing)
// Note: will not work for sandboxed apps
NSString *home = NSHomeDirectory();
if (!home) {
return;
}
NSString *path = [[[[[[home stringByAppendingPathComponent:@"Library"]
stringByAppendingPathComponent:@"Application Support"]
stringByAppendingPathComponent:@"discord"]
stringByAppendingPathComponent:@"games"]
stringByAppendingPathComponent:[NSString stringWithUTF8String:applicationId]]
stringByAppendingPathExtension:@"json"];
[[NSFileManager defaultManager] createDirectoryAtPath:[path stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil];
NSString *jsonBuffer = [NSString stringWithFormat:@"{\"command\": \"%s\"}", command];
[jsonBuffer writeToFile:path atomically:NO encoding:NSUTF8StringEncoding error:nil];
}
static void RegisterURL(const char *applicationId) {
char url[256];
snprintf(url, sizeof(url), "discord-%s", applicationId);
CFStringRef cfURL = CFStringCreateWithCString(NULL, url, kCFStringEncodingUTF8);
NSString* myBundleId = [[NSBundle mainBundle] bundleIdentifier];
if (!myBundleId) {
fprintf(stderr, "No bundle id found\n");
return;
}
NSURL* myURL = [[NSBundle mainBundle] bundleURL];
if (!myURL) {
fprintf(stderr, "No bundle url found\n");
return;
}
OSStatus status = LSSetDefaultHandlerForURLScheme(cfURL, (__bridge CFStringRef)myBundleId);
if (status != noErr) {
fprintf(stderr, "Error in LSSetDefaultHandlerForURLScheme: %d\n", (int)status);
return;
}
status = LSRegisterURL((__bridge CFURLRef)myURL, true);
if (status != noErr) {
fprintf(stderr, "Error in LSRegisterURL: %d\n", (int)status);
}
}
void Discord_Register(const char *applicationId, const char *command) {
if (command) {
RegisterCommand(applicationId, command);
}
else {
// raii lite
@autoreleasepool {
RegisterURL(applicationId);
}
}
}

View File

@@ -0,0 +1,165 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#include "discord_rpc.h"
#include "discord_register.h"
#define WIN32_LEAN_AND_MEAN
#define NOMCX
#define NOSERVICE
#define NOIME
#include <windows.h>
#include <psapi.h>
#include <cstdio>
/**
* Updated fixes for MinGW and WinXP
* This block is written the way it does not involve changing the rest of the code
* Checked to be compiling
* 1) strsafe.h belongs to Windows SDK and cannot be added to MinGW
* #include guarded, functions redirected to <string.h> substitutes
* 2) RegSetKeyValueW and LSTATUS are not declared in <winreg.h>
* The entire function is rewritten
*/
#ifdef __MINGW32__
# include <wchar.h>
/// strsafe.h fixes
static HRESULT StringCbPrintfW(LPWSTR pszDest, size_t cbDest, LPCWSTR pszFormat, ...) {
HRESULT ret;
va_list va;
va_start(va, pszFormat);
cbDest /= 2; // Size is divided by 2 to convert from bytes to wide characters - causes segfault
// othervise
ret = vsnwprintf(pszDest, cbDest, pszFormat, va);
pszDest[cbDest - 1] = 0; // Terminate the string in case a buffer overflow; -1 will be returned
va_end(va);
return ret;
}
#else
# include <cwchar>
# include <strsafe.h>
#endif // __MINGW32__
/// winreg.h fixes
#ifndef LSTATUS
# define LSTATUS LONG
#endif
#ifdef RegSetKeyValueW
# undefine RegSetKeyValueW
#endif
#define RegSetKeyValueW regset
static LSTATUS regset(HKEY hkey, LPCWSTR subkey, LPCWSTR name, DWORD type, const void *data, DWORD len) {
HKEY htkey = hkey, hsubkey = nullptr;
LSTATUS ret;
if (subkey && subkey[0]) {
if ((ret = RegCreateKeyExW(hkey, subkey, 0, 0, 0, KEY_ALL_ACCESS, 0, &hsubkey, 0)) !=
ERROR_SUCCESS)
return ret;
htkey = hsubkey;
}
ret = RegSetValueExW(htkey, name, 0, type, static_cast<const BYTE*>(data), len);
if (hsubkey && hsubkey != hkey)
RegCloseKey(hsubkey);
return ret;
}
static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *command) {
// https://msdn.microsoft.com/en-us/library/aa767914(v=vs.85).aspx
// we want to register games so we can run them as discord-<appid>://
// Update the HKEY_CURRENT_USER, because it doesn't seem to require special permissions.
wchar_t exeFilePath[MAX_PATH]{};
DWORD exeLen = GetModuleFileNameW(nullptr, exeFilePath, MAX_PATH);
wchar_t openCommand[1024]{};
if (command && command[0]) {
StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", command);
}
else {
// StringCbCopyW(openCommand, sizeof(openCommand), exeFilePath);
StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", exeFilePath);
}
wchar_t protocolName[64]{};
StringCbPrintfW(protocolName, sizeof(protocolName), L"discord-%s", applicationId);
wchar_t protocolDescription[128]{};
StringCbPrintfW(protocolDescription, sizeof(protocolDescription), L"URL:Run game %s protocol", applicationId);
wchar_t urlProtocol = 0;
wchar_t keyName[256]{};
StringCbPrintfW(keyName, sizeof(keyName), L"Software\\Classes\\%s", protocolName);
HKEY key;
auto status = RegCreateKeyExW(HKEY_CURRENT_USER, keyName, 0, nullptr, 0, KEY_WRITE, nullptr, &key, nullptr);
if (status != ERROR_SUCCESS) {
fprintf(stderr, "Error creating key\n");
return;
}
DWORD len;
LSTATUS result;
len = static_cast<DWORD>(lstrlenW(protocolDescription) + 1);
result = RegSetKeyValueW(key, nullptr, nullptr, REG_SZ, protocolDescription, len * sizeof(wchar_t));
if (FAILED(result)) {
fprintf(stderr, "Error writing description\n");
}
len = static_cast<DWORD>(lstrlenW(protocolDescription) + 1);
result = RegSetKeyValueW(key, nullptr, L"URL Protocol", REG_SZ, &urlProtocol, sizeof(wchar_t));
if (FAILED(result)) {
fprintf(stderr, "Error writing description\n");
}
result = RegSetKeyValueW(key, L"DefaultIcon", nullptr, REG_SZ, exeFilePath, (exeLen + 1) * sizeof(wchar_t));
if (FAILED(result)) {
fprintf(stderr, "Error writing icon\n");
}
len = static_cast<DWORD>(lstrlenW(openCommand) + 1);
result = RegSetKeyValueW(key, L"shell\\open\\command", nullptr, REG_SZ, openCommand, len * sizeof(wchar_t));
if (FAILED(result)) {
fprintf(stderr, "Error writing command\n");
}
RegCloseKey(key);
}
extern "C" void Discord_Register(const char *applicationId, const char *command) {
wchar_t appId[32]{};
MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32);
wchar_t openCommand[1024]{};
const wchar_t *wcommand = nullptr;
if (command && command[0]) {
const auto commandBufferLen = sizeof(openCommand) / sizeof(*openCommand);
MultiByteToWideChar(CP_UTF8, 0, command, -1, openCommand, commandBufferLen);
wcommand = openCommand;
}
Discord_RegisterW(appId, wcommand);
}

510
3rdparty/discord-rpc/discord_rpc.cpp vendored Normal file
View File

@@ -0,0 +1,510 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#include <atomic>
#include <chrono>
#include <mutex>
#include <condition_variable>
#include <thread>
#include "discord_rpc.h"
#include "discord_backoff.h"
#include "discord_register.h"
#include "discord_msg_queue.h"
#include "discord_rpc_connection.h"
#include "discord_serialization.h"
using namespace discord_rpc;
static void Discord_UpdateConnection();
namespace {
constexpr size_t MaxMessageSize{ 16 * 1024 };
constexpr size_t MessageQueueSize{ 8 };
constexpr size_t JoinQueueSize{ 8 };
struct QueuedMessage {
size_t length;
char buffer[MaxMessageSize];
void Copy(const QueuedMessage &other) {
length = other.length;
if (length) {
memcpy(buffer, other.buffer, length);
}
}
};
struct User {
// snowflake (64bit int), turned into a ascii decimal string, at most 20 chars +1 null
// terminator = 21
char userId[32];
// 32 unicode glyphs is max name size => 4 bytes per glyph in the worst case, +1 for null
// terminator = 129
char username[344];
// 4 decimal digits + 1 null terminator = 5
char discriminator[8];
// optional 'a_' + md5 hex digest (32 bytes) + null terminator = 35
char avatar[128];
// Rounded way up because I'm paranoid about games breaking from future changes in these sizes
};
static RpcConnection *Connection{ nullptr };
static DiscordEventHandlers QueuedHandlers{};
static DiscordEventHandlers Handlers{};
static std::atomic_bool WasJustConnected{ false };
static std::atomic_bool WasJustDisconnected{ false };
static std::atomic_bool GotErrorMessage{ false };
static std::atomic_bool WasJoinGame{ false };
static std::atomic_bool WasSpectateGame{ false };
static std::atomic_bool UpdatePresence{ false };
static char JoinGameSecret[256];
static char SpectateGameSecret[256];
static int LastErrorCode{ 0 };
static char LastErrorMessage[256];
static int LastDisconnectErrorCode{ 0 };
static char LastDisconnectErrorMessage[256];
static std::mutex PresenceMutex;
static std::mutex HandlerMutex;
static QueuedMessage QueuedPresence{};
static MsgQueue<QueuedMessage, MessageQueueSize> SendQueue;
static MsgQueue<User, JoinQueueSize> JoinAskQueue;
static User connectedUser;
// We want to auto connect, and retry on failure, but not as fast as possible. This does expoential backoff from 0.5 seconds to 1 minute
static Backoff ReconnectTimeMs(500, 60 * 1000);
static auto NextConnect = std::chrono::system_clock::now();
static int Pid{ 0 };
static int Nonce{ 1 };
class IoThreadHolder {
private:
std::atomic_bool keepRunning{ true };
std::mutex waitForIOMutex;
std::condition_variable waitForIOActivity;
std::thread ioThread;
public:
void Start() {
keepRunning.store(true);
ioThread = std::thread([&]() {
const std::chrono::duration<int64_t, std::milli> maxWait { 500LL };
Discord_UpdateConnection();
while (keepRunning.load()) {
std::unique_lock<std::mutex> lock(waitForIOMutex);
waitForIOActivity.wait_for(lock, maxWait);
Discord_UpdateConnection();
}
});
}
void Notify() { waitForIOActivity.notify_all(); }
void Stop() {
keepRunning.exchange(false);
Notify();
if (ioThread.joinable()) {
ioThread.join();
}
}
~IoThreadHolder() { Stop(); }
};
static IoThreadHolder *IoThread{ nullptr };
static void UpdateReconnectTime() {
NextConnect = std::chrono::system_clock::now() + std::chrono::duration<int64_t, std::milli> { ReconnectTimeMs.nextDelay() };
}
static void SignalIOActivity() {
if (IoThread != nullptr) {
IoThread->Notify();
}
}
static bool RegisterForEvent(const char *evtName) {
auto qmessage = SendQueue.GetNextAddMessage();
if (qmessage) {
qmessage->length = JsonWriteSubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
SendQueue.CommitAdd();
SignalIOActivity();
return true;
}
return false;
}
static bool DeregisterForEvent(const char *evtName) {
auto qmessage = SendQueue.GetNextAddMessage();
if (qmessage) {
qmessage->length = JsonWriteUnsubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
SendQueue.CommitAdd();
SignalIOActivity();
return true;
}
return false;
}
} // namespace
static void Discord_UpdateConnection() {
if (!Connection) {
return;
}
if (!Connection->IsOpen()) {
if (std::chrono::system_clock::now() >= NextConnect) {
UpdateReconnectTime();
Connection->Open();
}
}
else {
// reads
for (;;) {
JsonDocument message;
if (!Connection->Read(message)) {
break;
}
const char *evtName = GetStrMember(&message, "evt");
const char *nonce = GetStrMember(&message, "nonce");
if (nonce) {
// in responses only -- should use to match up response when needed.
if (evtName && strcmp(evtName, "ERROR") == 0) {
auto data = GetObjMember(&message, "data");
LastErrorCode = GetIntMember(data, "code");
StringCopy(LastErrorMessage, GetStrMember(data, "message", ""));
GotErrorMessage.store(true);
}
}
else {
// should have evt == name of event, optional data
if (evtName == nullptr) {
continue;
}
auto data = GetObjMember(&message, "data");
if (strcmp(evtName, "ACTIVITY_JOIN") == 0) {
auto secret = GetStrMember(data, "secret");
if (secret) {
StringCopy(JoinGameSecret, secret);
WasJoinGame.store(true);
}
}
else if (strcmp(evtName, "ACTIVITY_SPECTATE") == 0) {
auto secret = GetStrMember(data, "secret");
if (secret) {
StringCopy(SpectateGameSecret, secret);
WasSpectateGame.store(true);
}
}
else if (strcmp(evtName, "ACTIVITY_JOIN_REQUEST") == 0) {
auto user = GetObjMember(data, "user");
auto userId = GetStrMember(user, "id");
auto username = GetStrMember(user, "username");
auto avatar = GetStrMember(user, "avatar");
auto joinReq = JoinAskQueue.GetNextAddMessage();
if (userId && username && joinReq) {
StringCopy(joinReq->userId, userId);
StringCopy(joinReq->username, username);
auto discriminator = GetStrMember(user, "discriminator");
if (discriminator) {
StringCopy(joinReq->discriminator, discriminator);
}
if (avatar) {
StringCopy(joinReq->avatar, avatar);
}
else {
joinReq->avatar[0] = 0;
}
JoinAskQueue.CommitAdd();
}
}
}
}
// writes
if (UpdatePresence.exchange(false) && QueuedPresence.length) {
QueuedMessage local;
{
std::lock_guard<std::mutex> guard(PresenceMutex);
local.Copy(QueuedPresence);
}
if (!Connection->Write(local.buffer, local.length)) {
// if we fail to send, requeue
std::lock_guard<std::mutex> guard(PresenceMutex);
QueuedPresence.Copy(local);
UpdatePresence.exchange(true);
}
}
while (SendQueue.HavePendingSends()) {
auto qmessage = SendQueue.GetNextSendMessage();
Connection->Write(qmessage->buffer, qmessage->length);
SendQueue.CommitSend();
}
}
}
extern "C" void Discord_Initialize(const char *applicationId, DiscordEventHandlers *handlers, const int autoRegister) {
IoThread = new (std::nothrow) IoThreadHolder();
if (IoThread == nullptr) {
return;
}
if (autoRegister) {
Discord_Register(applicationId, nullptr);
}
Pid = GetProcessId();
{
std::lock_guard<std::mutex> guard(HandlerMutex);
if (handlers) {
QueuedHandlers = *handlers;
}
else {
QueuedHandlers = {};
}
Handlers = {};
}
if (Connection) {
return;
}
Connection = RpcConnection::Create(applicationId);
Connection->onConnect = [](JsonDocument &readyMessage) {
Discord_UpdateHandlers(&QueuedHandlers);
if (QueuedPresence.length > 0) {
UpdatePresence.exchange(true);
SignalIOActivity();
}
auto data = GetObjMember(&readyMessage, "data");
auto user = GetObjMember(data, "user");
auto userId = GetStrMember(user, "id");
auto username = GetStrMember(user, "username");
auto avatar = GetStrMember(user, "avatar");
if (userId && username) {
StringCopy(connectedUser.userId, userId);
StringCopy(connectedUser.username, username);
auto discriminator = GetStrMember(user, "discriminator");
if (discriminator) {
StringCopy(connectedUser.discriminator, discriminator);
}
if (avatar) {
StringCopy(connectedUser.avatar, avatar);
}
else {
connectedUser.avatar[0] = 0;
}
}
WasJustConnected.exchange(true);
ReconnectTimeMs.reset();
};
Connection->onDisconnect = [](int err, const char *message) {
LastDisconnectErrorCode = err;
StringCopy(LastDisconnectErrorMessage, message);
WasJustDisconnected.exchange(true);
UpdateReconnectTime();
};
IoThread->Start();
}
extern "C" void Discord_Shutdown() {
if (!Connection) {
return;
}
Connection->onConnect = nullptr;
Connection->onDisconnect = nullptr;
Handlers = {};
QueuedPresence.length = 0;
UpdatePresence.exchange(false);
if (IoThread != nullptr) {
IoThread->Stop();
delete IoThread;
IoThread = nullptr;
}
RpcConnection::Destroy(Connection);
}
extern "C" void Discord_UpdatePresence(const DiscordRichPresence *presence) {
{
std::lock_guard<std::mutex> guard(PresenceMutex);
QueuedPresence.length = JsonWriteRichPresenceObj(QueuedPresence.buffer, sizeof(QueuedPresence.buffer), Nonce++, Pid, presence);
UpdatePresence.exchange(true);
}
SignalIOActivity();
}
extern "C" void Discord_ClearPresence(void) {
Discord_UpdatePresence(nullptr);
}
extern "C" void Discord_Respond(const char *userId, /* DISCORD_REPLY_ */ int reply) {
// if we are not connected, let's not batch up stale messages for later
if (!Connection || !Connection->IsOpen()) {
return;
}
auto qmessage = SendQueue.GetNextAddMessage();
if (qmessage) {
qmessage->length = JsonWriteJoinReply(qmessage->buffer, sizeof(qmessage->buffer), userId, reply, Nonce++);
SendQueue.CommitAdd();
SignalIOActivity();
}
}
extern "C" void Discord_RunCallbacks() {
// Note on some weirdness: internally we might connect, get other signals, disconnect any number
// of times inbetween calls here. Externally, we want the sequence to seem sane, so any other
// signals are book-ended by calls to ready and disconnect.
if (!Connection) {
return;
}
const bool wasDisconnected = WasJustDisconnected.exchange(false);
const bool isConnected = Connection->IsOpen();
if (isConnected) {
// if we are connected, disconnect cb first
std::lock_guard<std::mutex> guard(HandlerMutex);
if (wasDisconnected && Handlers.disconnected) {
Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage);
}
}
if (WasJustConnected.exchange(false)) {
std::lock_guard<std::mutex> guard(HandlerMutex);
if (Handlers.ready) {
DiscordUser du{ connectedUser.userId, connectedUser.username, connectedUser.discriminator, connectedUser.avatar };
Handlers.ready(&du);
}
}
if (GotErrorMessage.exchange(false)) {
std::lock_guard<std::mutex> guard(HandlerMutex);
if (Handlers.errored) {
Handlers.errored(LastErrorCode, LastErrorMessage);
}
}
if (WasJoinGame.exchange(false)) {
std::lock_guard<std::mutex> guard(HandlerMutex);
if (Handlers.joinGame) {
Handlers.joinGame(JoinGameSecret);
}
}
if (WasSpectateGame.exchange(false)) {
std::lock_guard<std::mutex> guard(HandlerMutex);
if (Handlers.spectateGame) {
Handlers.spectateGame(SpectateGameSecret);
}
}
// Right now this batches up any requests and sends them all in a burst; I could imagine a world
// where the implementer would rather sequentially accept/reject each one before the next invite
// is sent. I left it this way because I could also imagine wanting to process these all and
// maybe show them in one common dialog and/or start fetching the avatars in parallel, and if
// not it should be trivial for the implementer to make a queue themselves.
while (JoinAskQueue.HavePendingSends()) {
const auto req = JoinAskQueue.GetNextSendMessage();
{
std::lock_guard<std::mutex> guard(HandlerMutex);
if (Handlers.joinRequest) {
DiscordUser du{ req->userId, req->username, req->discriminator, req->avatar };
Handlers.joinRequest(&du);
}
}
JoinAskQueue.CommitSend();
}
if (!isConnected) {
// if we are not connected, disconnect message last
std::lock_guard<std::mutex> guard(HandlerMutex);
if (wasDisconnected && Handlers.disconnected) {
Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage);
}
}
}
extern "C" void Discord_UpdateHandlers(DiscordEventHandlers *newHandlers) {
if (newHandlers) {
#define HANDLE_EVENT_REGISTRATION(handler_name, event) \
if (!Handlers.handler_name && newHandlers->handler_name) { \
RegisterForEvent(event); \
} \
else if (Handlers.handler_name && !newHandlers->handler_name) { \
DeregisterForEvent(event); \
}
std::lock_guard<std::mutex> guard(HandlerMutex);
HANDLE_EVENT_REGISTRATION(joinGame, "ACTIVITY_JOIN")
HANDLE_EVENT_REGISTRATION(spectateGame, "ACTIVITY_SPECTATE")
HANDLE_EVENT_REGISTRATION(joinRequest, "ACTIVITY_JOIN_REQUEST")
#undef HANDLE_EVENT_REGISTRATION
Handlers = *newHandlers;
}
else {
std::lock_guard<std::mutex> guard(HandlerMutex);
Handlers = {};
}
}

94
3rdparty/discord-rpc/discord_rpc.h vendored Normal file
View File

@@ -0,0 +1,94 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#ifndef DISCORD_RPC_H
#define DISCORD_RPC_H
#include <cstdint>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct DiscordRichPresence {
int type;
int status_display_type;
const char *name; /* max 128 bytes */
const char *state; /* max 128 bytes */
const char *details; /* max 128 bytes */
int64_t startTimestamp;
int64_t endTimestamp;
const char *largeImageKey; /* max 32 bytes */
const char *largeImageText; /* max 128 bytes */
const char *smallImageKey; /* max 32 bytes */
const char *smallImageText; /* max 128 bytes */
const char *partyId; /* max 128 bytes */
int partySize;
int partyMax;
int partyPrivacy;
const char *matchSecret; /* max 128 bytes */
const char *joinSecret; /* max 128 bytes */
const char *spectateSecret; /* max 128 bytes */
int8_t instance;
} DiscordRichPresence;
typedef struct DiscordUser {
const char *userId;
const char *username;
const char *discriminator;
const char *avatar;
} DiscordUser;
typedef struct DiscordEventHandlers {
void (*ready)(const DiscordUser *request);
void (*disconnected)(int errorCode, const char *message);
void (*errored)(int errorCode, const char *message);
void (*joinGame)(const char *joinSecret);
void (*spectateGame)(const char *spectateSecret);
void (*joinRequest)(const DiscordUser *request);
} DiscordEventHandlers;
#define DISCORD_REPLY_NO 0
#define DISCORD_REPLY_YES 1
#define DISCORD_REPLY_IGNORE 2
#define DISCORD_PARTY_PRIVATE 0
#define DISCORD_PARTY_PUBLIC 1
void Discord_Initialize(const char *applicationId, DiscordEventHandlers *handlers, const int autoRegister);
void Discord_Shutdown(void);
// checks for incoming messages, dispatches callbacks
void Discord_RunCallbacks(void);
void Discord_UpdatePresence(const DiscordRichPresence *presence);
void Discord_ClearPresence(void);
void Discord_Respond(const char *userid, /* DISCORD_REPLY_ */ int reply);
void Discord_UpdateHandlers(DiscordEventHandlers *handlers);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif // DISCORD_RPC_H

View File

@@ -0,0 +1,168 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#include "discord_rpc_connection.h"
#include "discord_serialization.h"
namespace discord_rpc {
static const int RpcVersion = 1;
static RpcConnection Instance;
RpcConnection *RpcConnection::Create(const char *applicationId) {
Instance.connection = BaseConnection::Create();
StringCopy(Instance.appId, applicationId);
return &Instance;
}
void RpcConnection::Destroy(RpcConnection *&c) {
c->Close();
BaseConnection::Destroy(c->connection);
c = nullptr;
}
void RpcConnection::Open() {
if (state == State::Connected) {
return;
}
if (state == State::Disconnected && !connection->Open()) {
return;
}
if (state == State::SentHandshake) {
JsonDocument message;
if (Read(message)) {
auto cmd = GetStrMember(&message, "cmd");
auto evt = GetStrMember(&message, "evt");
if (cmd && evt && !strcmp(cmd, "DISPATCH") && !strcmp(evt, "READY")) {
state = State::Connected;
if (onConnect) {
onConnect(message);
}
}
}
}
else {
sendFrame.opcode = Opcode::Handshake;
sendFrame.length = static_cast<uint32_t>(JsonWriteHandshakeObj(sendFrame.message, sizeof(sendFrame.message), RpcVersion, appId));
if (connection->Write(&sendFrame, sizeof(MessageFrameHeader) + sendFrame.length)) {
state = State::SentHandshake;
}
else {
Close();
}
}
}
void RpcConnection::Close() {
if (onDisconnect && (state == State::Connected || state == State::SentHandshake)) {
onDisconnect(lastErrorCode, lastErrorMessage);
}
connection->Close();
state = State::Disconnected;
}
bool RpcConnection::Write(const void *data, size_t length) {
sendFrame.opcode = Opcode::Frame;
memcpy(sendFrame.message, data, length);
sendFrame.length = static_cast<uint32_t>(length);
if (!connection->Write(&sendFrame, sizeof(MessageFrameHeader) + length)) {
Close();
return false;
}
return true;
}
bool RpcConnection::Read(JsonDocument &message) {
if (state != State::Connected && state != State::SentHandshake) {
return false;
}
MessageFrame readFrame{};
for (;;) {
bool didRead = connection->Read(&readFrame, sizeof(MessageFrameHeader));
if (!didRead) {
if (!connection->isOpen) {
lastErrorCode = static_cast<int>(ErrorCode::PipeClosed);
StringCopy(lastErrorMessage, "Pipe closed");
Close();
}
return false;
}
if (readFrame.length > 0) {
didRead = connection->Read(readFrame.message, readFrame.length);
if (!didRead) {
lastErrorCode = static_cast<int>(ErrorCode::ReadCorrupt);
StringCopy(lastErrorMessage, "Partial data in frame");
Close();
return false;
}
readFrame.message[readFrame.length] = 0;
}
switch (readFrame.opcode) {
case Opcode::Close: {
message.ParseInsitu(readFrame.message);
lastErrorCode = GetIntMember(&message, "code");
StringCopy(lastErrorMessage, GetStrMember(&message, "message", ""));
Close();
return false;
}
case Opcode::Frame:
message.ParseInsitu(readFrame.message);
return true;
case Opcode::Ping:
readFrame.opcode = Opcode::Pong;
if (!connection->Write(&readFrame, sizeof(MessageFrameHeader) + readFrame.length)) {
Close();
}
break;
case Opcode::Pong:
break;
case Opcode::Handshake:
default:
// something bad happened
lastErrorCode = static_cast<int>(ErrorCode::ReadCorrupt);
StringCopy(lastErrorMessage, "Bad ipc frame");
Close();
return false;
}
}
}
} // namespace discord_rpc

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#ifndef DISCORD_RPC_CONNECTION_H
#define DISCORD_RPC_CONNECTION_H
#include "discord_connection.h"
#include "discord_serialization.h"
namespace discord_rpc {
// I took this from the buffer size libuv uses for named pipes; I suspect ours would usually be much smaller.
constexpr size_t MaxRpcFrameSize = 64 * 1024;
struct RpcConnection {
enum class ErrorCode : int {
Success = 0,
PipeClosed = 1,
ReadCorrupt = 2,
};
enum class Opcode : uint32_t {
Handshake = 0,
Frame = 1,
Close = 2,
Ping = 3,
Pong = 4,
};
struct MessageFrameHeader {
Opcode opcode;
uint32_t length;
};
struct MessageFrame : public MessageFrameHeader {
char message[MaxRpcFrameSize - sizeof(MessageFrameHeader)];
};
enum class State : uint32_t {
Disconnected,
SentHandshake,
AwaitingResponse,
Connected,
};
BaseConnection *connection{ nullptr };
State state{ State::Disconnected };
void (*onConnect)(JsonDocument &message) { nullptr };
void (*onDisconnect)(int errorCode, const char *message) { nullptr };
char appId[64]{};
int lastErrorCode{ 0 };
char lastErrorMessage[256]{};
RpcConnection::MessageFrame sendFrame;
static RpcConnection *Create(const char *applicationId);
static void Destroy(RpcConnection*&);
inline bool IsOpen() const { return state == State::Connected; }
void Open();
void Close();
bool Write(const void *data, size_t length);
bool Read(JsonDocument &message);
};
} // namespace discord_rpc
#endif // DISCORD_RPC_CONNECTION_H

View File

@@ -0,0 +1,285 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#include "discord_serialization.h"
#include "discord_connection.h"
#include "discord_rpc.h"
namespace discord_rpc {
template<typename T>
void NumberToString(char *dest, T number) {
if (!number) {
*dest++ = '0';
*dest++ = 0;
return;
}
if (number < 0) {
*dest++ = '-';
number = -number;
}
char temp[32];
int place = 0;
while (number) {
auto digit = number % 10;
number = number / 10;
temp[place++] = '0' + static_cast<char>(digit);
}
for (--place; place >= 0; --place) {
*dest++ = temp[place];
}
*dest = 0;
}
// it's ever so slightly faster to not have to strlen the key
template<typename T>
void WriteKey(JsonWriter &w, T &k) {
w.Key(k, sizeof(T) - 1);
}
struct WriteObject {
JsonWriter &writer;
WriteObject(JsonWriter &w)
: writer(w) {
writer.StartObject();
}
template<typename T>
WriteObject(JsonWriter &w, T &name)
: writer(w) {
WriteKey(writer, name);
writer.StartObject();
}
~WriteObject() { writer.EndObject(); }
};
struct WriteArray {
JsonWriter &writer;
template<typename T>
WriteArray(JsonWriter &w, T &name)
: writer(w) {
WriteKey(writer, name);
writer.StartArray();
}
~WriteArray() { writer.EndArray(); }
};
template<typename T>
void WriteOptionalString(JsonWriter &w, T &k, const char *value) {
if (value && value[0]) {
w.Key(k, sizeof(T) - 1);
w.String(value);
}
}
static void JsonWriteNonce(JsonWriter &writer, const int nonce) {
WriteKey(writer, "nonce");
char nonceBuffer[32];
NumberToString(nonceBuffer, nonce);
writer.String(nonceBuffer);
}
size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence *presence) {
JsonWriter writer(dest, maxLen);
{
WriteObject top(writer);
JsonWriteNonce(writer, nonce);
WriteKey(writer, "cmd");
writer.String("SET_ACTIVITY");
{
WriteObject args(writer, "args");
WriteKey(writer, "pid");
writer.Int(pid);
if (presence != nullptr) {
WriteObject activity(writer, "activity");
if (presence->type >= 0 && presence->type <= 5) {
WriteKey(writer, "type");
writer.Int(presence->type);
WriteKey(writer, "status_display_type");
writer.Int(presence->status_display_type);
}
WriteOptionalString(writer, "name", presence->name);
WriteOptionalString(writer, "state", presence->state);
WriteOptionalString(writer, "details", presence->details);
if (presence->startTimestamp || presence->endTimestamp) {
WriteObject timestamps(writer, "timestamps");
if (presence->startTimestamp) {
WriteKey(writer, "start");
writer.Int64(presence->startTimestamp);
}
if (presence->endTimestamp) {
WriteKey(writer, "end");
writer.Int64(presence->endTimestamp);
}
}
if ((presence->largeImageKey && presence->largeImageKey[0]) ||
(presence->largeImageText && presence->largeImageText[0]) ||
(presence->smallImageKey && presence->smallImageKey[0]) ||
(presence->smallImageText && presence->smallImageText[0])) {
WriteObject assets(writer, "assets");
WriteOptionalString(writer, "large_image", presence->largeImageKey);
WriteOptionalString(writer, "large_text", presence->largeImageText);
WriteOptionalString(writer, "small_image", presence->smallImageKey);
WriteOptionalString(writer, "small_text", presence->smallImageText);
}
if ((presence->partyId && presence->partyId[0]) || presence->partySize ||
presence->partyMax || presence->partyPrivacy) {
WriteObject party(writer, "party");
WriteOptionalString(writer, "id", presence->partyId);
if (presence->partySize && presence->partyMax) {
WriteArray size(writer, "size");
writer.Int(presence->partySize);
writer.Int(presence->partyMax);
}
if (presence->partyPrivacy) {
WriteKey(writer, "privacy");
writer.Int(presence->partyPrivacy);
}
}
if ((presence->matchSecret && presence->matchSecret[0]) ||
(presence->joinSecret && presence->joinSecret[0]) ||
(presence->spectateSecret && presence->spectateSecret[0])) {
WriteObject secrets(writer, "secrets");
WriteOptionalString(writer, "match", presence->matchSecret);
WriteOptionalString(writer, "join", presence->joinSecret);
WriteOptionalString(writer, "spectate", presence->spectateSecret);
}
writer.Key("instance");
writer.Bool(presence->instance != 0);
}
}
}
return writer.Size();
}
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId) {
JsonWriter writer(dest, maxLen);
{
WriteObject obj(writer);
WriteKey(writer, "v");
writer.Int(version);
WriteKey(writer, "client_id");
writer.String(applicationId);
}
return writer.Size();
}
size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) {
JsonWriter writer(dest, maxLen);
{
WriteObject obj(writer);
JsonWriteNonce(writer, nonce);
WriteKey(writer, "cmd");
writer.String("SUBSCRIBE");
WriteKey(writer, "evt");
writer.String(evtName);
}
return writer.Size();
}
size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) {
JsonWriter writer(dest, maxLen);
{
WriteObject obj(writer);
JsonWriteNonce(writer, nonce);
WriteKey(writer, "cmd");
writer.String("UNSUBSCRIBE");
WriteKey(writer, "evt");
writer.String(evtName);
}
return writer.Size();
}
size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, const int reply, const int nonce) {
JsonWriter writer(dest, maxLen);
{
WriteObject obj(writer);
WriteKey(writer, "cmd");
if (reply == DISCORD_REPLY_YES) {
writer.String("SEND_ACTIVITY_JOIN_INVITE");
}
else {
writer.String("CLOSE_ACTIVITY_JOIN_REQUEST");
}
WriteKey(writer, "args");
{
WriteObject args(writer);
WriteKey(writer, "user_id");
writer.String(userId);
}
JsonWriteNonce(writer, nonce);
}
return writer.Size();
}
} // namespace discord_rpc

View File

@@ -0,0 +1,213 @@
/*
* Copyright 2017 Discord, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
#ifndef DISCORD_SERIALIZATION_H
#define DISCORD_SERIALIZATION_H
#include <rapidjson/document.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
struct DiscordRichPresence;
namespace discord_rpc {
// if only there was a standard library function for this
template<size_t Len>
inline size_t StringCopy(char (&dest)[Len], const char *src) {
if (!src || !Len) {
return 0;
}
size_t copied;
char *out = dest;
for (copied = 1; *src && copied < Len; ++copied) {
*out++ = *src++;
}
*out = 0;
return copied - 1;
}
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId);
// Commands
size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence *presence);
size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName);
size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName);
size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, int reply, int nonce);
// I want to use as few allocations as I can get away with, and to do that with RapidJson, you need
// to supply some of your own allocators for stuff rather than use the defaults
class LinearAllocator {
public:
char *buffer_;
char *end_;
LinearAllocator() {
assert(0); // needed for some default case in rapidjson, should not use
}
LinearAllocator(char *buffer, size_t size)
: buffer_(buffer), end_(buffer + size) {
}
static const bool kNeedFree = false;
void *Malloc(size_t size) {
char *res = buffer_;
buffer_ += size;
if (buffer_ > end_) {
buffer_ = res;
return nullptr;
}
return res;
}
void *Realloc(void *originalPtr, size_t originalSize, size_t newSize) {
if (newSize == 0) {
return nullptr;
}
// allocate how much you need in the first place
assert(!originalPtr && !originalSize);
// unused parameter warning
(void)(originalPtr);
(void)(originalSize);
return Malloc(newSize);
}
static void Free(void *ptr) {
/* shrug */
(void)ptr;
}
};
template<size_t Size>
class FixedLinearAllocator : public LinearAllocator {
public:
char fixedBuffer_[Size];
FixedLinearAllocator()
: LinearAllocator(fixedBuffer_, Size) {
}
static const bool kNeedFree = false;
};
// wonder why this isn't a thing already, maybe I missed it
class DirectStringBuffer {
public:
using Ch = char;
char *buffer_;
char *end_;
char *current_;
DirectStringBuffer(char *buffer, size_t maxLen)
: buffer_(buffer), end_(buffer + maxLen), current_(buffer) {
}
void Put(char c) {
if (current_ < end_) {
*current_++ = c;
}
}
void Flush() {}
size_t GetSize() const { return static_cast<size_t>(current_ - buffer_); }
};
using MallocAllocator = rapidjson::CrtAllocator;
using PoolAllocator = rapidjson::MemoryPoolAllocator<MallocAllocator>;
using UTF8 = rapidjson::UTF8<char>;
// Writer appears to need about 16 bytes per nested object level (with 64bit size_t)
using StackAllocator = FixedLinearAllocator<2048>;
constexpr size_t WriterNestingLevels = 2048 / (2 * sizeof(size_t));
using JsonWriterBase =
rapidjson::Writer<DirectStringBuffer, UTF8, UTF8, StackAllocator, rapidjson::kWriteNoFlags>;
class JsonWriter : public JsonWriterBase {
public:
DirectStringBuffer stringBuffer_;
StackAllocator stackAlloc_;
JsonWriter(char *dest, size_t maxLen)
: JsonWriterBase(stringBuffer_, &stackAlloc_, WriterNestingLevels), stringBuffer_(dest, maxLen), stackAlloc_() {
}
size_t Size() const { return stringBuffer_.GetSize(); }
};
using JsonDocumentBase = rapidjson::GenericDocument<UTF8, PoolAllocator, StackAllocator>;
class JsonDocument : public JsonDocumentBase {
public:
static const int kDefaultChunkCapacity = 32 * 1024;
// json parser will use this buffer first, then allocate more if needed; I seriously doubt we
// send any messages that would use all of this, though.
char parseBuffer_[32 * 1024];
MallocAllocator mallocAllocator_;
PoolAllocator poolAllocator_;
StackAllocator stackAllocator_;
JsonDocument()
: JsonDocumentBase(rapidjson::kObjectType,
&poolAllocator_,
sizeof(stackAllocator_.fixedBuffer_),
&stackAllocator_),
poolAllocator_(parseBuffer_, sizeof(parseBuffer_), kDefaultChunkCapacity, &mallocAllocator_), stackAllocator_() {
}
};
using JsonValue = rapidjson::GenericValue<UTF8, PoolAllocator>;
inline JsonValue *GetObjMember(JsonValue *obj, const char *name) {
if (obj) {
auto member = obj->FindMember(name);
if (member != obj->MemberEnd() && member->value.IsObject()) {
return &member->value;
}
}
return nullptr;
}
inline int GetIntMember(JsonValue *obj, const char *name, int notFoundDefault = 0) {
if (obj) {
auto member = obj->FindMember(name);
if (member != obj->MemberEnd() && member->value.IsInt()) {
return member->value.GetInt();
}
}
return notFoundDefault;
}
inline const char *GetStrMember(JsonValue *obj, const char *name, const char *notFoundDefault = nullptr) {
if (obj) {
auto member = obj->FindMember(name);
if (member != obj->MemberEnd() && member->value.IsString()) {
return member->value.GetString();
}
}
return notFoundDefault;
}
} // namespace discord_rpc
#endif // DISCORD_SERIALIZATION_H

View File

@@ -208,28 +208,26 @@ else()
pkg_check_modules(TAGLIB REQUIRED IMPORTED_TARGET taglib>=1.12)
endif()
find_package(projectM4 COMPONENTS Playlist)
if(projectM4_FOUND)
set(LIBPROJECTM_FOUND ON)
set(HAVE_PROJECTM4 ON)
set(LIBPROJECTM_LIBRARIES libprojectM::projectM libprojectM::playlist)
else()
pkg_check_modules(LIBPROJECTM libprojectM)
endif()
find_package(GTest)
pkg_check_modules(LIBSPARSEHASH IMPORTED_TARGET libsparsehash)
find_package(discord-rpc)
if(TARGET discord-rpc::discord-rpc)
set(DISCORD_RPC_FOUND ON)
set(DISCORD_RPC_LIBRARIES "discord-rpc::discord-rpc")
else()
find_library(DISCORD_RPC_LIBRARY discord-rpc)
find_path(DISCORD_RPC_INCLUDE_DIRS NAMES discord-rpc.h)
if(DISCORD_RPC_LIBRARY)
set(DISCORD_RPC_FOUND ON)
set(DISCORD_RPC_LIBRARIES ${DISCORD_RPC_LIBRARY})
endif()
endif()
find_package(RapidJSON)
set(QT_VERSION_MAJOR 6)
set(QT_MIN_VERSION 6.4.0)
set(QT_DEFAULT_MAJOR_VERSION ${QT_VERSION_MAJOR})
set(QT_COMPONENTS Core Concurrent Gui Widgets Network Sql)
set(QT_OPTIONAL_COMPONENTS GuiPrivate LinguistTools Test)
set(QT_OPTIONAL_COMPONENTS GuiPrivate OpenGLWidgets LinguistTools Test)
if(UNIX AND NOT APPLE)
list(APPEND QT_OPTIONAL_COMPONENTS DBus)
endif()
@@ -389,7 +387,7 @@ optional_component(STREAMTAGREADER ON "Stream tagreader"
)
optional_component(DISCORD_RPC ON "Discord Rich Presence"
DEPENDS "discord-rpc" DISCORD_RPC_FOUND
DEPENDS "RapidJSON" RapidJSON_FOUND
)
if(HAVE_SONGFINGERPRINTING OR HAVE_MUSICBRAINZ)
@@ -400,6 +398,11 @@ if(HAVE_X11_GLOBALSHORTCUTS OR HAVE_KGLOBALACCEL_GLOBALSHORTCUTS OR APPLE OR WIN
set(HAVE_GLOBALSHORTCUTS ON)
endif()
optional_component(VISUALIZATIONS ON "Visualizations"
DEPENDS "libprojectm" LIBPROJECTM_FOUND
DEPENDS "QtOpenGLWidgets" Qt${QT_VERSION_MAJOR}OpenGLWidgets_FOUND
)
if(NOT CMAKE_CROSSCOMPILING)
# Check that we have Qt with sqlite driver
set(CMAKE_REQUIRED_FLAGS "-std=c++17")
@@ -1491,6 +1494,26 @@ optional_source(HAVE_QOBUZ
src/settings/qobuzsettingspage.ui
)
optional_source(HAVE_VISUALIZATIONS
SOURCES
src/visualizations/projectmpresetmodel.cpp
src/visualizations/projectmvisualization.cpp
src/visualizations/visualizationcontainer.cpp
src/visualizations/visualizationoverlay.cpp
src/visualizations/visualizationselector.cpp
src/visualizations/visualizationopenglwidget.cpp
HEADERS
src/visualizations/projectmpresetmodel.h
src/visualizations/projectmvisualization.h
src/visualizations/visualizationcontainer.h
src/visualizations/visualizationoverlay.h
src/visualizations/visualizationselector.h
src/visualizations/visualizationopenglwidget.h
UI
src/visualizations/visualizationoverlay.ui
src/visualizations/visualizationselector.ui
)
qt_wrap_cpp(SOURCES ${HEADERS})
qt_wrap_ui(SOURCES ${UI})
qt_add_resources(SOURCES data/data.qrc data/icons.qrc)
@@ -1514,6 +1537,11 @@ if(LINUX AND LSB_RELEASE_EXEC AND DPKG_BUILDPACKAGE)
add_subdirectory(debian)
endif()
if(HAVE_DISCORD_RPC)
add_subdirectory(3rdparty/discord-rpc)
target_include_directories(strawberry_lib PUBLIC 3rdparty/discord-rpc)
endif()
if(HAVE_TRANSLATIONS)
qt_add_lupdate(strawberry_lib TS_FILES "${CMAKE_SOURCE_DIR}/src/translations/strawberry_en_US.ts" OPTIONS -locations none -no-ui-lines -no-obsolete)
file(GLOB_RECURSE ts_files ${CMAKE_SOURCE_DIR}/src/translations/*.ts)
@@ -1535,10 +1563,6 @@ if(SINGLEAPPLICATION_INCLUDE_DIRS)
target_include_directories(strawberry_lib SYSTEM PUBLIC ${SINGLEAPPLICATION_INCLUDE_DIRS})
endif()
if(DISCORD_RPC_INCLUDE_DIRS)
target_include_directories(strawberry_lib SYSTEM PUBLIC ${DISCORD_RPC_INCLUDE_DIRS})
endif()
target_link_libraries(strawberry_lib PUBLIC
${CMAKE_THREAD_LIBS_INIT}
$<$<BOOL:${HAVE_BACKTRACE}>:${Backtrace_LIBRARIES}>
@@ -1560,6 +1584,7 @@ target_link_libraries(strawberry_lib PUBLIC
Qt${QT_VERSION_MAJOR}::Sql
$<$<BOOL:${HAVE_DBUS}>:Qt${QT_VERSION_MAJOR}::DBus>
$<$<BOOL:${HAVE_QPA_QPLATFORMNATIVEINTERFACE}>:Qt${QT_VERSION_MAJOR}::GuiPrivate>
$<$<BOOL:${HAVE_VISUALIZATIONS}>:Qt${QT_VERSION_MAJOR}::OpenGLWidgets>
ICU::uc
ICU::i18n
$<$<BOOL:${HAVE_STREAMTAGREADER}>:PkgConfig::LIBSPARSEHASH>
@@ -1575,10 +1600,11 @@ target_link_libraries(strawberry_lib PUBLIC
$<$<BOOL:${HAVE_MTP}>:PkgConfig::LIBMTP>
$<$<BOOL:${HAVE_GPOD}>:PkgConfig::LIBGPOD PkgConfig::GDK_PIXBUF>
$<$<BOOL:${HAVE_QTSPARKLE}>:qtsparkle-qt${QT_VERSION_MAJOR}::qtsparkle>
$<$<BOOL:${HAVE_VISUALIZATIONS}>:${LIBPROJECTM_LIBRARIES}>
$<$<BOOL:${WIN32}>:dsound dwmapi ${GETOPT_LIBRARIES}>
$<$<BOOL:${MSVC}>:WindowsApp>
KDAB::kdsingleapplication
$<$<BOOL:${HAVE_DISCORD_RPC}>:${DISCORD_RPC_LIBRARIES}>
$<$<BOOL:${HAVE_DISCORD_RPC}>:discord-rpc>
)
if(APPLE)

3
debian/control vendored
View File

@@ -32,7 +32,8 @@ Build-Depends: debhelper-compat (= 12),
libfftw3-dev,
libebur128-dev,
libsparsehash-dev,
rapidjson-dev
rapidjson-dev,
libprojectm-dev
Standards-Version: 4.7.0
Package: strawberry

View File

@@ -43,6 +43,7 @@ BuildRequires: pkgconfig(taglib)
BuildRequires: pkgconfig(fftw3)
BuildRequires: pkgconfig(icu-uc)
BuildRequires: pkgconfig(icu-i18n)
BuildRequires: pkgconfig(libprojectM)
BuildRequires: cmake(Qt@QT_VERSION_MAJOR@Core)
BuildRequires: cmake(Qt@QT_VERSION_MAJOR@Concurrent)
BuildRequires: cmake(Qt@QT_VERSION_MAJOR@Network)

View File

@@ -73,7 +73,8 @@ AnalyzerContainer::AnalyzerContainer(QWidget *parent)
double_click_timer_(new QTimer(this)),
ignore_next_click_(false),
current_analyzer_(nullptr),
engine_(nullptr) {
engine_(nullptr),
action_visualization_(nullptr) {
QHBoxLayout *layout = new QHBoxLayout(this);
setLayout(layout);
@@ -118,6 +119,17 @@ void AnalyzerContainer::mouseReleaseEvent(QMouseEvent *e) {
}
void AnalyzerContainer::mouseDoubleClickEvent(QMouseEvent *e) {
Q_UNUSED(e);
double_click_timer_->stop();
ignore_next_click_ = true;
if (action_visualization_) action_visualization_->trigger();
}
void AnalyzerContainer::ShowPopupMenu() {
context_menu_->popup(last_click_pos_);
}
@@ -249,3 +261,10 @@ void AnalyzerContainer::AddFramerate(const QString &name, const int framerate) {
QObject::connect(action, &QAction::triggered, this, [this, framerate]() { ChangeFramerate(framerate); } );
}
void AnalyzerContainer::SetVisualizationsAction(QAction *visualization) {
action_visualization_ = visualization;
context_menu_->addAction(action_visualization_);
}

View File

@@ -46,6 +46,7 @@ class AnalyzerContainer : public QWidget {
explicit AnalyzerContainer(QWidget *parent);
void SetEngine(SharedPtr<EngineBase> engine);
void SetVisualizationsAction(QAction *visualization);
static const char *kSettingsGroup;
static const char *kSettingsFramerate;
@@ -55,6 +56,7 @@ class AnalyzerContainer : public QWidget {
protected:
void mouseReleaseEvent(QMouseEvent *e) override;
void mouseDoubleClickEvent(QMouseEvent *e) override;
void wheelEvent(QWheelEvent *e) override;
private Q_SLOTS:
@@ -89,6 +91,8 @@ class AnalyzerContainer : public QWidget {
AnalyzerBase *current_analyzer_;
SharedPtr<EngineBase> engine_;
QAction *action_visualization_;
};
template<typename T>

View File

@@ -48,4 +48,7 @@
#cmakedefine ENABLE_WIN32_CONSOLE
#cmakedefine HAVE_VISUALIZATIONS
#cmakedefine HAVE_PROJECTM4
#endif // CONFIG_H_IN

View File

@@ -206,6 +206,11 @@
#include "organize/organizeerrordialog.h"
#ifdef HAVE_VISUALIZATIONS
# include "visualizations/visualizationcontainer.h"
# include "engine/gstengine.h"
#endif
#ifdef Q_OS_WIN32
# include "core/windows7thumbbar.h"
#endif
@@ -623,6 +628,12 @@ MainWindow::MainWindow(Application *app,
stop_menu->addAction(ui_->action_stop_after_this_track);
ui_->stop_button->setMenu(stop_menu);
#ifdef HAVE_VISUALIZATIONS
QObject::connect(ui_->action_visualizations, &QAction::triggered, this, &MainWindow::ShowVisualizations);
#else
ui_->action_visualizations->setEnabled(false);
#endif
// Player connections
QObject::connect(ui_->volume, &VolumeSlider::valueChanged, &*app_->player(), &Player::SetVolumeFromSlider);
@@ -907,6 +918,7 @@ MainWindow::MainWindow(Application *app,
// Analyzer
QObject::connect(ui_->analyzer, &AnalyzerContainer::WheelEvent, this, &MainWindow::VolumeWheelEvent);
ui_->analyzer->SetVisualizationsAction(ui_->action_visualizations);
// Statusbar widgets
ui_->playlist_summary->setMinimumWidth(QFontMetrics(font()).horizontalAdvance(u"WW selected of WW tracks - [ WW:WW ]"_s));
@@ -3404,3 +3416,24 @@ void MainWindow::FocusSearchField() {
}
}
void MainWindow::ShowVisualizations() {
#ifdef HAVE_VISUALIZATIONS
if (!visualization_) {
visualization_.reset(new VisualizationContainer);
visualization_->SetActions(ui_->action_previous_track, ui_->action_play_pause, ui_->action_stop, ui_->action_next_track);
connect(&*app_->player(), &Player::Stopped, &*visualization_, &VisualizationContainer::Stopped);
connect(&*app_->player(), &Player::ForceShowOSD, &*visualization_, &VisualizationContainer::SongMetadataChanged);
connect(&*app_->playlist_manager(), &PlaylistManager::CurrentSongChanged, &*visualization_, &VisualizationContainer::SongMetadataChanged);
visualization_->SetEngine(qobject_cast<GstEngine*>(&*app_->player()->engine()));
}
visualization_->show();
#endif // HAVE_VISUALIZATIONS
}

View File

@@ -97,6 +97,7 @@ class Windows7ThumbBar;
class AddStreamDialog;
class LastFMImportDialog;
class RadioViewContainer;
class VisualizationContainer;
#ifdef HAVE_DISCORD_RPC
namespace discord {
@@ -281,6 +282,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
public Q_SLOTS:
void CommandlineOptionsReceived(const QByteArray &string_options);
void Raise();
void ShowVisualizations();
private:
void SaveSettings();
@@ -360,6 +362,10 @@ class MainWindow : public QMainWindow, public PlatformInterface {
LastFMImportDialog *lastfm_import_dialog_;
#ifdef HAVE_VISUALIZATIONS
ScopedPtr<VisualizationContainer> visualization_;
#endif
QAction *collection_show_all_;
QAction *collection_show_duplicates_;
QAction *collection_show_untagged_;

View File

@@ -517,6 +517,7 @@
<addaction name="action_cover_manager"/>
<addaction name="action_equalizer"/>
<addaction name="action_transcoder"/>
<addaction name="action_visualizations"/>
<addaction name="separator"/>
<addaction name="action_update_collection"/>
<addaction name="action_full_collection_scan"/>
@@ -863,6 +864,11 @@
<string>Import data from last.fm...</string>
</property>
</action>
<action name="action_visualizations">
<property name="text">
<string>Visualizations</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<customwidgets>

View File

@@ -75,7 +75,7 @@ void RichPresence::ReloadSettings() {
s.endGroup();
if (enabled && !initialized_) {
Discord_Initialize(kDiscordApplicationId, nullptr, 0, nullptr);
Discord_Initialize(kDiscordApplicationId, nullptr, 0);
initialized_ = true;
}
else if (!enabled && initialized_) {
@@ -124,6 +124,7 @@ void RichPresence::SendPresenceUpdate() {
// Listening to
presence_data.type = 2;
presence_data.status_display_type = status_display_type_;
presence_data.largeImageKey = kStrawberryIconResourceName;
presence_data.smallImageKey = kStrawberryIconResourceName;
presence_data.smallImageText = kStrawberryIconDescription;

View File

@@ -36,7 +36,7 @@ class GstBufferConsumer {
// This is called in some unspecified GStreamer thread.
// Ownership of the buffer is transferred to the BufferConsumer, and it should gst_buffer_unref it.
virtual void ConsumeBuffer(GstBuffer *buffer, const int pipeline_id, const QString &format) = 0;
virtual void ConsumeBuffer(GstBuffer *buffer, const int pipeline_id, const QString &format, const int channels) = 0;
private:
Q_DISABLE_COPY(GstBufferConsumer)

View File

@@ -543,7 +543,9 @@ void GstEngine::ReloadSettings() {
}
void GstEngine::ConsumeBuffer(GstBuffer *buffer, const int pipeline_id, const QString &format) {
void GstEngine::ConsumeBuffer(GstBuffer *buffer, const int pipeline_id, const QString &format, const int channels) {
Q_UNUSED(channels);
// Schedule this to run in the GUI thread. The buffer gets added to the queue and unreffed by UpdateScope.
if (!QMetaObject::invokeMethod(this, "AddBufferToScope", Q_ARG(GstBuffer*, buffer), Q_ARG(int, pipeline_id), Q_ARG(QString, format))) {

View File

@@ -84,7 +84,7 @@ class GstEngine : public EngineBase, public GstBufferConsumer {
bool ALSADeviceSupport(const QString &output) const override;
bool ExclusiveModeSupport(const QString &output) const override;
void ConsumeBuffer(GstBuffer *buffer, const int pipeline_id, const QString &format) override;
void ConsumeBuffer(GstBuffer *buffer, const int pipeline_id, const QString &format, const int channels) override;
public Q_SLOTS:
void ReloadSettings() override;

View File

@@ -1385,7 +1385,7 @@ GstPadProbeReturn GstEnginePipeline::BufferProbeCallback(GstPad *pad, GstPadProb
for (GstBufferConsumer *consumer : std::as_const(consumers)) {
gst_buffer_ref(buf);
consumer->ConsumeBuffer(buf, instance->id(), format);
consumer->ConsumeBuffer(buf, instance->id(), format, channels);
}
if (buf16) {

View File

@@ -0,0 +1,159 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QAbstractItemModel>
#include <QString>
#include <QStringList>
#include <QFileInfo>
#include <QDir>
#include <QDirIterator>
#include "core/logging.h"
#include "projectmpresetmodel.h"
#include "projectmvisualization.h"
ProjectMPresetModel::ProjectMPresetModel(ProjectMVisualization *projectm_visualization, QObject *parent)
: QAbstractItemModel(parent),
projectm_visualization_(projectm_visualization) {
// Find presets
if (QFileInfo::exists(projectm_visualization_->preset_path())) {
QDirIterator it(projectm_visualization_->preset_path(), QStringList() << QStringLiteral("*.milk") << QStringLiteral("*.prjm"), QDir::Files | QDir::NoDotAndDotDot | QDir::Readable, QDirIterator::Subdirectories);
QStringList files;
while (it.hasNext()) {
it.next();
files << it.filePath();
}
std::stable_sort(files.begin(), files.end());
for (const QString &filepath : std::as_const(files)) {
const QFileInfo fileinfo(filepath);
all_presets_ << Preset(fileinfo.filePath(), fileinfo.fileName(), false);
}
}
else {
qLog(Error) << "ProjectM preset path" << projectm_visualization_->preset_path() << "does not exist";
}
}
int ProjectMPresetModel::rowCount(const QModelIndex &idx) const {
Q_UNUSED(idx);
if (!projectm_visualization_) return 0;
return static_cast<int>(all_presets_.count());
}
int ProjectMPresetModel::columnCount(const QModelIndex &idx) const {
Q_UNUSED(idx);
return 1;
}
QModelIndex ProjectMPresetModel::index(const int row, const int column, const QModelIndex &idx) const {
Q_UNUSED(idx);
return createIndex(row, column);
}
QModelIndex ProjectMPresetModel::parent(const QModelIndex &child) const {
Q_UNUSED(child);
return QModelIndex();
}
QVariant ProjectMPresetModel::data(const QModelIndex &index, const int role) const {
switch (role) {
case Qt::DisplayRole:
return all_presets_[index.row()].name_;
case Qt::CheckStateRole:{
bool selected = all_presets_[index.row()].selected_;
return selected ? Qt::Checked : Qt::Unchecked;
}
case Role::Role_Path:
return all_presets_[index.row()].path_;
default:
return QVariant();
}
}
Qt::ItemFlags ProjectMPresetModel::flags(const QModelIndex &idx) const {
if (!idx.isValid()) return QAbstractItemModel::flags(idx);
return Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled;
}
bool ProjectMPresetModel::setData(const QModelIndex &idx, const QVariant &value, int role) {
if (role == Qt::CheckStateRole) {
all_presets_[idx.row()].selected_ = value.toBool();
projectm_visualization_->SetSelected(QStringList() << all_presets_[idx.row()].path_, value.toBool());
return true;
}
return false;
}
void ProjectMPresetModel::SetImmediatePreset(const QModelIndex &idx) {
projectm_visualization_->SetImmediatePreset(all_presets_[idx.row()].path_);
}
void ProjectMPresetModel::SelectAll() {
QStringList paths;
paths.reserve(all_presets_.count());
for (int i = 0; i < all_presets_.count(); ++i) {
paths << all_presets_[i].path_;
all_presets_[i].selected_ = true;
}
projectm_visualization_->SetSelected(paths, true);
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0));
}
void ProjectMPresetModel::SelectNone() {
projectm_visualization_->ClearSelected();
for (int i = 0; i < all_presets_.count(); ++i) {
all_presets_[i].selected_ = false;
}
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0));
}
void ProjectMPresetModel::MarkSelected(const QString &path, const bool selected) {
for (int i = 0; i < all_presets_.count(); ++i) {
if (path == all_presets_[i].path_) {
all_presets_[i].selected_ = selected;
return;
}
}
}

View File

@@ -0,0 +1,77 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef PROJECTMPRESETMODEL_H
#define PROJECTMPRESETMODEL_H
#include "config.h"
#include <QList>
#include <QString>
#include <QAbstractItemModel>
class ProjectMVisualization;
class ProjectMPresetModel : public QAbstractItemModel {
Q_OBJECT
friend class ProjectMVisualization;
public:
explicit ProjectMPresetModel(ProjectMVisualization *projectm_visualization, QObject *parent = nullptr);
enum Role {
Role_Path = Qt::UserRole,
};
void MarkSelected(const QString &path, bool selected);
// QAbstractItemModel
QModelIndex index(const int row, const int column, const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &child) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, const int role = Qt::DisplayRole) const override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
bool setData(const QModelIndex &index, const QVariant &value, const int role = Qt::EditRole) override;
public Q_SLOTS:
void SetImmediatePreset(const QModelIndex &index);
void SelectAll();
void SelectNone();
private:
struct Preset {
explicit Preset(const QString &path, const QString &name, const bool selected)
: path_(path),
name_(name),
selected_(selected) {}
QString path_;
QString name_;
bool selected_;
};
ProjectMVisualization *projectm_visualization_;
QList<Preset> all_presets_;
};
#endif // PROJECTMPRESETMODEL_H

View File

@@ -0,0 +1,469 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <string>
#include <memory>
#ifdef HAVE_PROJECTM4
# include <projectM-4/types.h>
# include <projectM-4/core.h>
# include <projectM-4/parameters.h>
# include <projectM-4/memory.h>
# include <projectM-4/audio.h>
# include <projectM-4/render_opengl.h>
# include <projectM-4/playlist_types.h>
# include <projectM-4/playlist_core.h>
# include <projectM-4/playlist_memory.h>
# include <projectM-4/playlist_items.h>
# include <projectM-4/playlist_playback.h>
#else
# include <libprojectM/projectM.hpp>
#endif // HAVE_PROJECTM4
#include <QCoreApplication>
#include <QGraphicsScene>
#include <QString>
#include <QStringList>
#include <QDir>
#include <QFileInfo>
#include <QScopeGuard>
#include <QPainter>
#include <QRandomGenerator>
#include <QMessageBox>
#include <QTimerEvent>
#include "core/logging.h"
#include "core/settings.h"
#include "projectmvisualization.h"
#include "projectmpresetmodel.h"
#include "visualizationcontainer.h"
ProjectMVisualization::ProjectMVisualization(VisualizationContainer *container)
: QGraphicsScene(container),
container_(container),
preset_model_(nullptr),
#ifdef HAVE_PROJECTM4
projectm_instance_(nullptr),
projectm_playlist_instance_(nullptr),
#endif
mode_(Mode::Random),
duration_(15),
texture_size_(512) {
QObject::connect(this, &QGraphicsScene::sceneRectChanged, this, &ProjectMVisualization::SceneRectChanged);
#ifndef HAVE_PROJECTM4
for (int i = 0; i < TOTAL_RATING_TYPES; ++i) {
default_rating_list_.push_back(3);
}
#endif // HAVE_PROJECTM4
}
ProjectMVisualization::~ProjectMVisualization() {
#ifdef HAVE_PROJECTM4
if (projectm_playlist_instance_) {
projectm_playlist_destroy(projectm_playlist_instance_);
}
if (projectm_instance_) {
projectm_destroy(projectm_instance_);
}
#endif // HAVE_PROJECTM4
}
void ProjectMVisualization::Init() {
#ifdef HAVE_PROJECTM4
if (projectm_instance_) {
return;
}
#else
if (projectm_) {
return;
}
#endif // HAVE_PROJECTM4
// Find the projectM presets
QStringList data_paths = QStringList() << QStringLiteral("/usr/share")
<< QStringLiteral("/usr/local/share")
<< QLatin1String(CMAKE_INSTALL_PREFIX) + QLatin1String("/share");
const QStringList xdg_data_dirs = QString::fromUtf8(qgetenv("XDG_DATA_DIRS")).split(QLatin1Char(':'));
for (const QString &xdg_data_dir : xdg_data_dirs) {
if (!data_paths.contains(xdg_data_dir)) {
data_paths.append(xdg_data_dir);
}
}
#if defined(Q_OS_WIN32)
data_paths.prepend(QCoreApplication::applicationDirPath());
#endif
const QStringList projectm_paths = QStringList() << QStringLiteral("projectM/presets")
<< QStringLiteral("projectm-presets");
QStringList preset_paths;
for (const QString &data_path : std::as_const(data_paths)) {
for (const QString &projectm_path : projectm_paths) {
const QString path = data_path + QLatin1Char('/') + projectm_path;
if (!QFileInfo::exists(path) || QDir(path).entryList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot).isEmpty()) {
preset_paths << path;
continue;
}
preset_path_ = path;
break;
}
}
// Create projectM settings
#ifdef HAVE_PROJECTM4
Q_ASSERT(projectm_instance_ == nullptr);
Q_ASSERT(projectm_playlist_instance_ == nullptr);
projectm_instance_ = projectm_create();
projectm_set_preset_duration(projectm_instance_, duration_);
projectm_set_mesh_size(projectm_instance_, 32, 24);
projectm_set_fps(projectm_instance_, 35);
projectm_set_window_size(projectm_instance_, 512, 512);
projectm_playlist_instance_ = projectm_playlist_create(projectm_instance_);
#else
projectM::Settings s;
s.presetURL = preset_path_.toStdString();
s.meshX = 32;
s.meshY = 24;
s.textureSize = texture_size_;
s.fps = 35;
s.windowWidth = 512;
s.windowHeight = 512;
s.smoothPresetDuration = 5;
s.presetDuration = duration_;
s.shuffleEnabled = true;
s.softCutRatingsEnabled = false;
s.easterEgg = 0;
projectm_ = std::make_unique<projectM>(s);
#endif // HAVE_PROJECTM4
Q_ASSERT(preset_model_ == nullptr);
preset_model_ = new ProjectMPresetModel(this, this);
Load();
// Start at a random preset.
#ifdef HAVE_PROJECTM4
const uint count = projectm_playlist_size(projectm_playlist_instance_);
if (count > 0) {
const uint position = QRandomGenerator::global()->bounded(count);
projectm_playlist_set_position(projectm_playlist_instance_, position, true);
}
#else
const uint count = projectm_->getPlaylistSize();
if (count > 0) {
const uint selection = QRandomGenerator::global()->bounded(count);
projectm_->selectPreset(selection, true);
}
#endif // HAVE_PROJECTM4
if (preset_path_.isEmpty()) {
qWarning("ProjectM presets could not be found, search path was:\n %s", preset_paths.join(QLatin1String("\n ")).toLocal8Bit().constData());
QMessageBox::warning(nullptr, tr("Missing projectM presets"), tr("Strawberry could not load any projectM visualizations. Check that you have installed Strawberry properly."));
}
Resize(sceneRect().width(), sceneRect().height(), container_->devicePixelRatio());
}
void ProjectMVisualization::drawBackground(QPainter *p, const QRectF &rect) {
Q_UNUSED(rect);
p->beginNativePainting();
#ifdef HAVE_PROJECTM4
projectm_opengl_render_frame(projectm_instance_);
#else
projectm_->renderFrame();
#endif
p->endNativePainting();
}
void ProjectMVisualization::SceneRectChanged(const QRectF &rect) {
Resize(rect.width(), rect.height(), container_->devicePixelRatio());
}
void ProjectMVisualization::Resize(const qreal width, const qreal height, const qreal pixel_ratio) {
#ifdef HAVE_PROJECTM4
if (projectm_instance_) {
projectm_set_window_size(projectm_instance_, static_cast<size_t>(width * pixel_ratio), static_cast<size_t>(height * pixel_ratio));
}
#else
if (projectm_) {
projectm_->projectM_resetGL(static_cast<int>(width * pixel_ratio), static_cast<int>(height * pixel_ratio));
}
#endif // HAVE_PROJECTM4
}
void ProjectMVisualization::SetTextureSize(const int size) {
texture_size_ = size;
#ifndef HAVE_PROJECTM4
if (projectm_) {
projectm_->changeTextureSize(texture_size_);
}
#endif // HAVE_PROJECTM4
}
void ProjectMVisualization::SetDuration(const int seconds) {
duration_ = seconds;
#ifdef HAVE_PROJECTM4
if (projectm_instance_) {
projectm_set_preset_duration(projectm_instance_, duration_);
}
#else
if (projectm_) {
projectm_->changePresetDuration(duration_);
}
#endif // HAVE_PROJECTM4
Save();
}
void ProjectMVisualization::ConsumeBuffer(GstBuffer *buffer, const int pipeline_id, const QString &format, const int channels) {
Q_UNUSED(pipeline_id);
Q_UNUSED(format);
GstMapInfo map;
gst_buffer_map(buffer, &map, GST_MAP_READ);
#ifdef HAVE_PROJECTM4
if (projectm_instance_) {
const unsigned int samples_per_channel = (map.size / sizeof(int16_t)) / channels;
const int16_t *data = reinterpret_cast<int16_t*>(map.data);
projectm_pcm_add_int16(projectm_instance_, data, samples_per_channel, static_cast<projectm_channels>(channels));
}
#else
if (projectm_) {
const int16_t samples_per_channel = static_cast<int16_t>((map.size / sizeof(int16_t)) / channels);
const int16_t *data = reinterpret_cast<int16_t*>(map.data);
projectm_->pcm()->addPCM16Data(data, samples_per_channel);
}
#endif // HAVE_PROJECTM4
gst_buffer_unmap(buffer, &map);
gst_buffer_unref(buffer);
}
void ProjectMVisualization::SetSelected(const QStringList &paths, const bool selected) {
for (const QString &path : paths) {
const int index = IndexOfPreset(path);
if (selected && index == -1) {
#ifdef HAVE_PROJECTM4
projectm_playlist_add_preset(projectm_playlist_instance_, path.toUtf8().constData(), true);
#else
projectm_->addPresetURL(path.toStdString(), std::string(), default_rating_list_);
#endif
}
else if (!selected && index != -1) {
#ifdef HAVE_PROJECTM4
projectm_playlist_remove_preset(projectm_playlist_instance_, index);
#else
projectm_->removePreset(index);
#endif
}
}
Save();
}
void ProjectMVisualization::ClearSelected() {
#ifdef HAVE_PROJECTM4
projectm_playlist_clear(projectm_playlist_instance_);
#else
projectm_->clearPlaylist();
#endif
Save();
}
int ProjectMVisualization::IndexOfPreset(const QString &preset_path) const {
#ifdef HAVE_PROJECTM4
const uint count = projectm_playlist_size(projectm_playlist_instance_);
for (uint i = 0; i < count; ++i) {
char *projectm_preset_path = projectm_playlist_item(projectm_playlist_instance_, i);
if (projectm_preset_path) {
const QScopeGuard projectm_preset_path_deleter = qScopeGuard([projectm_preset_path](){ projectm_playlist_free_string(projectm_preset_path); });
if (QLatin1String(projectm_preset_path) == preset_path) {
return static_cast<int>(i);
}
}
}
#else
const uint count = projectm_->getPlaylistSize();
for (uint i = 0; i < count; ++i) {
if (QString::fromStdString(projectm_->getPresetURL(i)) == preset_path) return static_cast<int>(i);
}
#endif // HAVE_PROJECTM4
return -1;
}
void ProjectMVisualization::Load() {
Settings s;
s.beginGroup(QLatin1String(VisualizationContainer::kSettingsGroup));
mode_ = Mode(s.value("mode", 0).toInt());
duration_ = s.value("duration", duration_).toInt();
s.endGroup();
#ifdef HAVE_PROJECTM4
projectm_set_preset_duration(projectm_instance_, duration_);
projectm_playlist_clear(projectm_playlist_instance_);
#else
projectm_->changePresetDuration(duration_);
projectm_->clearPlaylist();
#endif // HAVE_PROJECTM4
switch (mode_) {
case Mode::Random:{
for (int i = 0; i < preset_model_->all_presets_.count(); ++i) {
#ifdef HAVE_PROJECTM4
projectm_playlist_add_preset(projectm_playlist_instance_, preset_model_->all_presets_[i].path_.toUtf8().constData(), false);
#else
projectm_->addPresetURL(preset_model_->all_presets_[i].path_.toStdString(), std::string(), default_rating_list_);
#endif
preset_model_->all_presets_[i].selected_ = true;
}
break;
}
case Mode::FromList:{
s.beginGroup(QLatin1String(VisualizationContainer::kSettingsGroup));
const QStringList paths = s.value("preset_paths").toStringList();
s.endGroup();
for (const QString &path : paths) {
#ifdef HAVE_PROJECTM4
projectm_playlist_add_preset(projectm_playlist_instance_, path.toUtf8().constData(), true);
#else
projectm_->addPresetURL(path.toStdString(), std::string(), default_rating_list_);
#endif
preset_model_->MarkSelected(path, true);
}
}
}
}
void ProjectMVisualization::Save() {
QStringList paths;
for (const ProjectMPresetModel::Preset &preset : std::as_const(preset_model_->all_presets_)) {
if (preset.selected_) paths << preset.path_;
}
Settings s;
s.beginGroup(VisualizationContainer::kSettingsGroup);
s.setValue("preset_paths", paths);
s.setValue("mode", static_cast<int>(mode_));
s.setValue("duration", duration_);
s.endGroup();
}
void ProjectMVisualization::SetMode(const Mode mode) {
mode_ = mode;
Save();
}
QString ProjectMVisualization::preset_path() const {
#ifdef HAVE_PROJECTM4
return preset_path_;
#else
if (projectm_) {
return QString::fromStdString(projectm_->settings().presetURL);
}
return QString();
#endif // HAVE_PROJECTM4
}
void ProjectMVisualization::SetImmediatePreset(const int index) {
#ifdef HAVE_PROJECTM4
if (projectm_playlist_instance_) {
projectm_playlist_set_position(projectm_playlist_instance_, index, true);
}
#else
if (projectm_) {
projectm_->selectPreset(index, true);
}
#endif // HAVE_PROJECTM4
}
void ProjectMVisualization::SetImmediatePreset(const QString &path) {
const int index = IndexOfPreset(path);
if (index != -1) {
SetImmediatePreset(index);
}
}
void ProjectMVisualization::Lock(const bool lock) {
#ifdef HAVE_PROJECTM4
if (projectm_instance_) {
projectm_set_preset_locked(projectm_instance_, lock);
}
#else
if (projectm_) {
projectm_->setPresetLock(lock);
}
#endif // HAVE_PROJECTM4
if (!lock) Load();
}

View File

@@ -0,0 +1,114 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef PROJECTMVISUALIZATION_H
#define PROJECTMVISUALIZATION_H
#include "config.h"
#include <memory>
#include <vector>
#ifdef HAVE_PROJECTM4
# include <projectM-4/types.h>
# include <projectM-4/playlist_types.h>
#else
# include <libprojectM/projectM.hpp>
#endif
#include <QString>
#include <QStringList>
#include <QGraphicsScene>
#include "engine/gstbufferconsumer.h"
class projectM;
class QPainter;
class ProjectMPresetModel;
class VisualizationContainer;
class ProjectMVisualization : public QGraphicsScene, public GstBufferConsumer {
Q_OBJECT
public:
explicit ProjectMVisualization(VisualizationContainer *container);
~ProjectMVisualization();
enum class Mode {
Random = 0,
FromList = 1,
};
QString preset_path() const;
ProjectMPresetModel *preset_model() const { return preset_model_; }
Mode mode() const { return mode_; }
int duration() const { return duration_; }
void Init();
// BufferConsumer
void ConsumeBuffer(GstBuffer *buffer, const int pipeline_id, const QString &format, const int channels) override;
public Q_SLOTS:
void SetTextureSize(const int size);
void SetDuration(const int seconds);
void SetSelected(const QStringList &paths, const bool selected);
void ClearSelected();
void SetImmediatePreset(const int index);
void SetImmediatePreset(const QString &path);
void SetMode(const Mode mode);
void Lock(const bool lock);
protected:
// QGraphicsScene
void drawBackground(QPainter *painter, const QRectF &rect) override;
private Q_SLOTS:
void SceneRectChanged(const QRectF &rect);
private:
void Load();
void Save();
int IndexOfPreset(const QString &preset_path) const;
void Resize(const qreal width, const qreal height, const qreal pixel_ratio);
private:
VisualizationContainer *container_;
ProjectMPresetModel *preset_model_;
#ifdef HAVE_PROJECTM4
projectm_handle projectm_instance_;
projectm_playlist_handle projectm_playlist_instance_;
#else
std::unique_ptr<projectM> projectm_;
#endif
Mode mode_;
int duration_;
std::vector<int> default_rating_list_;
int texture_size_;
QString preset_path_;
};
#endif // PROJECTMVISUALIZATION_H

View File

@@ -0,0 +1,328 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QtGlobal>
#include <QOpenGLWidget>
#include <QGraphicsProxyWidget>
#include <QHBoxLayout>
#include <QLabel>
#include <QMenu>
#include <QMessageBox>
#include <QShortcut>
#include <QActionGroup>
#include "core/logging.h"
#include "core/iconloader.h"
#include "core/settings.h"
#include "engine/gstengine.h"
#include "visualizationcontainer.h"
#include "visualizationopenglwidget.h"
#include "visualizationoverlay.h"
#include "visualizationselector.h"
#include "projectmvisualization.h"
const char *VisualizationContainer::kSettingsGroup = "Visualizations";
namespace {
constexpr int kLowFramerate = 15;
constexpr int kMediumFramerate = 25;
constexpr int kHighFramerate = 35;
constexpr int kSuperHighFramerate = 60;
constexpr int kDefaultWidth = 828;
constexpr int kDefaultHeight = 512;
constexpr int kDefaultFps = kHighFramerate;
constexpr int kDefaultTextureSize = 512;
} // namespace
VisualizationContainer::VisualizationContainer(QWidget *parent)
: QGraphicsView(parent),
projectm_visualization_(new ProjectMVisualization(this)),
overlay_(new VisualizationOverlay),
selector_(new VisualizationSelector(this)),
overlay_proxy_(nullptr),
engine_(nullptr),
menu_(new QMenu(this)),
fps_(kDefaultFps),
size_(kDefaultTextureSize) {
setWindowTitle(tr("Visualizations"));
setWindowIcon(IconLoader::Load(QStringLiteral("strawberry")));
setMinimumSize(64, 64);
{
Settings s;
s.beginGroup(QLatin1String(kSettingsGroup));
if (!restoreGeometry(s.value("geometry").toByteArray())) {
resize(kDefaultWidth, kDefaultHeight);
}
fps_ = s.value("fps", kDefaultFps).toInt();
size_ = s.value("size", kDefaultTextureSize).toInt();
s.endGroup();
}
QShortcut *close = new QShortcut(QKeySequence::Close, this);
QObject::connect(close, &QShortcut::activated, this, &VisualizationContainer::close);
// Set up the graphics view
setScene(projectm_visualization_);
setViewport(new VisualizationOpenGLWidget(projectm_visualization_));
setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setFrameStyle(QFrame::NoFrame);
// Add the overlay
overlay_proxy_ = scene()->addWidget(overlay_);
QObject::connect(overlay_, &VisualizationOverlay::OpacityChanged, this, &VisualizationContainer::ChangeOverlayOpacity);
QObject::connect(overlay_, &VisualizationOverlay::ShowPopupMenu, this, &VisualizationContainer::ShowPopupMenu);
ChangeOverlayOpacity(0.0);
projectm_visualization_->SetTextureSize(size_);
SizeChanged();
// Selector
selector_->SetVisualization(projectm_visualization_);
// Settings menu
menu_->addAction(IconLoader::Load(QStringLiteral("view-fullscreen")), tr("Toggle fullscreen"), this, &VisualizationContainer::ToggleFullscreen);
QMenu *fps_menu = menu_->addMenu(tr("Framerate"));
QActionGroup *fps_group = new QActionGroup(this);
AddFramerateMenuItem(tr("Low (%1 fps)").arg(kLowFramerate), kLowFramerate, fps_, fps_group);
AddFramerateMenuItem(tr("Medium (%1 fps)").arg(kMediumFramerate), kMediumFramerate, fps_, fps_group);
AddFramerateMenuItem(tr("High (%1 fps)").arg(kHighFramerate), kHighFramerate, fps_, fps_group);
AddFramerateMenuItem(tr("Super high (%1 fps)").arg(kSuperHighFramerate), kSuperHighFramerate, fps_, fps_group);
fps_menu->addActions(fps_group->actions());
QMenu *quality_menu = menu_->addMenu(tr("Quality", "Visualization quality"));
QActionGroup *quality_group = new QActionGroup(this);
AddQualityMenuItem(tr("Low (256x256)"), 256, size_, quality_group);
AddQualityMenuItem(tr("Medium (512x512)"), 512, size_, quality_group);
AddQualityMenuItem(tr("High (1024x1024)"), 1024, size_, quality_group);
AddQualityMenuItem(tr("Super high (2048x2048)"), 2048, size_, quality_group);
quality_menu->addActions(quality_group->actions());
menu_->addAction(tr("Select visualizations..."), selector_, &VisualizationContainer::show);
menu_->addSeparator();
menu_->addAction(IconLoader::Load(QStringLiteral("application-exit")), tr("Close visualization"), this, &VisualizationContainer::hide);
}
void VisualizationContainer::AddFramerateMenuItem(const QString &name, const int value, const int def, QActionGroup *group) {
QAction *action = group->addAction(name);
action->setCheckable(true);
action->setChecked(value == def);
QObject::connect(action, &QAction::triggered, this, [this, value]() { SetFps(value); });
}
void VisualizationContainer::AddQualityMenuItem(const QString &name, const int value, const int def, QActionGroup *group) {
QAction *action = group->addAction(name);
action->setCheckable(true);
action->setChecked(value == def);
QObject::connect(action, &QAction::triggered, this, [this, value]() { SetQuality(value); });
}
void VisualizationContainer::SetEngine(GstEngine *engine) {
engine_ = engine;
if (isVisible()) engine_->AddBufferConsumer(projectm_visualization_);
}
void VisualizationContainer::showEvent(QShowEvent *e) {
qLog(Debug) << "Showing visualization";
QGraphicsView::showEvent(e);
update_timer_.start(1000 / fps_, this);
if (engine_) engine_->AddBufferConsumer(projectm_visualization_);
}
void VisualizationContainer::hideEvent(QHideEvent *e) {
qLog(Debug) << "Hiding visualization";
QGraphicsView::hideEvent(e);
update_timer_.stop();
if (engine_) engine_->RemoveBufferConsumer(projectm_visualization_);
}
void VisualizationContainer::closeEvent(QCloseEvent *e) {
Q_UNUSED(e);
// Don't close the window. Just hide it.
e->ignore();
hide();
}
void VisualizationContainer::resizeEvent(QResizeEvent *e) {
QGraphicsView::resizeEvent(e);
SizeChanged();
}
void VisualizationContainer::SizeChanged() {
// Save the geometry
Settings s;
s.beginGroup(kSettingsGroup);
s.setValue("geometry", saveGeometry());
// Resize the scene
if (scene()) scene()->setSceneRect(QRect(QPoint(0, 0), size()));
// Resize the overlay
if (overlay_) overlay_->resize(size());
}
void VisualizationContainer::timerEvent(QTimerEvent *e) {
QGraphicsView::timerEvent(e);
if (e->timerId() == update_timer_.timerId()) scene()->update();
}
void VisualizationContainer::SetActions(QAction *previous, QAction *play_pause, QAction *stop, QAction *next) {
overlay_->SetActions(previous, play_pause, stop, next);
}
void VisualizationContainer::SongMetadataChanged(const Song &metadata) {
overlay_->SetSongTitle(QStringLiteral("%1 - %2").arg(metadata.artist(), metadata.title()));
}
void VisualizationContainer::Stopped() {
overlay_->SetSongTitle(tr("strawberry"));
}
void VisualizationContainer::ChangeOverlayOpacity(const qreal value) {
overlay_proxy_->setOpacity(value);
// Hide the cursor if the overlay is hidden
if (value < 0.5) {
viewport()->setCursor(Qt::BlankCursor);
}
else {
viewport()->unsetCursor();
}
}
void VisualizationContainer::enterEvent(QEnterEvent *e) {
QGraphicsView::enterEvent(e);
overlay_->SetVisible(true);
}
void VisualizationContainer::leaveEvent(QEvent *e) {
QGraphicsView::leaveEvent(e);
overlay_->SetVisible(false);
}
void VisualizationContainer::mouseMoveEvent(QMouseEvent *e) {
QGraphicsView::mouseMoveEvent(e);
overlay_->SetVisible(true);
}
void VisualizationContainer::mouseDoubleClickEvent(QMouseEvent *e) {
QGraphicsView::mouseDoubleClickEvent(e);
ToggleFullscreen();
}
void VisualizationContainer::contextMenuEvent(QContextMenuEvent *event) {
QGraphicsView::contextMenuEvent(event);
ShowPopupMenu(event->pos());
}
void VisualizationContainer::keyReleaseEvent(QKeyEvent *event) {
if (event->matches(QKeySequence::Close) || event->key() == Qt::Key_Escape) {
if (isFullScreen()) {
ToggleFullscreen();
}
else {
hide();
}
return;
}
QGraphicsView::keyReleaseEvent(event);
}
void VisualizationContainer::ToggleFullscreen() {
setWindowState(windowState() ^ Qt::WindowFullScreen);
}
void VisualizationContainer::SetFps(const int fps) {
fps_ = fps;
// Save settings
Settings s;
s.beginGroup(kSettingsGroup);
s.setValue("fps", fps_);
update_timer_.stop();
update_timer_.start(1000 / fps_, this);
}
void VisualizationContainer::ShowPopupMenu(const QPoint &pos) {
menu_->popup(mapToGlobal(pos));
}
void VisualizationContainer::SetQuality(const int size) {
size_ = size;
// Save settings
Settings s;
s.beginGroup(kSettingsGroup);
s.setValue("size", size_);
projectm_visualization_->SetTextureSize(size_);
}

View File

@@ -0,0 +1,103 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef VISUALIZATIONCONTAINER_H
#define VISUALIZATIONCONTAINER_H
#include "config.h"
#include <QBasicTimer>
#include <QGraphicsView>
#include "core/song.h"
class GstEngine;
class ProjectMVisualization;
class VisualizationOverlay;
class VisualizationSelector;
class QMenu;
class QActionGroup;
class QEvent;
class QShowEvent;
class QHideEvent;
class QCloseEvent;
class QResizeEvent;
class QTimerEvent;
class QMouseEvent;
class QContextMenuEvent;
class QKeyEvent;
class QEnterEvent;
class VisualizationContainer : public QGraphicsView {
Q_OBJECT
public:
explicit VisualizationContainer(QWidget *parent = nullptr);
static const char *kSettingsGroup;
void SetEngine(GstEngine *engine);
void SetActions(QAction *previous, QAction *play_pause, QAction *stop, QAction *next);
public Q_SLOTS:
void SongMetadataChanged(const Song &metadata);
void Stopped();
protected:
// QWidget
void showEvent(QShowEvent *e) override;
void hideEvent(QHideEvent *e) override;
void closeEvent(QCloseEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void timerEvent(QTimerEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void enterEvent(QEnterEvent *e) override;
void leaveEvent(QEvent *e) override;
void mouseDoubleClickEvent(QMouseEvent *e) override;
void contextMenuEvent(QContextMenuEvent *event) override;
void keyReleaseEvent(QKeyEvent *event) override;
private:
void SizeChanged();
void AddFramerateMenuItem(const QString &name, int value, int def, QActionGroup *group);
void AddQualityMenuItem(const QString &name, int value, int def, QActionGroup *group);
private Q_SLOTS:
void ChangeOverlayOpacity(qreal value);
void ShowPopupMenu(const QPoint &pos);
void ToggleFullscreen();
void SetFps(const int fps);
void SetQuality(const int size);
private:
ProjectMVisualization *projectm_visualization_;
VisualizationOverlay *overlay_;
VisualizationSelector *selector_;
QGraphicsProxyWidget *overlay_proxy_;
GstEngine *engine_;
QMenu *menu_;
QBasicTimer update_timer_;
int fps_;
int size_;
};
#endif // VISUALIZATIONCONTAINER_H

View File

@@ -0,0 +1,39 @@
/*
* Strawberry Music Player
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QtGlobal>
#include "visualizationopenglwidget.h"
#include "projectmvisualization.h"
VisualizationOpenGLWidget::VisualizationOpenGLWidget(ProjectMVisualization *projectm_visualization, QWidget *parent, Qt::WindowFlags f)
: QOpenGLWidget(parent, f),
projectm_visualization_(projectm_visualization) {}
void VisualizationOpenGLWidget::initializeGL() {
projectm_visualization_->Init();
QOpenGLWidget::initializeGL();
glEnable(GL_BLEND);
}

View File

@@ -0,0 +1,42 @@
/*
* Strawberry Music Player
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef VISUALIZATIONOPENGLWIDGET_H
#define VISUALIZATIONOPENGLWIDGET_H
#include "config.h"
#include <QOpenGLWidget>
class ProjectMVisualization;
class VisualizationOpenGLWidget : public QOpenGLWidget {
Q_OBJECT
public:
explicit VisualizationOpenGLWidget(ProjectMVisualization *projectm_visualization, QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags());
protected:
void initializeGL() override;
private:
ProjectMVisualization *projectm_visualization_;
};
#endif // VISUALIZATIONOPENGLWIDGET_H

View File

@@ -0,0 +1,116 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QString>
#include <QBasicTimer>
#include <QTimeLine>
#include <QGraphicsProxyWidget>
#include <QTimerEvent>
#include <QMouseEvent>
#include "core/iconloader.h"
#include "visualizationoverlay.h"
#include "ui_visualizationoverlay.h"
namespace {
constexpr int kFadeDuration = 500;
constexpr int kFadeTimeout = 5000;
}
VisualizationOverlay::VisualizationOverlay(QWidget *parent)
: QWidget(parent),
ui_(new Ui_VisualizationOverlay),
fade_timeline_(new QTimeLine(kFadeDuration, this)),
visible_(false) {
ui_->setupUi(this);
setAttribute(Qt::WA_TranslucentBackground);
setMouseTracking(true);
ui_->settings->setIcon(IconLoader::Load(QStringLiteral("configure")));
QObject::connect(ui_->settings, &QToolButton::clicked, this, &VisualizationOverlay::ShowSettingsMenu);
QObject::connect(fade_timeline_, &QTimeLine::valueChanged, this, &VisualizationOverlay::OpacityChanged);
}
VisualizationOverlay::~VisualizationOverlay() { delete ui_; }
QGraphicsProxyWidget *VisualizationOverlay::title(QGraphicsProxyWidget *proxy) const {
return proxy->createProxyForChildWidget(ui_->song_title);
}
void VisualizationOverlay::SetActions(QAction *previous, QAction *play_pause, QAction *stop, QAction *next) {
ui_->previous->setDefaultAction(previous);
ui_->play_pause->setDefaultAction(play_pause);
ui_->stop->setDefaultAction(stop);
ui_->next->setDefaultAction(next);
}
void VisualizationOverlay::ShowSettingsMenu() {
Q_EMIT ShowPopupMenu(ui_->settings->mapToGlobal(ui_->settings->rect().bottomLeft()));
}
void VisualizationOverlay::timerEvent(QTimerEvent *e) {
QWidget::timerEvent(e);
if (e->timerId() == fade_out_timeout_.timerId()) {
SetVisible(false);
}
}
void VisualizationOverlay::SetVisible(const bool visible) {
// If we're showing the overlay, then fade out again in a little while
fade_out_timeout_.stop();
if (visible) fade_out_timeout_.start(kFadeTimeout, this);
// Don't change to the state we're in already
if (visible == visible_) return;
visible_ = visible;
// If there's already another fader running then start from the same time that one was already at.
int start_time = visible ? 0 : fade_timeline_->duration();
if (fade_timeline_->state() == QTimeLine::Running)
start_time = fade_timeline_->currentTime();
fade_timeline_->stop();
fade_timeline_->setDirection(visible ? QTimeLine::Forward : QTimeLine::Backward);
fade_timeline_->setCurrentTime(start_time);
fade_timeline_->resume();
}
void VisualizationOverlay::SetSongTitle(const QString &title) {
ui_->song_title->setText(title);
SetVisible(true);
}

View File

@@ -0,0 +1,71 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef VISUALIZATIONOVERLAY_H
#define VISUALIZATIONOVERLAY_H
#include "config.h"
#include <QWidget>
#include <QString>
#include <QBasicTimer>
class Ui_VisualizationOverlay;
class QGraphicsProxyWidget;
class QTimeLine;
class QAction;
class VisualizationOverlay : public QWidget {
Q_OBJECT
public:
explicit VisualizationOverlay(QWidget *parent = nullptr);
~VisualizationOverlay();
QGraphicsProxyWidget *title(QGraphicsProxyWidget *proxy) const;
void SetActions(QAction *previous, QAction *play_pause, QAction *stop, QAction *next);
void SetSongTitle(const QString &title);
public Q_SLOTS:
void SetVisible(const bool visible);
Q_SIGNALS:
void OpacityChanged(const qreal value);
void ShowPopupMenu(const QPoint &pos);
protected:
// QWidget
void timerEvent(QTimerEvent *e);
private Q_SLOTS:
void ShowSettingsMenu();
private:
Ui_VisualizationOverlay *ui_;
QTimeLine *fade_timeline_;
QBasicTimer fade_out_timeout_;
bool visible_;
};
#endif // VISUALIZATIONOVERLAY_H

View File

@@ -0,0 +1,234 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>VisualizationOverlay</class>
<widget class="QWidget" name="VisualizationOverlay">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>523</width>
<height>302</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<property name="styleSheet">
<string notr="true">VisualizationOverlay {
background-color: transparent;
}
#frame {
background-color: rgba(96, 59, 25, 70%);
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-color: rgba(145, 89, 38, 100%);
border-width: 4px 4px 0px 4px;
border-style: solid;
}
#song_title {
font-weight: bold;
font-size: 20px;
color: #feae65;
}
QToolButton {
background: transparent;
border: none;
}</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="margin">
<number>0</number>
</property>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>210</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="song_title">
<property name="text">
<string>Strawberry</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Preferred</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QWidget" name="widget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QToolButton" name="previous">
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="play_pause">
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="stop">
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="next">
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>13</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="settings">
<property name="toolTip">
<string>Visualizations Settings</string>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Preferred</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,88 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QPushButton>
#include "visualizationselector.h"
#include "projectmpresetmodel.h"
#include "projectmvisualization.h"
#include "ui_visualizationselector.h"
VisualizationSelector::VisualizationSelector(QWidget *parent)
: QDialog(parent),
ui_(new Ui_VisualizationSelector),
projectm_visualization_(nullptr),
select_all_(nullptr),
select_none_(nullptr) {
ui_->setupUi(this);
select_all_ = ui_->buttonBox->addButton(tr("Select All"), QDialogButtonBox::ActionRole);
select_none_ = ui_->buttonBox->addButton(tr("Select None"), QDialogButtonBox::ActionRole);
QObject::connect(select_all_, &QPushButton::clicked, this, &VisualizationSelector::SelectAll);
QObject::connect(select_none_, &QPushButton::clicked, this, &VisualizationSelector::SelectNone);
select_all_->setEnabled(false);
select_none_->setEnabled(false);
QObject::connect(ui_->mode, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &VisualizationSelector::ModeChanged);
}
VisualizationSelector::~VisualizationSelector() { delete ui_; }
void VisualizationSelector::showEvent(QShowEvent *e) {
Q_UNUSED(e);
if (!ui_->list->model()) {
ui_->delay->setValue(projectm_visualization_->duration());
ui_->list->setModel(projectm_visualization_->preset_model());
QObject::connect(ui_->list->selectionModel(), &QItemSelectionModel::currentChanged, projectm_visualization_->preset_model(), &ProjectMPresetModel::SetImmediatePreset);
QObject::connect(ui_->delay, QOverload<int>::of(&QSpinBox::valueChanged), projectm_visualization_, &ProjectMVisualization::SetDuration);
ui_->mode->setCurrentIndex(static_cast<int>(projectm_visualization_->mode()));
}
projectm_visualization_->Lock(true);
}
void VisualizationSelector::hideEvent(QHideEvent *e) {
Q_UNUSED(e);
projectm_visualization_->Lock(false);
}
void VisualizationSelector::ModeChanged(const int mode) {
const bool enabled = mode == 1;
ui_->list->setEnabled(enabled);
select_all_->setEnabled(enabled);
select_none_->setEnabled(enabled);
projectm_visualization_->SetMode(static_cast<ProjectMVisualization::Mode>(mode));
}
void VisualizationSelector::SelectAll() { projectm_visualization_->preset_model()->SelectAll(); }
void VisualizationSelector::SelectNone() { projectm_visualization_->preset_model()->SelectNone(); }

View File

@@ -0,0 +1,61 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef VISUALIZATIONSELECTOR_H
#define VISUALIZATIONSELECTOR_H
#include "config.h"
#include <QDialog>
class QPushButton;
class QShowEvent;
class QHideEvent;
class ProjectMVisualization;
class Ui_VisualizationSelector;
class VisualizationSelector : public QDialog {
Q_OBJECT
public:
explicit VisualizationSelector(QWidget *parent = nullptr);
~VisualizationSelector();
void SetVisualization(ProjectMVisualization *projectm_visualization) { projectm_visualization_ = projectm_visualization; }
protected:
void showEvent(QShowEvent *e) override;
void hideEvent(QHideEvent *e) override;
private Q_SLOTS:
void ModeChanged(const int mode);
void SelectAll();
void SelectNone();
private:
Ui_VisualizationSelector *ui_;
ProjectMVisualization *projectm_visualization_;
QPushButton *select_all_;
QPushButton *select_none_;
};
#endif // VISUALIZATIONSELECTOR_H

View File

@@ -0,0 +1,140 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>VisualizationSelector</class>
<widget class="QDialog" name="VisualizationSelector">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>595</width>
<height>475</height>
</rect>
</property>
<property name="windowTitle">
<string>Select visualizations</string>
</property>
<property name="windowIcon">
<iconset resource="../../data/data.qrc">
<normaloff>:/icon.png</normaloff>:/icon.png</iconset>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Visualization mode</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="mode">
<item>
<property name="text">
<string>Random visualization</string>
</property>
</item>
<item>
<property name="text">
<string>Choose from the list</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Delay between visualizations</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="delay">
<property name="suffix">
<string> seconds</string>
</property>
<property name="minimum">
<number>2</number>
</property>
<property name="maximum">
<number>120</number>
</property>
<property name="value">
<number>15</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QListView" name="list">
<property name="enabled">
<bool>false</bool>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="uniformItemSizes">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>mode</tabstop>
<tabstop>delay</tabstop>
<tabstop>list</tabstop>
<tabstop>buttonBox</tabstop>
</tabstops>
<resources>
<include location="../../data/data.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>VisualizationSelector</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>VisualizationSelector</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>