Compare commits
1 Commits
artistbio
...
discord_rp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53d5d06222 |
19
.github/workflows/build.yml
vendored
19
.github/workflows/build.yml
vendored
@@ -81,7 +81,7 @@ jobs:
|
||||
gtest
|
||||
gmock
|
||||
sparsehash-devel
|
||||
rapidjson-devel
|
||||
discord-rpc-devel
|
||||
- name: Install kdsingleapplication-qt6-devel
|
||||
if: matrix.opensuse_version != 'leap:15.6'
|
||||
run: zypper -n --gpg-auto-import-keys in kdsingleapplication-qt6-devel
|
||||
@@ -207,7 +207,7 @@ jobs:
|
||||
gtest-devel
|
||||
gmock-devel
|
||||
sparsehash-devel
|
||||
rapidjson-devel
|
||||
discord-rpc-devel
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
@@ -303,7 +303,7 @@ jobs:
|
||||
appstream
|
||||
appstream-util
|
||||
hicolor-icon-theme
|
||||
rapidjson
|
||||
discord-rpc-devel
|
||||
- name: Remove files
|
||||
run: rm -rf /usr/lib64/qt6/lib/cmake/Qt6Sql/{Qt6QMYSQL*,Qt6QODBCD*,Qt6QPSQL*,Qt6QIBase*}
|
||||
- name: Checkout
|
||||
@@ -399,7 +399,7 @@ jobs:
|
||||
appstream-util
|
||||
hicolor-icon-theme
|
||||
gtest
|
||||
rapidjson
|
||||
discord-rpc-devel
|
||||
- name: Build and install KDSingleApplication
|
||||
if: matrix.mageia_version == '9'
|
||||
run: |
|
||||
@@ -520,7 +520,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
|
||||
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_WERROR=ON -DENABLE_DISCORD_RPC=OFF
|
||||
- name: Delete build directory
|
||||
run: rm -rf build
|
||||
- name: make deb
|
||||
@@ -616,7 +616,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
|
||||
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_WERROR=ON -DENABLE_DISCORD_RPC=OFF
|
||||
- name: Delete build directory
|
||||
run: rm -rf build
|
||||
- name: make deb
|
||||
@@ -692,7 +692,6 @@ jobs:
|
||||
gstreamer1.0-alsa
|
||||
gstreamer1.0-pulseaudio
|
||||
libkdsingleapplication-qt6-dev
|
||||
rapidjson-dev
|
||||
- name: Install keyboxd
|
||||
if: matrix.ubuntu_version == 'noble'
|
||||
env:
|
||||
@@ -712,7 +711,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
|
||||
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr -DBUILD_WERROR=ON -DENABLE_DISCORD_RPC=OFF
|
||||
- name: Delete build directory
|
||||
run: rm -rf build
|
||||
- name: Import Ubuntu PPA GPG private key
|
||||
@@ -756,7 +755,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"
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE="Debug" -DENABLE_DISCORD_RPC=OFF
|
||||
cmake --build build --config Debug --parallel 4
|
||||
|
||||
|
||||
@@ -776,7 +775,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 rapidjson
|
||||
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
|
||||
run: |
|
||||
set -e
|
||||
export LDFLAGS="-L/usr/local/lib"
|
||||
|
||||
41
3rdparty/discord-rpc/CMakeLists.txt
vendored
41
3rdparty/discord-rpc/CMakeLists.txt
vendored
@@ -1,41 +0,0 @@
|
||||
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
19
3rdparty/discord-rpc/LICENSE
vendored
@@ -1,19 +0,0 @@
|
||||
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
162
3rdparty/discord-rpc/README.md
vendored
@@ -1,162 +0,0 @@
|
||||
# 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 | [](https://travis-ci.org/discordapp/discord-rpc) |
|
||||
| AppVeyor | [](https://ci.appveyor.com/project/crmarsh/discord-rpc) |
|
||||
| Buildkite (internal) | [](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
63
3rdparty/discord-rpc/discord_backoff.h
vendored
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
48
3rdparty/discord-rpc/discord_connection.h
vendored
48
3rdparty/discord-rpc/discord_connection.h
vendored
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
160
3rdparty/discord-rpc/discord_connection_unix.cpp
vendored
160
3rdparty/discord-rpc/discord_connection_unix.cpp
vendored
@@ -1,160 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
160
3rdparty/discord-rpc/discord_connection_win.cpp
vendored
160
3rdparty/discord-rpc/discord_connection_win.cpp
vendored
@@ -1,160 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
64
3rdparty/discord-rpc/discord_msg_queue.h
vendored
64
3rdparty/discord-rpc/discord_msg_queue.h
vendored
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* 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
37
3rdparty/discord-rpc/discord_register.h
vendored
@@ -1,37 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
120
3rdparty/discord-rpc/discord_register_linux.cpp
vendored
120
3rdparty/discord-rpc/discord_register_linux.cpp
vendored
@@ -1,120 +0,0 @@
|
||||
/*
|
||||
* 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");
|
||||
}
|
||||
|
||||
}
|
||||
99
3rdparty/discord-rpc/discord_register_osx.m
vendored
99
3rdparty/discord-rpc/discord_register_osx.m
vendored
@@ -1,99 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
165
3rdparty/discord-rpc/discord_register_win.cpp
vendored
165
3rdparty/discord-rpc/discord_register_win.cpp
vendored
@@ -1,165 +0,0 @@
|
||||
/*
|
||||
* 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
510
3rdparty/discord-rpc/discord_rpc.cpp
vendored
@@ -1,510 +0,0 @@
|
||||
/*
|
||||
* 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
94
3rdparty/discord-rpc/discord_rpc.h
vendored
@@ -1,94 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
168
3rdparty/discord-rpc/discord_rpc_connection.cpp
vendored
168
3rdparty/discord-rpc/discord_rpc_connection.cpp
vendored
@@ -1,168 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
88
3rdparty/discord-rpc/discord_rpc_connection.h
vendored
88
3rdparty/discord-rpc/discord_rpc_connection.h
vendored
@@ -1,88 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
285
3rdparty/discord-rpc/discord_serialization.cpp
vendored
285
3rdparty/discord-rpc/discord_serialization.cpp
vendored
@@ -1,285 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
213
3rdparty/discord-rpc/discord_serialization.h
vendored
213
3rdparty/discord-rpc/discord_serialization.h
vendored
@@ -1,213 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
@@ -212,7 +212,18 @@ find_package(GTest)
|
||||
|
||||
pkg_check_modules(LIBSPARSEHASH IMPORTED_TARGET libsparsehash)
|
||||
|
||||
find_package(RapidJSON)
|
||||
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()
|
||||
|
||||
set(QT_VERSION_MAJOR 6)
|
||||
set(QT_MIN_VERSION 6.4.0)
|
||||
@@ -378,7 +389,7 @@ optional_component(STREAMTAGREADER ON "Stream tagreader"
|
||||
)
|
||||
|
||||
optional_component(DISCORD_RPC ON "Discord Rich Presence"
|
||||
DEPENDS "RapidJSON" RapidJSON_FOUND
|
||||
DEPENDS "discord-rpc" DISCORD_RPC_FOUND
|
||||
)
|
||||
|
||||
if(HAVE_SONGFINGERPRINTING OR HAVE_MUSICBRAINZ)
|
||||
@@ -834,12 +845,6 @@ set(SOURCES
|
||||
src/device/devicestatefiltermodel.cpp
|
||||
src/device/deviceviewcontainer.cpp
|
||||
src/device/deviceview.cpp
|
||||
|
||||
src/artistbio/artistbioview.cpp
|
||||
src/artistbio/artistbiofetcher.cpp
|
||||
src/artistbio/artistbioprovider.cpp
|
||||
src/artistbio/lastfmartistbio.cpp
|
||||
src/artistbio/wikipediaartistbio.cpp
|
||||
)
|
||||
|
||||
set(HEADERS
|
||||
@@ -1128,12 +1133,6 @@ set(HEADERS
|
||||
src/device/devicestatefiltermodel.h
|
||||
src/device/deviceviewcontainer.h
|
||||
src/device/deviceview.h
|
||||
|
||||
src/artistbio/artistbiofetcher.h
|
||||
src/artistbio/artistbioprovider.h
|
||||
src/artistbio/artistbioview.h
|
||||
src/artistbio/lastfmartistbio.h
|
||||
src/artistbio/wikipediaartistbio.h
|
||||
)
|
||||
|
||||
set(UI
|
||||
@@ -1515,11 +1514,6 @@ 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)
|
||||
@@ -1541,6 +1535,10 @@ 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}>
|
||||
@@ -1580,7 +1578,7 @@ target_link_libraries(strawberry_lib PUBLIC
|
||||
$<$<BOOL:${WIN32}>:dsound dwmapi ${GETOPT_LIBRARIES}>
|
||||
$<$<BOOL:${MSVC}>:WindowsApp>
|
||||
KDAB::kdsingleapplication
|
||||
$<$<BOOL:${HAVE_DISCORD_RPC}>:discord-rpc>
|
||||
$<$<BOOL:${HAVE_DISCORD_RPC}>:${DISCORD_RPC_LIBRARIES}>
|
||||
)
|
||||
|
||||
if(APPLE)
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
<file>schema/schema-21.sql</file>
|
||||
<file>schema/device-schema.sql</file>
|
||||
<file>style/strawberry.css</file>
|
||||
<file>style/artistbio.css</file>
|
||||
<file>style/smartplaylistsearchterm.css</file>
|
||||
<file>html/oauthsuccess.html</file>
|
||||
<file>pictures/strawberry.png</file>
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
<file>icons/128x128/somafm.png</file>
|
||||
<file>icons/128x128/radioparadise.png</file>
|
||||
<file>icons/128x128/musicbrainz.png</file>
|
||||
<file>icons/128x128/guitar.png</file>
|
||||
<file>icons/64x64/albums.png</file>
|
||||
<file>icons/64x64/alsa.png</file>
|
||||
<file>icons/64x64/application-exit.png</file>
|
||||
@@ -198,7 +197,6 @@
|
||||
<file>icons/64x64/somafm.png</file>
|
||||
<file>icons/64x64/radioparadise.png</file>
|
||||
<file>icons/64x64/musicbrainz.png</file>
|
||||
<file>icons/64x64/guitar.png</file>
|
||||
<file>icons/48x48/albums.png</file>
|
||||
<file>icons/48x48/alsa.png</file>
|
||||
<file>icons/48x48/application-exit.png</file>
|
||||
@@ -302,7 +300,6 @@
|
||||
<file>icons/48x48/somafm.png</file>
|
||||
<file>icons/48x48/radioparadise.png</file>
|
||||
<file>icons/48x48/musicbrainz.png</file>
|
||||
<file>icons/48x48/guitar.png</file>
|
||||
<file>icons/32x32/albums.png</file>
|
||||
<file>icons/32x32/alsa.png</file>
|
||||
<file>icons/32x32/application-exit.png</file>
|
||||
@@ -406,7 +403,6 @@
|
||||
<file>icons/32x32/somafm.png</file>
|
||||
<file>icons/32x32/radioparadise.png</file>
|
||||
<file>icons/32x32/musicbrainz.png</file>
|
||||
<file>icons/32x32/guitar.png</file>
|
||||
<file>icons/22x22/albums.png</file>
|
||||
<file>icons/22x22/alsa.png</file>
|
||||
<file>icons/22x22/application-exit.png</file>
|
||||
@@ -510,6 +506,5 @@
|
||||
<file>icons/22x22/somafm.png</file>
|
||||
<file>icons/22x22/radioparadise.png</file>
|
||||
<file>icons/22x22/musicbrainz.png</file>
|
||||
<file>icons/22x22/guitar.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 125 KiB |
@@ -1,7 +0,0 @@
|
||||
QScrollArea {
|
||||
background: qpalette(base);
|
||||
}
|
||||
|
||||
QTextEdit {
|
||||
border: 0px;
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* 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 <QObject>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
|
||||
#include "artistbiofetcher.h"
|
||||
#include "artistbioprovider.h"
|
||||
#include "core/logging.h"
|
||||
|
||||
ArtistBioFetcher::ArtistBioFetcher(QObject *parent)
|
||||
: QObject(parent),
|
||||
timeout_duration_(kDefaultTimeoutDuration),
|
||||
next_id_(1) {}
|
||||
|
||||
void ArtistBioFetcher::AddProvider(ArtistBioProvider *provider) {
|
||||
|
||||
providers_ << provider;
|
||||
connect(provider, SIGNAL(ImageReady(int, QUrl)), SLOT(ImageReady(int, QUrl)), Qt::QueuedConnection);
|
||||
connect(provider, SIGNAL(InfoReady(int, CollapsibleInfoPane::Data)), SLOT(InfoReady(int, CollapsibleInfoPane::Data)), Qt::QueuedConnection);
|
||||
connect(provider, SIGNAL(Finished(int)), SLOT(ProviderFinished(int)), Qt::QueuedConnection);
|
||||
|
||||
}
|
||||
|
||||
ArtistBioFetcher::~ArtistBioFetcher() {
|
||||
|
||||
while (!providers_.isEmpty()) {
|
||||
ArtistBioProvider *provider = providers_.takeFirst();
|
||||
provider->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int ArtistBioFetcher::FetchInfo(const Song &metadata) {
|
||||
|
||||
const int id = next_id_++;
|
||||
results_[id] = Result();
|
||||
timeout_timers_[id] = new QTimer(this);
|
||||
timeout_timers_[id]->setSingleShot(true);
|
||||
timeout_timers_[id]->setInterval(timeout_duration_);
|
||||
timeout_timers_[id]->start();
|
||||
|
||||
connect(timeout_timers_[id], &QTimer::timeout, [this, id]() { Timeout(id); });
|
||||
|
||||
for (ArtistBioProvider *provider : providers_) {
|
||||
if (provider->is_enabled()) {
|
||||
waiting_for_[id].append(provider);
|
||||
provider->Start(id, metadata);
|
||||
}
|
||||
}
|
||||
return id;
|
||||
|
||||
}
|
||||
|
||||
void ArtistBioFetcher::ImageReady(const int id, const QUrl &url) {
|
||||
|
||||
if (!results_.contains(id)) return;
|
||||
results_[id].images_ << url;
|
||||
|
||||
}
|
||||
|
||||
void ArtistBioFetcher::InfoReady(const int id, const CollapsibleInfoPane::Data &data) {
|
||||
|
||||
if (!results_.contains(id)) return;
|
||||
results_[id].info_ << data;
|
||||
|
||||
if (!waiting_for_.contains(id)) return;
|
||||
Q_EMIT InfoResultReady(id, data);
|
||||
|
||||
}
|
||||
|
||||
void ArtistBioFetcher::ProviderFinished(const int id) {
|
||||
|
||||
if (!results_.contains(id)) return;
|
||||
if (!waiting_for_.contains(id)) return;
|
||||
|
||||
ArtistBioProvider *provider = qobject_cast<ArtistBioProvider*>(sender());
|
||||
if (!waiting_for_[id].contains(provider)) return;
|
||||
|
||||
waiting_for_[id].removeAll(provider);
|
||||
if (waiting_for_[id].isEmpty()) {
|
||||
Result result = results_.take(id);
|
||||
Q_EMIT ResultReady(id, result);
|
||||
waiting_for_.remove(id);
|
||||
delete timeout_timers_.take(id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void ArtistBioFetcher::Timeout(const int id) {
|
||||
|
||||
if (!results_.contains(id)) return;
|
||||
if (!waiting_for_.contains(id)) return;
|
||||
|
||||
// Emit the results that we have already
|
||||
Q_EMIT ResultReady(id, results_.take(id));
|
||||
|
||||
// Cancel any providers that we're still waiting for
|
||||
for (ArtistBioProvider *provider : waiting_for_[id]) {
|
||||
qLog(Info) << "Request timed out from info provider" << provider->name();
|
||||
provider->Cancel(id);
|
||||
}
|
||||
waiting_for_.remove(id);
|
||||
|
||||
// Remove the timer
|
||||
delete timeout_timers_.take(id);
|
||||
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* 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 ARTISTBIOFETCHER_H
|
||||
#define ARTISTBIOFETCHER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QUrl>
|
||||
|
||||
#include "widgets/collapsibleinfopane.h"
|
||||
#include "core/song.h"
|
||||
|
||||
class QTimer;
|
||||
class ArtistBioProvider;
|
||||
|
||||
class ArtistBioFetcher : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ArtistBioFetcher(QObject *parent = nullptr);
|
||||
~ArtistBioFetcher() override;
|
||||
|
||||
struct Result {
|
||||
QList<QUrl> images_;
|
||||
QList<CollapsibleInfoPane::Data> info_;
|
||||
};
|
||||
|
||||
static const int kDefaultTimeoutDuration = 25000;
|
||||
|
||||
void AddProvider(ArtistBioProvider *provider);
|
||||
int FetchInfo(const Song &metadata);
|
||||
|
||||
QList<ArtistBioProvider*> providers() const { return providers_; }
|
||||
|
||||
Q_SIGNALS:
|
||||
void InfoResultReady(int, CollapsibleInfoPane::Data);
|
||||
void ResultReady(int, ArtistBioFetcher::Result);
|
||||
|
||||
private Q_SLOTS:
|
||||
void ImageReady(const int id, const QUrl &url);
|
||||
void InfoReady(const int id, const CollapsibleInfoPane::Data &data);
|
||||
void ProviderFinished(const int id);
|
||||
void Timeout(const int id);
|
||||
|
||||
private:
|
||||
QList<ArtistBioProvider*> providers_;
|
||||
|
||||
QMap<int, Result> results_;
|
||||
QMap<int, QList<ArtistBioProvider*>> waiting_for_;
|
||||
QMap<int, QTimer*> timeout_timers_;
|
||||
|
||||
int timeout_duration_;
|
||||
int next_id_;
|
||||
};
|
||||
|
||||
#endif // ARTISTBIOFETCHER_H
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* 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 "artistbioprovider.h"
|
||||
|
||||
ArtistBioProvider::ArtistBioProvider() : enabled_(true) {}
|
||||
|
||||
QString ArtistBioProvider::name() const { return QString::fromLatin1(metaObject()->className()); }
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* 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 ARTISTBIOPROVIDER_H
|
||||
#define ARTISTBIOPROVIDER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QUrl>
|
||||
|
||||
#include "widgets/collapsibleinfopane.h"
|
||||
#include "core/song.h"
|
||||
|
||||
class ArtistBioProvider : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ArtistBioProvider();
|
||||
|
||||
virtual void Start(const int id, const Song &song) = 0;
|
||||
virtual void Cancel(const int) {}
|
||||
|
||||
virtual QString name() const;
|
||||
|
||||
bool is_enabled() const { return enabled_; }
|
||||
void set_enabled(bool enabled) { enabled_ = enabled; }
|
||||
|
||||
Q_SIGNALS:
|
||||
void ImageReady(int, QUrl);
|
||||
void InfoReady(int, CollapsibleInfoPane::Data);
|
||||
void Finished(int);
|
||||
|
||||
private:
|
||||
bool enabled_;
|
||||
};
|
||||
|
||||
#endif // ARTISTBIOPROVIDER_H
|
||||
@@ -1,287 +0,0 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* 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 <QWidget>
|
||||
#include <QFile>
|
||||
#include <QScrollArea>
|
||||
#include <QSettings>
|
||||
#include <QSpacerItem>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include <QShowEvent>
|
||||
|
||||
#include "core/song.h"
|
||||
#include "core/networkaccessmanager.h"
|
||||
#include "widgets/prettyimageview.h"
|
||||
#include "widgets/widgetfadehelper.h"
|
||||
#include "artistbiofetcher.h"
|
||||
#include "lastfmartistbio.h"
|
||||
#include "wikipediaartistbio.h"
|
||||
|
||||
#include "artistbioview.h"
|
||||
|
||||
const char *ArtistBioView::kSettingsGroup = "ArtistBio";
|
||||
|
||||
ArtistBioView::ArtistBioView(QWidget *parent)
|
||||
: QWidget(parent),
|
||||
network_(new NetworkAccessManager(this)),
|
||||
fetcher_(new ArtistBioFetcher(this)),
|
||||
current_request_id_(-1),
|
||||
container_(new QVBoxLayout),
|
||||
section_container_(nullptr),
|
||||
fader_(new WidgetFadeHelper(this, 1000)),
|
||||
dirty_(false) {
|
||||
|
||||
// Add the top-level scroll area
|
||||
QScrollArea *scrollarea = new QScrollArea(this);
|
||||
setLayout(new QVBoxLayout);
|
||||
layout()->setContentsMargins(0, 0, 0, 0);
|
||||
layout()->addWidget(scrollarea);
|
||||
|
||||
// Add a container widget to the scroll area
|
||||
QWidget *container_widget = new QWidget;
|
||||
container_widget->setLayout(container_);
|
||||
container_widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
|
||||
container_widget->setBackgroundRole(QPalette::Base);
|
||||
container_->setSizeConstraint(QLayout::SetMinAndMaxSize);
|
||||
container_->setContentsMargins(0, 0, 0, 0);
|
||||
container_->setSpacing(6);
|
||||
scrollarea->setWidget(container_widget);
|
||||
scrollarea->setWidgetResizable(true);
|
||||
|
||||
// Add a spacer to the bottom of the container
|
||||
container_->addStretch();
|
||||
|
||||
// Set stylesheet
|
||||
QFile stylesheet(QStringLiteral(":/style/artistbio.css"));
|
||||
if (stylesheet.open(QIODevice::ReadOnly)) {
|
||||
setStyleSheet(QString::fromLatin1(stylesheet.readAll()));
|
||||
stylesheet.close();
|
||||
}
|
||||
|
||||
fetcher_->AddProvider(new LastFMArtistBio);
|
||||
fetcher_->AddProvider(new WikipediaArtistBio);
|
||||
|
||||
connect(fetcher_, SIGNAL(ResultReady(int, ArtistBioFetcher::Result)), SLOT(ResultReady(int, ArtistBioFetcher::Result)));
|
||||
connect(fetcher_, SIGNAL(InfoResultReady(int, CollapsibleInfoPane::Data)), SLOT(InfoResultReady(int, CollapsibleInfoPane::Data)));
|
||||
|
||||
}
|
||||
|
||||
ArtistBioView::~ArtistBioView() {}
|
||||
|
||||
void ArtistBioView::showEvent(QShowEvent *e) {
|
||||
|
||||
if (dirty_) {
|
||||
MaybeUpdate(queued_metadata_);
|
||||
dirty_ = false;
|
||||
}
|
||||
|
||||
QWidget::showEvent(e);
|
||||
|
||||
}
|
||||
|
||||
void ArtistBioView::ReloadSettings() {
|
||||
|
||||
for (CollapsibleInfoPane *pane : sections_) {
|
||||
QWidget *contents = pane->data().contents_;
|
||||
if (!contents) continue;
|
||||
|
||||
QMetaObject::invokeMethod(contents, "ReloadSettings");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool ArtistBioView::NeedsUpdate(const Song &old_metadata, const Song &new_metadata) const {
|
||||
|
||||
if (new_metadata.artist().isEmpty()) return false;
|
||||
|
||||
return old_metadata.artist() != new_metadata.artist();
|
||||
|
||||
}
|
||||
|
||||
void ArtistBioView::InfoResultReady(const int id, const CollapsibleInfoPane::Data &_data) {
|
||||
|
||||
if (id != current_request_id_) return;
|
||||
|
||||
AddSection(new CollapsibleInfoPane(_data, this));
|
||||
CollapseSections();
|
||||
|
||||
}
|
||||
|
||||
void ArtistBioView::ResultReady(const int id, const ArtistBioFetcher::Result &result) {
|
||||
|
||||
if (id != current_request_id_) return;
|
||||
|
||||
if (!result.images_.isEmpty()) {
|
||||
// Image view goes at the top
|
||||
PrettyImageView *image_view = new PrettyImageView(network_, this);
|
||||
AddWidget(image_view);
|
||||
|
||||
for (const QUrl& url : result.images_) {
|
||||
image_view->AddImage(url);
|
||||
}
|
||||
}
|
||||
|
||||
CollapseSections();
|
||||
|
||||
}
|
||||
|
||||
void ArtistBioView::Clear() {
|
||||
|
||||
fader_->StartFade();
|
||||
|
||||
qDeleteAll(widgets_);
|
||||
widgets_.clear();
|
||||
if (section_container_) {
|
||||
container_->removeWidget(section_container_);
|
||||
delete section_container_;
|
||||
}
|
||||
sections_.clear();
|
||||
|
||||
// Container for collapsible sections goes below
|
||||
section_container_ = new QWidget;
|
||||
section_container_->setLayout(new QVBoxLayout);
|
||||
section_container_->layout()->setContentsMargins(0, 0, 0, 0);
|
||||
section_container_->layout()->setSpacing(1);
|
||||
section_container_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
|
||||
container_->insertWidget(0, section_container_);
|
||||
|
||||
}
|
||||
|
||||
void ArtistBioView::AddSection(CollapsibleInfoPane *section) {
|
||||
|
||||
int i = 0;
|
||||
for (; i < sections_.count(); ++i) {
|
||||
if (section->data() < sections_[i]->data()) break;
|
||||
}
|
||||
|
||||
ConnectWidget(section->data().contents_);
|
||||
|
||||
sections_.insert(i, section);
|
||||
qobject_cast<QVBoxLayout*>(section_container_->layout())->insertWidget(i, section);
|
||||
section->show();
|
||||
|
||||
}
|
||||
|
||||
void ArtistBioView::AddWidget(QWidget *widget) {
|
||||
|
||||
ConnectWidget(widget);
|
||||
|
||||
container_->insertWidget(container_->count() - 2, widget);
|
||||
widgets_ << widget;
|
||||
|
||||
}
|
||||
|
||||
void ArtistBioView::SongChanged(const Song &metadata) {
|
||||
|
||||
if (isVisible()) {
|
||||
MaybeUpdate(metadata);
|
||||
dirty_ = false;
|
||||
}
|
||||
else {
|
||||
queued_metadata_ = metadata;
|
||||
dirty_ = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void ArtistBioView::SongFinished() { dirty_ = false; }
|
||||
|
||||
void ArtistBioView::MaybeUpdate(const Song &metadata) {
|
||||
|
||||
if (old_metadata_.is_valid()) {
|
||||
if (!NeedsUpdate(old_metadata_, metadata)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Update(metadata);
|
||||
old_metadata_ = metadata;
|
||||
|
||||
}
|
||||
|
||||
void ArtistBioView::Update(const Song &metadata) {
|
||||
|
||||
current_request_id_ = fetcher_->FetchInfo(metadata);
|
||||
|
||||
// Do this after the new pane has been shown otherwise it'll just grab a black rectangle.
|
||||
Clear();
|
||||
QTimer::singleShot(0, fader_, SLOT(StartBlur()));
|
||||
|
||||
}
|
||||
|
||||
void ArtistBioView::CollapseSections() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
|
||||
// Sections are already sorted by type and relevance, so the algorithm we use to determine which ones to show by default is:
|
||||
// * In the absence of any user preference, show the first (highest relevance section of each type and hide the rest)
|
||||
// * If one or more sections in a type have been explicitly hidden/shown by the user before then hide all sections in that type and show only the ones that are explicitly shown.
|
||||
|
||||
QMultiMap<CollapsibleInfoPane::Data::Type, CollapsibleInfoPane*> types_;
|
||||
QSet<CollapsibleInfoPane::Data::Type> has_user_preference_;
|
||||
for (CollapsibleInfoPane *pane : sections_) {
|
||||
const CollapsibleInfoPane::Data::Type type = pane->data().type_;
|
||||
types_.insert(type, pane);
|
||||
|
||||
QVariant preference = s.value(pane->data().id_);
|
||||
if (preference.isValid()) {
|
||||
has_user_preference_.insert(type);
|
||||
if (preference.toBool()) {
|
||||
pane->Expand();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (CollapsibleInfoPane::Data::Type type : types_.keys()) {
|
||||
if (!has_user_preference_.contains(type)) {
|
||||
// Expand the first one
|
||||
types_.values(type).last()->Expand();
|
||||
}
|
||||
}
|
||||
|
||||
for (CollapsibleInfoPane *pane : sections_) {
|
||||
connect(pane, SIGNAL(Toggled(bool)), SLOT(SectionToggled(bool)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void ArtistBioView::SectionToggled(const bool value) {
|
||||
|
||||
CollapsibleInfoPane *pane = qobject_cast<CollapsibleInfoPane*>(sender());
|
||||
if (!pane || !sections_.contains(pane)) return;
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
s.setValue(pane->data().id_, value);
|
||||
s.endGroup();
|
||||
|
||||
}
|
||||
|
||||
void ArtistBioView::ConnectWidget(QWidget *widget) {
|
||||
|
||||
const QMetaObject *m = widget->metaObject();
|
||||
|
||||
if (m->indexOfSignal("ShowSettingsDialog()") != -1) {
|
||||
connect(widget, SIGNAL(ShowSettingsDialog()), SIGNAL(ShowSettingsDialog()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* 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 ARTISTBIOVIEW_H
|
||||
#define ARTISTBIOVIEW_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QWidget>
|
||||
#include <QList>
|
||||
|
||||
#include "core/song.h"
|
||||
#include "widgets/collapsibleinfopane.h"
|
||||
#include "widgets/widgetfadehelper.h"
|
||||
#include "widgets/collapsibleinfopane.h"
|
||||
#include "playlist/playlistitem.h"
|
||||
#include "smartplaylists/playlistgenerator_fwd.h"
|
||||
#include "artistbiofetcher.h"
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QTimeLine;
|
||||
class QVBoxLayout;
|
||||
class QScrollArea;
|
||||
class QShowEvent;
|
||||
|
||||
class PrettyImageView;
|
||||
class CollapsibleInfoPane;
|
||||
class WidgetFadeHelper;
|
||||
|
||||
class ArtistBioView : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ArtistBioView(QWidget *parent = nullptr);
|
||||
~ArtistBioView() override;
|
||||
|
||||
static const char *kSettingsGroup;
|
||||
|
||||
public Q_SLOTS:
|
||||
void SongChanged(const Song& metadata);
|
||||
void SongFinished();
|
||||
virtual void ReloadSettings();
|
||||
|
||||
Q_SIGNALS:
|
||||
void ShowSettingsDialog();
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent *e) override;
|
||||
|
||||
void Update(const Song &metadata);
|
||||
void AddWidget(QWidget *widget);
|
||||
void AddSection(CollapsibleInfoPane *section);
|
||||
void Clear();
|
||||
void CollapseSections();
|
||||
|
||||
bool NeedsUpdate(const Song& old_metadata, const Song &new_metadata) const;
|
||||
|
||||
protected Q_SLOTS:
|
||||
void ResultReady(const int id, const ArtistBioFetcher::Result &result);
|
||||
void InfoResultReady(const int id, const CollapsibleInfoPane::Data &data);
|
||||
|
||||
protected:
|
||||
QNetworkAccessManager *network_;
|
||||
ArtistBioFetcher *fetcher_;
|
||||
int current_request_id_;
|
||||
|
||||
private:
|
||||
void MaybeUpdate(const Song &metadata);
|
||||
void ConnectWidget(QWidget *widget);
|
||||
|
||||
private Q_SLOTS:
|
||||
void SectionToggled(const bool value);
|
||||
|
||||
private:
|
||||
QVBoxLayout *container_;
|
||||
QList<QWidget*> widgets_;
|
||||
|
||||
QWidget *section_container_;
|
||||
QList<CollapsibleInfoPane*> sections_;
|
||||
|
||||
WidgetFadeHelper *fader_;
|
||||
|
||||
Song queued_metadata_;
|
||||
Song old_metadata_;
|
||||
bool dirty_;
|
||||
};
|
||||
|
||||
#endif // ARTISTBIOVIEW_H
|
||||
@@ -1,209 +0,0 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2020, 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 <algorithm>
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QLocale>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QDateTime>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonValue>
|
||||
|
||||
#include "core/networkaccessmanager.h"
|
||||
#include "core/song.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/iconloader.h"
|
||||
|
||||
#include "lastfmartistbio.h"
|
||||
#include "widgets/infotextview.h"
|
||||
#include "scrobbler/scrobblingapi20.h"
|
||||
#include "scrobbler/lastfmscrobbler.h"
|
||||
|
||||
LastFMArtistBio::LastFMArtistBio() : ArtistBioProvider(), network_(new NetworkAccessManager(this)) {}
|
||||
|
||||
LastFMArtistBio::~LastFMArtistBio() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void LastFMArtistBio::Start(const int id, const Song &song) {
|
||||
|
||||
ParamList params = ParamList()
|
||||
<< Param(QStringLiteral("api_key"), QString::fromLatin1(ScrobblingAPI20::kApiKey))
|
||||
<< Param(QStringLiteral("lang"), QLocale().name().left(2).toLower())
|
||||
<< Param(QStringLiteral("format"), QStringLiteral("json"))
|
||||
<< Param(QStringLiteral("method"), QStringLiteral("artist.getinfo"))
|
||||
<< Param(QStringLiteral("artist"), song.artist());
|
||||
|
||||
std::sort(params.begin(), params.end());
|
||||
|
||||
QUrlQuery url_query;
|
||||
for (const Param ¶m : params) {
|
||||
url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first)), QString::fromLatin1(QUrl::toPercentEncoding(param.second)));
|
||||
}
|
||||
|
||||
QUrl url(QString::fromLatin1(LastFMScrobbler::kApiUrl));
|
||||
url.setQuery(url_query);
|
||||
QNetworkRequest req(url);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
|
||||
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
#else
|
||||
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
#endif
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded"));
|
||||
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
replies_ << reply;
|
||||
connect(reply, &QNetworkReply::finished, [=] { RequestFinished(reply, id); });
|
||||
|
||||
qLog(Debug) << "Sending request" << url_query.toString(QUrl::FullyDecoded);
|
||||
|
||||
}
|
||||
|
||||
QByteArray LastFMArtistBio::GetReplyData(QNetworkReply *reply) {
|
||||
|
||||
QByteArray data;
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
|
||||
data = reply->readAll();
|
||||
}
|
||||
else {
|
||||
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
|
||||
// This is a network error, there is nothing more to do.
|
||||
Error(QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
|
||||
}
|
||||
else {
|
||||
QString error;
|
||||
// See if there is Json data containing "error" and "message" - then use that instead.
|
||||
data = reply->readAll();
|
||||
QJsonParseError json_error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
|
||||
int error_code = -1;
|
||||
if (json_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) {
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (json_obj.contains(QLatin1String("error")) && json_obj.contains(QLatin1String("message"))) {
|
||||
error_code = json_obj[QLatin1String("error")].toInt();
|
||||
QString error_message = json_obj[QLatin1String("message")].toString();
|
||||
error = QStringLiteral("%1 (%2)").arg(error_message).arg(error_code);
|
||||
}
|
||||
}
|
||||
if (error.isEmpty()) {
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
error = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error());
|
||||
}
|
||||
else {
|
||||
error = QStringLiteral("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
|
||||
}
|
||||
}
|
||||
Error(error);
|
||||
}
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
return data;
|
||||
|
||||
}
|
||||
|
||||
QJsonObject LastFMArtistBio::ExtractJsonObj(const QByteArray &data) {
|
||||
|
||||
if (data.isEmpty()) return QJsonObject();
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
|
||||
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
Error(QStringLiteral("Reply from server missing Json data."), data);
|
||||
return QJsonObject();
|
||||
}
|
||||
if (json_doc.isEmpty()) {
|
||||
Error(QStringLiteral("Received empty Json document."), json_doc);
|
||||
return QJsonObject();
|
||||
}
|
||||
if (!json_doc.isObject()) {
|
||||
Error(QStringLiteral("Json document is not an object."), json_doc);
|
||||
return QJsonObject();
|
||||
}
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (json_obj.isEmpty()) {
|
||||
Error(QStringLiteral("Received empty Json object."), json_doc);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
return json_obj;
|
||||
|
||||
}
|
||||
|
||||
void LastFMArtistBio::RequestFinished(QNetworkReply *reply, const int id) {
|
||||
|
||||
if (!replies_.contains(reply)) return;
|
||||
replies_.removeAll(reply);
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
reply->deleteLater();
|
||||
|
||||
QJsonObject json_obj = ExtractJsonObj(GetReplyData(reply));
|
||||
|
||||
QString title;
|
||||
QString text;
|
||||
if (!json_obj.isEmpty() && json_obj.contains(QLatin1String("artist")) && json_obj[QLatin1String("artist")].isObject()) {
|
||||
json_obj = json_obj[QLatin1String("artist")].toObject();
|
||||
if (json_obj.contains(QLatin1String("bio")) && json_obj[QLatin1String("bio")].isObject()) {
|
||||
title = json_obj[QLatin1String("name")].toString();
|
||||
QJsonObject obj_bio = json_obj[QLatin1String("bio")].toObject();
|
||||
if (obj_bio.contains(QLatin1String("content"))) {
|
||||
text = obj_bio[QLatin1String("content")].toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollapsibleInfoPane::Data info_data;
|
||||
info_data.id_ = title;
|
||||
info_data.title_ = tr("Biography");
|
||||
info_data.type_ = CollapsibleInfoPane::Data::Type_Biography;
|
||||
info_data.icon_ = IconLoader::Load(QStringLiteral("scrobble"));
|
||||
InfoTextView *editor = new InfoTextView;
|
||||
editor->SetHtml(text);
|
||||
info_data.contents_ = editor;
|
||||
Q_EMIT InfoReady(id, info_data);
|
||||
Q_EMIT Finished(id);
|
||||
|
||||
}
|
||||
|
||||
void LastFMArtistBio::Error(const QString &error, const QVariant &debug) {
|
||||
|
||||
qLog(Error) << error;
|
||||
if (debug.isValid()) qLog(Debug) << debug;
|
||||
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2020, 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 LASTFMARTISTBIO_H
|
||||
#define LASTFMARTISTBIO_H
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVariant>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
|
||||
#include "core/song.h"
|
||||
|
||||
#include "artistbioprovider.h"
|
||||
|
||||
class NetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
|
||||
class LastFMArtistBio : public ArtistBioProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit LastFMArtistBio();
|
||||
~LastFMArtistBio();
|
||||
|
||||
void Start(const int id, const Song &song) override;
|
||||
|
||||
private:
|
||||
using Param = QPair<QString, QString>;
|
||||
using ParamList = QList<Param>;
|
||||
|
||||
QNetworkReply *CreateRequest(const ParamList &request_params);
|
||||
QByteArray GetReplyData(QNetworkReply *reply);
|
||||
QJsonObject ExtractJsonObj(const QByteArray &data);
|
||||
void Error(const QString &error, const QVariant &debug = QVariant());
|
||||
|
||||
private Q_SLOTS:
|
||||
void RequestFinished(QNetworkReply *reply, const int id);
|
||||
|
||||
private:
|
||||
NetworkAccessManager *network_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
};
|
||||
|
||||
#endif // LASTFMARTISTBIO_H
|
||||
@@ -1,319 +0,0 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2020, 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 <QObject>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QNetworkReply>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/networkaccessmanager.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "core/latch.h"
|
||||
|
||||
#include "widgets/infotextview.h"
|
||||
#include "wikipediaartistbio.h"
|
||||
|
||||
namespace {
|
||||
constexpr char kApiUrl[] = "https://en.wikipedia.org/w/api.php";
|
||||
constexpr int kMinimumImageSize = 400;
|
||||
}
|
||||
|
||||
WikipediaArtistBio::WikipediaArtistBio() : ArtistBioProvider(), network_(new NetworkAccessManager(this)) {}
|
||||
|
||||
WikipediaArtistBio::~WikipediaArtistBio() {
|
||||
|
||||
while (!replies_.isEmpty()) {
|
||||
QNetworkReply *reply = replies_.takeFirst();
|
||||
disconnect(reply, nullptr, this, nullptr);
|
||||
if (reply->isRunning()) reply->abort();
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QNetworkReply *WikipediaArtistBio::CreateRequest(QList<Param> ¶ms) {
|
||||
|
||||
params << Param(QLatin1String("format"), QLatin1String("json"));
|
||||
params << Param(QLatin1String("action"), QLatin1String("query"));
|
||||
|
||||
QUrlQuery url_query;
|
||||
for (const Param ¶m : params) {
|
||||
url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first)), QString::fromLatin1(QUrl::toPercentEncoding(param.second)));
|
||||
}
|
||||
|
||||
QUrl url(QString::fromLatin1(kApiUrl));
|
||||
url.setQuery(url_query);
|
||||
QNetworkRequest req(url);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
|
||||
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
#else
|
||||
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
#endif
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded"));
|
||||
|
||||
QNetworkReply *reply = network_->get(req);
|
||||
connect(reply, &QNetworkReply::sslErrors, this, &WikipediaArtistBio::HandleSSLErrors);
|
||||
replies_ << reply;
|
||||
|
||||
return reply;
|
||||
|
||||
}
|
||||
|
||||
QByteArray WikipediaArtistBio::GetReplyData(QNetworkReply *reply) {
|
||||
|
||||
QByteArray data;
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
|
||||
data = reply->readAll();
|
||||
}
|
||||
else {
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
qLog(Error) << "Wikipedia artist biography error: Received HTTP code" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
}
|
||||
else {
|
||||
qLog(Error) << "Wikipedia artist biography error:" << reply->error() << reply->errorString();
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
|
||||
}
|
||||
|
||||
QJsonObject WikipediaArtistBio::ExtractJsonObj(const QByteArray &data) {
|
||||
|
||||
if (data.isEmpty()) return QJsonObject();
|
||||
|
||||
QJsonParseError json_error;
|
||||
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
|
||||
|
||||
if (json_error.error != QJsonParseError::NoError) {
|
||||
qLog(Error) << "Wikipedia artist biography error: Failed to parse json data:" << json_error.errorString();
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
if (json_doc.isEmpty()) {
|
||||
qLog(Error) << "Wikipedia artist biography error: Received empty Json document.";
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
if (!json_doc.isObject()) {
|
||||
qLog(Error) << "Wikipedia artist biography error: Json document is not an object.";
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
QJsonObject json_obj = json_doc.object();
|
||||
if (json_obj.isEmpty()) {
|
||||
qLog(Error) << "Wikipedia artist biography error: Received empty Json object.";
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
return json_obj;
|
||||
|
||||
}
|
||||
|
||||
void WikipediaArtistBio::HandleSSLErrors(QList<QSslError>) {}
|
||||
|
||||
void WikipediaArtistBio::Start(const int id, const Song &metadata) {
|
||||
|
||||
if (metadata.artist().isEmpty()) {
|
||||
Q_EMIT Finished(id);
|
||||
return;
|
||||
}
|
||||
|
||||
CountdownLatch *latch = new CountdownLatch;
|
||||
connect(latch, &CountdownLatch::Done, [this, id, latch](){
|
||||
latch->deleteLater();
|
||||
Q_EMIT Finished(id);
|
||||
});
|
||||
|
||||
GetImageTitles(id, metadata.artist(), latch);
|
||||
//GetArticle(id, metadata.artist(), latch);
|
||||
|
||||
}
|
||||
|
||||
void WikipediaArtistBio::GetArticle(const int id, const QString &artist, CountdownLatch *latch) {
|
||||
|
||||
latch->Wait();
|
||||
|
||||
ParamList params = ParamList() << Param(QStringLiteral("titles"), artist)
|
||||
<< Param(QStringLiteral("prop"), QStringLiteral("extracts"));
|
||||
|
||||
QNetworkReply *reply = CreateRequest(params);
|
||||
connect(reply, &QNetworkReply::finished, [this, reply, id, latch]() { GetArticleReply(reply, id, latch); });
|
||||
|
||||
}
|
||||
|
||||
void WikipediaArtistBio::GetArticleReply(QNetworkReply *reply, const int id, CountdownLatch *latch) {
|
||||
|
||||
reply->deleteLater();
|
||||
replies_.removeAll(reply);
|
||||
|
||||
QJsonObject json_obj = ExtractJsonObj(GetReplyData(reply));
|
||||
|
||||
QString title;
|
||||
QString text;
|
||||
if (!json_obj.isEmpty() && json_obj.contains(QLatin1String("query")) && json_obj[QLatin1String("query")].isObject()) {
|
||||
json_obj = json_obj[QLatin1String("query")].toObject();
|
||||
if (json_obj.contains(QLatin1String("pages")) && json_obj[QLatin1String("pages")].isObject()) {
|
||||
QJsonObject value_pages = json_obj[QLatin1String("pages")].toObject();
|
||||
for (const QJsonValue value_page : value_pages) {
|
||||
if (!value_page.isObject()) continue;
|
||||
QJsonObject obj_page = value_page.toObject();
|
||||
if (!obj_page.contains(QLatin1String("title")) || !obj_page.contains(QLatin1String("extract"))) continue;
|
||||
title = obj_page[QLatin1String("title")].toString();
|
||||
text = obj_page[QLatin1String("extract")].toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollapsibleInfoPane::Data info_data;
|
||||
info_data.id_ = title;
|
||||
info_data.title_ = tr("Biography");
|
||||
info_data.type_ = CollapsibleInfoPane::Data::Type_Biography;
|
||||
info_data.icon_ = IconLoader::Load(QStringLiteral("wikipedia"));
|
||||
InfoTextView *editor = new InfoTextView;
|
||||
editor->SetHtml(text);
|
||||
info_data.contents_ = editor;
|
||||
Q_EMIT InfoReady(id, info_data);
|
||||
|
||||
latch->CountDown();
|
||||
|
||||
}
|
||||
|
||||
void WikipediaArtistBio::GetImageTitles(const int id, const QString &artist, CountdownLatch *latch) {
|
||||
|
||||
latch->Wait();
|
||||
|
||||
ParamList params = ParamList() << Param(QStringLiteral("titles"), artist)
|
||||
<< Param(QStringLiteral("prop"), QStringLiteral("images"))
|
||||
<< Param(QStringLiteral("imlimit"), QString::number(25));
|
||||
|
||||
QNetworkReply *reply = CreateRequest(params);
|
||||
connect(reply, &QNetworkReply::finished, [this, reply, id, latch]() { GetImageTitlesFinished(reply, id, latch); });
|
||||
|
||||
}
|
||||
|
||||
void WikipediaArtistBio::GetImageTitlesFinished(QNetworkReply *reply, const int id, CountdownLatch *latch) {
|
||||
|
||||
reply->deleteLater();
|
||||
replies_.removeAll(reply);
|
||||
|
||||
QJsonObject json_obj = ExtractJsonObj(GetReplyData(reply));
|
||||
|
||||
QString title;
|
||||
QStringList titles;
|
||||
if (!json_obj.isEmpty() && json_obj.contains(QLatin1String("query")) && json_obj[QLatin1String("query")].isObject()) {
|
||||
json_obj = json_obj[QLatin1String("query")].toObject();
|
||||
if (json_obj.contains(QLatin1String("pages")) && json_obj[QLatin1String("pages")].isObject()) {
|
||||
QJsonObject value_pages = json_obj[QLatin1String("pages")].toObject();
|
||||
for (const QJsonValue value_page : value_pages) {
|
||||
if (!value_page.isObject()) continue;
|
||||
QJsonObject obj_page = value_page.toObject();
|
||||
if (!obj_page.contains(QLatin1String("title")) || !obj_page.contains(QLatin1String("images")) || !obj_page[QLatin1String("images")].isArray()) continue;
|
||||
title = obj_page[QLatin1String("title")].toString();
|
||||
QJsonArray array_images = obj_page[QLatin1String("images")].toArray();
|
||||
for (const QJsonValue value_image : array_images) {
|
||||
if (!value_image.isObject()) continue;
|
||||
QJsonObject obj_image = value_image.toObject();
|
||||
if (!obj_image.contains(QLatin1String("title"))) continue;
|
||||
QString filename = obj_image[QLatin1String("title")].toString();
|
||||
if (filename.endsWith(QLatin1String(".jpg"), Qt::CaseInsensitive) || filename.endsWith(QLatin1String(".png"), Qt::CaseInsensitive)) {
|
||||
titles << filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const QString &image_title : titles) {
|
||||
GetImage(id, image_title, latch);
|
||||
}
|
||||
|
||||
latch->CountDown();
|
||||
|
||||
}
|
||||
|
||||
void WikipediaArtistBio::GetImage(const int id, const QString &title, CountdownLatch *latch) {
|
||||
|
||||
latch->Wait();
|
||||
|
||||
ParamList params2 = ParamList() << Param(QStringLiteral("titles"), title)
|
||||
<< Param(QStringLiteral("prop"), QStringLiteral("imageinfo"))
|
||||
<< Param(QStringLiteral("iiprop"), QStringLiteral("url|size"));
|
||||
|
||||
QNetworkReply *reply = CreateRequest(params2);
|
||||
connect(reply, &QNetworkReply::finished, [this, reply, id, latch]() { GetImageFinished(reply, id, latch); });
|
||||
|
||||
}
|
||||
|
||||
void WikipediaArtistBio::GetImageFinished(QNetworkReply *reply, const int id, CountdownLatch *latch) {
|
||||
|
||||
reply->deleteLater();
|
||||
replies_.removeAll(reply);
|
||||
|
||||
QJsonObject json_obj = ExtractJsonObj(GetReplyData(reply));
|
||||
|
||||
if (!json_obj.isEmpty()) {
|
||||
QList<QUrl> urls = ExtractImageUrls(json_obj);
|
||||
for (const QUrl &url : urls) {
|
||||
Q_EMIT ImageReady(id, url);
|
||||
}
|
||||
}
|
||||
|
||||
latch->CountDown();
|
||||
|
||||
}
|
||||
|
||||
QList<QUrl> WikipediaArtistBio::ExtractImageUrls(QJsonObject json_obj) {
|
||||
|
||||
QList<QUrl> urls;
|
||||
if (json_obj.contains(QLatin1String("query")) && json_obj[QLatin1String("query")].isObject()) {
|
||||
json_obj = json_obj[QLatin1String("query")].toObject();
|
||||
if (json_obj.contains(QLatin1String("pages")) && json_obj[QLatin1String("pages")].isObject()) {
|
||||
QJsonObject value_pages = json_obj[QLatin1String("pages")].toObject();
|
||||
for (const QJsonValue value_page : value_pages) {
|
||||
if (!value_page.isObject()) continue;
|
||||
QJsonObject obj_page = value_page.toObject();
|
||||
if (!obj_page.contains(QLatin1String("title")) || !obj_page.contains(QLatin1String("imageinfo")) || !obj_page[QLatin1String("imageinfo")].isArray()) continue;
|
||||
QJsonArray array_images = obj_page[QLatin1String("imageinfo")].toArray();
|
||||
for (const QJsonValue value_image : array_images) {
|
||||
if (!value_image.isObject()) continue;
|
||||
QJsonObject obj_image = value_image.toObject();
|
||||
if (!obj_image.contains(QLatin1String("url")) || !obj_image.contains(QLatin1String("width")) || !obj_image.contains(QLatin1String("height"))) continue;
|
||||
QUrl url(obj_image[QLatin1String("url")].toString());
|
||||
const int width = obj_image[QLatin1String("width")].toInt();
|
||||
const int height = obj_image[QLatin1String("height")].toInt();
|
||||
if (!url.isValid() || width < kMinimumImageSize || height < kMinimumImageSize) continue;
|
||||
urls << url;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return urls;
|
||||
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* Copyright 2020, 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 WIKIPEDIAARTISTBIO_H
|
||||
#define WIKIPEDIAARTISTBIO_H
|
||||
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QSslError>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "artistbioprovider.h"
|
||||
|
||||
class QNetworkReply;
|
||||
class CountdownLatch;
|
||||
class NetworkAccessManager;
|
||||
|
||||
class WikipediaArtistBio : public ArtistBioProvider {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit WikipediaArtistBio();
|
||||
~WikipediaArtistBio();
|
||||
|
||||
void Start(const int id, const Song &song) override;
|
||||
|
||||
private:
|
||||
typedef QPair<QString, QString> Param;
|
||||
typedef QList<Param> ParamList;
|
||||
|
||||
QNetworkReply *CreateRequest(QList<Param> ¶ms);
|
||||
QByteArray GetReplyData(QNetworkReply *reply);
|
||||
QJsonObject ExtractJsonObj(const QByteArray &data);
|
||||
void GetArticle(const int id, const QString &artist, CountdownLatch* latch);
|
||||
void GetImageTitles(const int id, const QString &artist, CountdownLatch* latch);
|
||||
void GetImage(const int id, const QString &title, CountdownLatch *latch);
|
||||
QList<QUrl> ExtractImageUrls(QJsonObject json_obj);
|
||||
|
||||
private Q_SLOTS:
|
||||
void HandleSSLErrors(QList<QSslError> ssl_errors);
|
||||
void GetArticleReply(QNetworkReply *reply, const int id, CountdownLatch *latch);
|
||||
void GetImageTitlesFinished(QNetworkReply *reply, const int id, CountdownLatch *latch);
|
||||
void GetImageFinished(QNetworkReply *reply, const int id, CountdownLatch *latch);
|
||||
|
||||
private:
|
||||
NetworkAccessManager *network_;
|
||||
QList<QNetworkReply*> replies_;
|
||||
|
||||
};
|
||||
|
||||
#endif // WIKIPEDIAARTISTBIO_H
|
||||
@@ -1,39 +0,0 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2016, John Maguire <john.maguire@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
#include <QMutexLocker>
|
||||
|
||||
#include "latch.h"
|
||||
|
||||
CountdownLatch::CountdownLatch() : count_(0) {}
|
||||
|
||||
void CountdownLatch::Wait() {
|
||||
|
||||
QMutexLocker l(&mutex_);
|
||||
++count_;
|
||||
|
||||
}
|
||||
|
||||
void CountdownLatch::CountDown() {
|
||||
|
||||
QMutexLocker l(&mutex_);
|
||||
Q_ASSERT(count_ > 0);
|
||||
--count_;
|
||||
if (count_ == 0) {
|
||||
emit Done();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2016, John Maguire <john.maguire@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef LATCH_H
|
||||
#define LATCH_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QMutex>
|
||||
|
||||
class CountdownLatch : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CountdownLatch();
|
||||
void Wait();
|
||||
void CountDown();
|
||||
|
||||
Q_SIGNALS:
|
||||
void Done();
|
||||
|
||||
private:
|
||||
QMutex mutex_;
|
||||
int count_;
|
||||
};
|
||||
|
||||
#endif // LATCH_H
|
||||
@@ -205,7 +205,6 @@
|
||||
#include "smartplaylists/smartplaylistsviewcontainer.h"
|
||||
|
||||
#include "organize/organizeerrordialog.h"
|
||||
#include "artistbio/artistbioview.h"
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
# include "core/windows7thumbbar.h"
|
||||
@@ -233,7 +232,6 @@
|
||||
|
||||
using std::make_unique;
|
||||
using std::make_shared;
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
@@ -359,7 +357,6 @@ MainWindow::MainWindow(Application *app,
|
||||
qobuz_view_(new StreamingTabsView(app->streaming_services()->ServiceBySource(Song::Source::Qobuz), app->albumcover_loader(), QLatin1String(QobuzSettings::kSettingsGroup), this)),
|
||||
#endif
|
||||
radio_view_(new RadioViewContainer(this)),
|
||||
artistbio_view_(new ArtistBioView(this)),
|
||||
lastfm_import_dialog_(new LastFMImportDialog(app_->lastfm_import(), this)),
|
||||
collection_show_all_(nullptr),
|
||||
collection_show_duplicates_(nullptr),
|
||||
@@ -444,7 +441,6 @@ MainWindow::MainWindow(Application *app,
|
||||
#ifdef HAVE_QOBUZ
|
||||
ui_->tabs->AddTab(qobuz_view_, u"qobuz"_s, IconLoader::Load(u"qobuz"_s, true, 0, 32), tr("Qobuz"));
|
||||
#endif
|
||||
ui_->tabs->AddTab(artistbio_view_, QStringLiteral("artistbio"), IconLoader::Load(QStringLiteral("guitar")), tr("Artist biography"));
|
||||
|
||||
// Add the playing widget to the fancy tab widget
|
||||
ui_->tabs->AddBottomWidget(ui_->widget_playing);
|
||||
@@ -981,10 +977,6 @@ MainWindow::MainWindow(Application *app,
|
||||
ui_->action_open_cd->setVisible(false);
|
||||
#endif
|
||||
|
||||
connect(&*app_->playlist_manager(), &PlaylistManager::CurrentSongChanged, artistbio_view_, &ArtistBioView::SongChanged);
|
||||
connect(&*app_->player(), &Player::PlaylistFinished, artistbio_view_, &ArtistBioView::SongFinished);
|
||||
connect(&*app_->player(), &Player::Stopped, artistbio_view_, &ArtistBioView::SongFinished);
|
||||
|
||||
// Load settings
|
||||
qLog(Debug) << "Loading settings";
|
||||
Settings settings;
|
||||
@@ -1240,16 +1232,6 @@ void MainWindow::ReloadSettings() {
|
||||
album_cover_choice_controller_->search_cover_auto_action()->setChecked(s.value(MainWindowSettings::kSearchForCoverAuto, true).toBool());
|
||||
s.endGroup();
|
||||
|
||||
s.beginGroup(BehaviourSettings::kSettingsGroup);
|
||||
bool artistbio = s.value("artistbio", false).toBool();
|
||||
s.endGroup();
|
||||
if (artistbio) {
|
||||
ui_->tabs->EnableTab(artistbio_view_);
|
||||
}
|
||||
else {
|
||||
ui_->tabs->DisableTab(artistbio_view_);
|
||||
}
|
||||
|
||||
#ifdef HAVE_SUBSONIC
|
||||
s.beginGroup(SubsonicSettings::kSettingsGroup);
|
||||
bool enable_subsonic = s.value(SubsonicSettings::kEnabled, false).toBool();
|
||||
|
||||
@@ -97,7 +97,6 @@ class Windows7ThumbBar;
|
||||
class AddStreamDialog;
|
||||
class LastFMImportDialog;
|
||||
class RadioViewContainer;
|
||||
class ArtistBioView;
|
||||
|
||||
#ifdef HAVE_DISCORD_RPC
|
||||
namespace discord {
|
||||
@@ -359,8 +358,6 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
||||
|
||||
RadioViewContainer *radio_view_;
|
||||
|
||||
ArtistBioView *artistbio_view_;
|
||||
|
||||
LastFMImportDialog *lastfm_import_dialog_;
|
||||
|
||||
QAction *collection_show_all_;
|
||||
|
||||
@@ -68,7 +68,6 @@
|
||||
#include "smartplaylists/playlistgenerator_fwd.h"
|
||||
|
||||
#include "radios/radiochannel.h"
|
||||
#include "widgets/collapsibleinfopane.h"
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
# include "device/mtpconnection.h"
|
||||
@@ -148,8 +147,6 @@ void RegisterMetaTypes() {
|
||||
qRegisterMetaType<RadioChannel>("RadioChannel");
|
||||
qRegisterMetaType<RadioChannelList>("RadioChannelList");
|
||||
|
||||
qRegisterMetaType<CollapsibleInfoPane::Data>("CollapsibleInfoPane::Data");
|
||||
|
||||
#ifdef HAVE_MTP
|
||||
qRegisterMetaType<MtpConnection*>("MtpConnection*");
|
||||
#endif
|
||||
|
||||
@@ -75,7 +75,7 @@ void RichPresence::ReloadSettings() {
|
||||
s.endGroup();
|
||||
|
||||
if (enabled && !initialized_) {
|
||||
Discord_Initialize(kDiscordApplicationId, nullptr, 0);
|
||||
Discord_Initialize(kDiscordApplicationId, nullptr, 0, nullptr);
|
||||
initialized_ = true;
|
||||
}
|
||||
else if (!enabled && initialized_) {
|
||||
@@ -124,7 +124,6 @@ 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;
|
||||
|
||||
@@ -165,7 +165,6 @@ void BehaviourSettingsPage::Load() {
|
||||
|
||||
ui_->checkbox_resumeplayback->setChecked(s.value(kResumePlayback, false).toBool());
|
||||
ui_->checkbox_playingwidget->setChecked(s.value(kPlayingWidget, true).toBool());
|
||||
ui_->checkbox_artistbio->setChecked(s.value("artistbio", false).toBool());
|
||||
|
||||
#ifndef Q_OS_MACOS
|
||||
const StartupBehaviour startup_behaviour = static_cast<StartupBehaviour>(s.value(kStartupBehaviour, static_cast<int>(StartupBehaviour::Remember)).toInt());
|
||||
@@ -233,12 +232,9 @@ void BehaviourSettingsPage::Save() {
|
||||
#if defined(HAVE_DBUS) && !defined(Q_OS_MACOS)
|
||||
s.setValue(kTaskbarProgress, ui_->checkbox_taskbar_progress->isChecked());
|
||||
#endif
|
||||
|
||||
s.setValue(kResumePlayback, ui_->checkbox_resumeplayback->isChecked());
|
||||
s.setValue(kPlayingWidget, ui_->checkbox_playingwidget->isChecked());
|
||||
|
||||
s.setValue("artistbio", ui_->checkbox_artistbio->isChecked());
|
||||
|
||||
StartupBehaviour startup_behaviour = StartupBehaviour::Remember;
|
||||
if (ui_->radiobutton_remember->isChecked()) startup_behaviour = StartupBehaviour::Remember;
|
||||
if (ui_->radiobutton_show->isChecked()) startup_behaviour = StartupBehaviour::Show;
|
||||
|
||||
@@ -65,13 +65,6 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkbox_artistbio">
|
||||
<property name="text">
|
||||
<string>Artist biography</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupbox_startup">
|
||||
<property name="title">
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* 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 <QWidget>
|
||||
#include <QApplication>
|
||||
#include <QString>
|
||||
#include <QIcon>
|
||||
#include <QPainter>
|
||||
#include <QPalette>
|
||||
#include <QColor>
|
||||
#include <QStyle>
|
||||
#include <QFont>
|
||||
#include <QPropertyAnimation>
|
||||
#include <QStyleOption>
|
||||
#include <QEnterEvent>
|
||||
#include <QPaintEvent>
|
||||
#include <QMouseEvent>
|
||||
#include <QEvent>
|
||||
|
||||
#include "collapsibleinfoheader.h"
|
||||
|
||||
const int CollapsibleInfoHeader::kHeight = 20;
|
||||
const int CollapsibleInfoHeader::kIconSize = 16;
|
||||
|
||||
CollapsibleInfoHeader::CollapsibleInfoHeader(QWidget* parent)
|
||||
: QWidget(parent),
|
||||
expanded_(false),
|
||||
hovering_(false),
|
||||
animation_(new QPropertyAnimation(this, "opacity", this)),
|
||||
opacity_(0.0) {
|
||||
|
||||
setMinimumHeight(kHeight);
|
||||
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
setCursor(QCursor(Qt::PointingHandCursor));
|
||||
|
||||
}
|
||||
|
||||
void CollapsibleInfoHeader::SetTitle(const QString &title) {
|
||||
title_ = title;
|
||||
update();
|
||||
}
|
||||
|
||||
void CollapsibleInfoHeader::SetIcon(const QIcon &icon) {
|
||||
icon_ = icon;
|
||||
update();
|
||||
}
|
||||
|
||||
void CollapsibleInfoHeader::SetExpanded(const bool expanded) {
|
||||
|
||||
expanded_ = expanded;
|
||||
|
||||
emit ExpandedToggled(expanded);
|
||||
if (expanded)
|
||||
emit Expanded();
|
||||
else
|
||||
emit Collapsed();
|
||||
}
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
void CollapsibleInfoHeader::enterEvent(QEnterEvent*) {
|
||||
#else
|
||||
void CollapsibleInfoHeader::enterEvent(QEvent*) {
|
||||
#endif
|
||||
|
||||
hovering_ = true;
|
||||
if (!expanded_) {
|
||||
animation_->stop();
|
||||
animation_->setEndValue(1.0);
|
||||
animation_->setDuration(80);
|
||||
animation_->start();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollapsibleInfoHeader::leaveEvent(QEvent*) {
|
||||
|
||||
hovering_ = false;
|
||||
if (!expanded_) {
|
||||
animation_->stop();
|
||||
animation_->setEndValue(0.0);
|
||||
animation_->setDuration(160);
|
||||
animation_->start();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void CollapsibleInfoHeader::set_opacity(const float opacity) {
|
||||
opacity_ = opacity;
|
||||
update();
|
||||
}
|
||||
|
||||
void CollapsibleInfoHeader::paintEvent(QPaintEvent*) {
|
||||
|
||||
QPainter p(this);
|
||||
|
||||
QColor active_text_color(palette().color(QPalette::Active, QPalette::HighlightedText));
|
||||
QColor inactive_text_color(palette().color(QPalette::Active, QPalette::Text));
|
||||
QColor text_color;
|
||||
if (expanded_) {
|
||||
text_color = active_text_color;
|
||||
}
|
||||
else {
|
||||
p.setOpacity(0.4 + opacity_ * 0.6);
|
||||
text_color = QColor(active_text_color.red() * opacity_ + inactive_text_color.red() * (1.0 - opacity_), active_text_color.green() * opacity_ + inactive_text_color.green() * (1.0 - opacity_), active_text_color.blue() * opacity_ + inactive_text_color.blue() * (1.0 - opacity_));
|
||||
}
|
||||
|
||||
QRect indicator_rect(0, 0, height(), height());
|
||||
QRect icon_rect(height() + 2, (kHeight - kIconSize) / 2, kIconSize, kIconSize);
|
||||
QRect text_rect(rect());
|
||||
text_rect.setLeft(icon_rect.right() + 4);
|
||||
|
||||
// Draw the background
|
||||
QColor highlight(palette().color(QPalette::Active, QPalette::Highlight));
|
||||
const QColor bg_color_1(highlight.lighter(120));
|
||||
const QColor bg_color_2(highlight.darker(120));
|
||||
const QColor bg_border(palette().color(QPalette::Dark));
|
||||
QLinearGradient bg_brush(rect().topLeft(), rect().bottomLeft());
|
||||
bg_brush.setColorAt(0.0, bg_color_1);
|
||||
bg_brush.setColorAt(0.5, bg_color_1);
|
||||
bg_brush.setColorAt(0.5, bg_color_2);
|
||||
bg_brush.setColorAt(1.0, bg_color_2);
|
||||
|
||||
p.setPen(Qt::NoPen);
|
||||
p.fillRect(rect(), bg_brush);
|
||||
|
||||
p.setPen(bg_border);
|
||||
p.drawLine(rect().topLeft(), rect().topRight());
|
||||
p.drawLine(rect().bottomLeft(), rect().bottomRight());
|
||||
|
||||
// Draw the expand/collapse indicator
|
||||
QStyleOption opt;
|
||||
opt.initFrom(this);
|
||||
opt.rect = indicator_rect;
|
||||
opt.state |= QStyle::State_Children;
|
||||
if (expanded_) opt.state |= QStyle::State_Open;
|
||||
if (hovering_) opt.state |= QStyle::State_Active;
|
||||
|
||||
// Have to use the application's style here because using the widget's style
|
||||
// will trigger QStyleSheetStyle's recursion guard (I don't know why).
|
||||
QApplication::style()->drawPrimitive(QStyle::PE_IndicatorBranch, &opt, &p, this);
|
||||
|
||||
// Draw the icon
|
||||
p.drawPixmap(icon_rect, icon_.pixmap(kIconSize));
|
||||
|
||||
// Draw the title text
|
||||
QFont bold_font(font());
|
||||
bold_font.setBold(true);
|
||||
p.setFont(bold_font);
|
||||
|
||||
p.setPen(text_color);
|
||||
p.drawText(text_rect, Qt::AlignLeft | Qt::AlignVCenter, title_);
|
||||
|
||||
}
|
||||
|
||||
void CollapsibleInfoHeader::mouseReleaseEvent(QMouseEvent*) {
|
||||
SetExpanded(!expanded_);
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* 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 COLLAPSIBLEINFOHEADER_H
|
||||
#define COLLAPSIBLEINFOHEADER_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QString>
|
||||
#include <QIcon>
|
||||
|
||||
class QPropertyAnimation;
|
||||
class QEnterEvent;
|
||||
class QEvent;
|
||||
class QPaintEvent;
|
||||
class QMouseEvent;
|
||||
|
||||
class CollapsibleInfoHeader : public QWidget {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(float opacity READ opacity WRITE set_opacity)
|
||||
|
||||
public:
|
||||
CollapsibleInfoHeader(QWidget *parent = nullptr);
|
||||
|
||||
static const int kHeight;
|
||||
static const int kIconSize;
|
||||
|
||||
bool expanded() const { return expanded_; }
|
||||
bool hovering() const { return hovering_; }
|
||||
const QString &title() const { return title_; }
|
||||
const QIcon &icon() const { return icon_; }
|
||||
|
||||
float opacity() const { return opacity_; }
|
||||
void set_opacity(const float opacity);
|
||||
|
||||
public Q_SLOTS:
|
||||
void SetExpanded(const bool expanded);
|
||||
void SetTitle(const QString &title);
|
||||
void SetIcon(const QIcon &icon);
|
||||
|
||||
Q_SIGNALS:
|
||||
void Expanded();
|
||||
void Collapsed();
|
||||
void ExpandedToggled(const bool expanded);
|
||||
|
||||
protected:
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
void enterEvent(QEnterEvent*) override;
|
||||
#else
|
||||
void enterEvent(QEvent*) override;
|
||||
#endif
|
||||
void leaveEvent(QEvent*) override;
|
||||
void paintEvent(QPaintEvent*) override;
|
||||
void mouseReleaseEvent(QMouseEvent*) override;
|
||||
|
||||
private:
|
||||
bool expanded_;
|
||||
bool hovering_;
|
||||
QString title_;
|
||||
QIcon icon_;
|
||||
|
||||
QPropertyAnimation *animation_;
|
||||
float opacity_;
|
||||
};
|
||||
|
||||
#endif // COLLAPSIBLEINFOHEADER_H
|
||||
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* 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 "collapsibleinfoheader.h"
|
||||
#include "collapsibleinfopane.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
|
||||
CollapsibleInfoPane::CollapsibleInfoPane(const Data &data, QWidget *parent)
|
||||
: QWidget(parent), data_(data), header_(new CollapsibleInfoHeader(this)) {
|
||||
|
||||
QVBoxLayout* layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(3);
|
||||
layout->setSizeConstraint(QLayout::SetMinAndMaxSize);
|
||||
setLayout(layout);
|
||||
|
||||
layout->addWidget(header_);
|
||||
layout->addWidget(data.contents_);
|
||||
data.contents_->hide();
|
||||
|
||||
header_->SetTitle(data.title_);
|
||||
header_->SetIcon(data.icon_);
|
||||
|
||||
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
|
||||
|
||||
connect(header_, SIGNAL(ExpandedToggled(bool)), SLOT(ExpandedToggled(bool)));
|
||||
connect(header_, SIGNAL(ExpandedToggled(bool)), SIGNAL(Toggled(bool)));
|
||||
|
||||
}
|
||||
|
||||
void CollapsibleInfoPane::Collapse() { header_->SetExpanded(false); }
|
||||
|
||||
void CollapsibleInfoPane::Expand() { header_->SetExpanded(true); }
|
||||
|
||||
void CollapsibleInfoPane::ExpandedToggled(bool expanded) {
|
||||
data_.contents_->setVisible(expanded);
|
||||
}
|
||||
|
||||
bool CollapsibleInfoPane::Data::operator<(const CollapsibleInfoPane::Data &other) const {
|
||||
|
||||
const int my_score = (TypeCount - type_) * 1000 + relevance_;
|
||||
const int other_score = (TypeCount - other.type_) * 1000 + other.relevance_;
|
||||
|
||||
return my_score > other_score;
|
||||
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* 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 COLLAPSIBLEINFOPANE_H
|
||||
#define COLLAPSIBLEINFOPANE_H
|
||||
|
||||
#include <QIcon>
|
||||
#include <QWidget>
|
||||
|
||||
class CollapsibleInfoHeader;
|
||||
|
||||
class CollapsibleInfoPane : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
struct Data {
|
||||
explicit Data() : type_(Type_Biography), relevance_(0) {}
|
||||
|
||||
bool operator<(const Data& other) const;
|
||||
|
||||
enum Type {
|
||||
Type_Biography,
|
||||
TypeCount
|
||||
};
|
||||
|
||||
QString id_;
|
||||
QString title_;
|
||||
QIcon icon_;
|
||||
Type type_;
|
||||
int relevance_;
|
||||
|
||||
QWidget *contents_;
|
||||
QObject *content_object_;
|
||||
};
|
||||
|
||||
CollapsibleInfoPane(const Data &data, QWidget* parent = nullptr);
|
||||
|
||||
const Data &data() const { return data_; }
|
||||
|
||||
public Q_SLOTS:
|
||||
void Expand();
|
||||
void Collapse();
|
||||
|
||||
Q_SIGNALS:
|
||||
void Toggled(const bool expanded);
|
||||
|
||||
private Q_SLOTS:
|
||||
void ExpandedToggled(const bool expanded);
|
||||
|
||||
private:
|
||||
Data data_;
|
||||
CollapsibleInfoHeader *header_;
|
||||
};
|
||||
|
||||
#endif // COLLAPSIBLEINFOPANE_H
|
||||
@@ -1,89 +0,0 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* 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 <QWidget>
|
||||
#include <QApplication>
|
||||
#include <QMenu>
|
||||
#include <QWheelEvent>
|
||||
#include <QRegularExpression>
|
||||
#include <QtDebug>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "infotextview.h"
|
||||
|
||||
InfoTextView::InfoTextView(QWidget *parent) : QTextBrowser(parent), last_width_(-1), recursion_filter_(false) {
|
||||
|
||||
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
setOpenExternalLinks(true);
|
||||
|
||||
}
|
||||
|
||||
void InfoTextView::resizeEvent(QResizeEvent *e) {
|
||||
|
||||
const int w = qMax(100, width());
|
||||
if (w == last_width_) return;
|
||||
last_width_ = w;
|
||||
|
||||
document()->setTextWidth(w);
|
||||
setMinimumHeight(document()->size().height());
|
||||
|
||||
QTextBrowser::resizeEvent(e);
|
||||
|
||||
}
|
||||
|
||||
QSize InfoTextView::sizeHint() const { return minimumSize(); }
|
||||
|
||||
void InfoTextView::wheelEvent(QWheelEvent *e) { e->ignore(); }
|
||||
|
||||
void InfoTextView::SetHtml(const QString &html) {
|
||||
|
||||
QString copy(html.trimmed());
|
||||
|
||||
// Simplify newlines
|
||||
copy.replace(QRegularExpression(QStringLiteral("\\r\\n?")), QStringLiteral("\n"));
|
||||
|
||||
// Convert two or more newlines to <p>, convert single newlines to <br>
|
||||
copy.replace(QRegularExpression(QStringLiteral("([^>])([\\t ]*\\n){2,}")), QStringLiteral("\\1<p>"));
|
||||
copy.replace(QRegularExpression(QStringLiteral("([^>])[\\t ]*\\n")), QStringLiteral("\\1<br>"));
|
||||
|
||||
// Strip any newlines from the end
|
||||
copy.replace(QRegularExpression(QStringLiteral("((<\\s*br\\s*/?\\s*>)|(<\\s*/?\\s*p\\s*/?\\s*>))+$")), QLatin1String(""));
|
||||
|
||||
setHtml(copy);
|
||||
|
||||
}
|
||||
|
||||
// Prevents QTextDocument from trying to load remote images before they are ready.
|
||||
QVariant InfoTextView::loadResource(int type, const QUrl &name) {
|
||||
|
||||
if (recursion_filter_) {
|
||||
recursion_filter_ = false;
|
||||
return QVariant();
|
||||
}
|
||||
recursion_filter_ = true;
|
||||
if (type == QTextDocument::ImageResource && name.scheme() == QLatin1String("http")) {
|
||||
if (document()->resource(type, name).isNull()) {
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
return QTextBrowser::loadResource(type, name);
|
||||
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* 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 INFOTEXTVIEW_H
|
||||
#define INFOTEXTVIEW_H
|
||||
|
||||
#include <QTextBrowser>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
class QResizeEvent;
|
||||
class QWheelEvent;
|
||||
|
||||
class InfoTextView : public QTextBrowser {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit InfoTextView(QWidget *parent = nullptr);
|
||||
|
||||
QSize sizeHint() const override;
|
||||
|
||||
public Q_SLOTS:
|
||||
void SetHtml(const QString &html);
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
void wheelEvent(QWheelEvent *e) override;
|
||||
QVariant loadResource(int type, const QUrl &name) override;
|
||||
|
||||
private:
|
||||
int last_width_;
|
||||
bool recursion_filter_;
|
||||
};
|
||||
|
||||
#endif // INFOTEXTVIEW_H
|
||||
@@ -1,257 +0,0 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* 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 <QApplication>
|
||||
#include <QWindow>
|
||||
#include <QScreen>
|
||||
#include <QtConcurrentRun>
|
||||
#include <QFuture>
|
||||
#include <QString>
|
||||
#include <QImage>
|
||||
#include <QPixmap>
|
||||
#include <QPainter>
|
||||
#include <QFileInfo>
|
||||
#include <QDir>
|
||||
#include <QFileDialog>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QSettings>
|
||||
#include <QLabel>
|
||||
#include <QMenu>
|
||||
#include <QScrollArea>
|
||||
#include <QContextMenuEvent>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/networkaccessmanager.h"
|
||||
#include "core/iconloader.h"
|
||||
|
||||
#include "prettyimage.h"
|
||||
|
||||
const int PrettyImage::kTotalHeight = 200;
|
||||
const int PrettyImage::kReflectionHeight = 40;
|
||||
const int PrettyImage::kImageHeight = PrettyImage::kTotalHeight - PrettyImage::kReflectionHeight;
|
||||
|
||||
const int PrettyImage::kMaxImageWidth = 300;
|
||||
|
||||
const char *PrettyImage::kSettingsGroup = "PrettyImageView";
|
||||
|
||||
PrettyImage::PrettyImage(const QUrl &url, QNetworkAccessManager *network, QWidget *parent)
|
||||
: QWidget(parent),
|
||||
network_(network),
|
||||
state_(State_WaitingForLazyLoad),
|
||||
url_(url),
|
||||
menu_(nullptr) {
|
||||
|
||||
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
|
||||
|
||||
LazyLoad();
|
||||
|
||||
}
|
||||
|
||||
void PrettyImage::LazyLoad() {
|
||||
|
||||
if (state_ != State_WaitingForLazyLoad) return;
|
||||
|
||||
// Start fetching the image
|
||||
QNetworkReply *reply = network_->get(QNetworkRequest(url_));
|
||||
state_ = State_Fetching;
|
||||
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { ImageFetched(reply); });
|
||||
|
||||
}
|
||||
|
||||
QSize PrettyImage::image_size() const {
|
||||
|
||||
if (state_ != State_Finished) return QSize(kImageHeight * 1.6, kImageHeight);
|
||||
|
||||
QSize ret = image_.size();
|
||||
ret.scale(kMaxImageWidth, kImageHeight, Qt::KeepAspectRatio);
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
QSize PrettyImage::sizeHint() const {
|
||||
return QSize(image_size().width(), kTotalHeight);
|
||||
}
|
||||
|
||||
void PrettyImage::ImageFetched(QNetworkReply *reply) {
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
QImage image = QImage::fromData(reply->readAll());
|
||||
if (image.isNull()) {
|
||||
qLog(Debug) << "Image failed to load" << reply->request().url() << reply->error();
|
||||
deleteLater();
|
||||
}
|
||||
else {
|
||||
state_ = State_CreatingThumbnail;
|
||||
image_ = image;
|
||||
(void)QtConcurrent::run([=]{ ImageScaled(image_.scaled(image_size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PrettyImage::ImageScaled(QImage image) {
|
||||
|
||||
thumbnail_ = QPixmap::fromImage(image);
|
||||
state_ = State_Finished;
|
||||
|
||||
updateGeometry();
|
||||
update();
|
||||
emit Loaded();
|
||||
|
||||
}
|
||||
|
||||
void PrettyImage::paintEvent(QPaintEvent*) {
|
||||
|
||||
// Draw at the bottom of our area
|
||||
QRect image_rect(QPoint(0, 0), image_size());
|
||||
image_rect.moveBottom(kImageHeight);
|
||||
|
||||
QPainter p(this);
|
||||
|
||||
// Draw the main image
|
||||
DrawThumbnail(&p, image_rect);
|
||||
|
||||
// Draw the reflection
|
||||
// Figure out where to draw it
|
||||
QRect reflection_rect(image_rect);
|
||||
reflection_rect.moveTop(image_rect.bottom());
|
||||
|
||||
// Create the reflected pixmap
|
||||
QImage reflection(reflection_rect.size(), QImage::Format_ARGB32_Premultiplied);
|
||||
reflection.fill(palette().color(QPalette::Base).rgba());
|
||||
QPainter reflection_painter(&reflection);
|
||||
|
||||
// Set up the transformation
|
||||
QTransform transform;
|
||||
transform.scale(1.0, -1.0);
|
||||
transform.translate(0.0, -reflection_rect.height());
|
||||
reflection_painter.setTransform(transform);
|
||||
|
||||
QRect fade_rect(reflection.rect().bottomLeft() - QPoint(0, kReflectionHeight), reflection.rect().bottomRight());
|
||||
|
||||
// Draw the reflection into the buffer
|
||||
DrawThumbnail(&reflection_painter, reflection.rect());
|
||||
|
||||
// Make it fade out towards the bottom
|
||||
QLinearGradient fade_gradient(fade_rect.topLeft(), fade_rect.bottomLeft());
|
||||
fade_gradient.setColorAt(0.0, QColor(0, 0, 0, 0));
|
||||
fade_gradient.setColorAt(1.0, QColor(0, 0, 0, 128));
|
||||
|
||||
reflection_painter.setCompositionMode(QPainter::CompositionMode_DestinationIn);
|
||||
reflection_painter.fillRect(fade_rect, fade_gradient);
|
||||
|
||||
reflection_painter.end();
|
||||
|
||||
// Draw the reflection on the image
|
||||
p.drawImage(reflection_rect, reflection);
|
||||
|
||||
}
|
||||
|
||||
void PrettyImage::DrawThumbnail(QPainter *p, const QRect &rect) {
|
||||
|
||||
switch (state_) {
|
||||
case State_WaitingForLazyLoad:
|
||||
case State_Fetching:
|
||||
case State_CreatingThumbnail:
|
||||
p->setPen(palette().color(QPalette::Disabled, QPalette::Text));
|
||||
p->drawText(rect, Qt::AlignHCenter | Qt::AlignBottom, tr("Loading..."));
|
||||
break;
|
||||
|
||||
case State_Finished:
|
||||
p->drawPixmap(rect, thumbnail_);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PrettyImage::contextMenuEvent(QContextMenuEvent *e) {
|
||||
|
||||
if (e->pos().y() >= kImageHeight) return;
|
||||
|
||||
if (!menu_) {
|
||||
menu_ = new QMenu(this);
|
||||
menu_->addAction(IconLoader::Load(QStringLiteral("zoom-in")), tr("Show fullsize..."), this, &PrettyImage::ShowFullsize);
|
||||
menu_->addAction(IconLoader::Load(QStringLiteral("document-save")), tr("Save image") + QLatin1String("..."), this, &PrettyImage::SaveAs);
|
||||
}
|
||||
|
||||
menu_->popup(e->globalPos());
|
||||
|
||||
}
|
||||
|
||||
void PrettyImage::ShowFullsize() {
|
||||
|
||||
// Create the window
|
||||
QScrollArea *pwindow = new QScrollArea;
|
||||
pwindow->setAttribute(Qt::WA_DeleteOnClose, true);
|
||||
pwindow->setWindowTitle(tr("%1 image viewer").arg(QLatin1String("Strawberry")));
|
||||
|
||||
// Work out how large to make the window, based on the size of the screen
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
|
||||
QScreen *screen = QWidget::screen();
|
||||
#else
|
||||
QScreen *screen = (window() && window()->windowHandle() ? window()->windowHandle()->screen() : QGuiApplication::primaryScreen());
|
||||
#endif
|
||||
if (screen) {
|
||||
QRect desktop_rect(screen->availableGeometry());
|
||||
QSize window_size(qMin(desktop_rect.width() - 20, image_.width()), qMin(desktop_rect.height() - 20, image_.height()));
|
||||
pwindow->resize(window_size);
|
||||
}
|
||||
|
||||
// Create the label that displays the image
|
||||
QLabel *label = new QLabel(pwindow);
|
||||
label->setPixmap(QPixmap::fromImage(image_));
|
||||
|
||||
// Show the label in the window
|
||||
pwindow->setWidget(label);
|
||||
pwindow->setFrameShape(QFrame::NoFrame);
|
||||
pwindow->show();
|
||||
|
||||
}
|
||||
|
||||
void PrettyImage::SaveAs() {
|
||||
|
||||
QString filename = QFileInfo(url_.path()).fileName();
|
||||
|
||||
if (filename.isEmpty()) filename = QLatin1String("artwork.jpg");
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
QString last_save_dir = s.value("last_save_dir", QDir::homePath()).toString();
|
||||
|
||||
QString path = last_save_dir.isEmpty() ? QDir::homePath() : last_save_dir;
|
||||
QFileInfo path_info(path);
|
||||
if (path_info.isDir()) {
|
||||
path += QLatin1Char('/') + filename;
|
||||
}
|
||||
else {
|
||||
path = path_info.path() + QLatin1Char('/') + filename;
|
||||
}
|
||||
|
||||
filename = QFileDialog::getSaveFileName(this, tr("Save image"), path);
|
||||
if (filename.isEmpty()) return;
|
||||
|
||||
image_.save(filename);
|
||||
|
||||
s.setValue("last_save_dir", last_save_dir);
|
||||
|
||||
s.endGroup();
|
||||
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* 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 PRETTYIMAGE_H
|
||||
#define PRETTYIMAGE_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QImage>
|
||||
#include <QPixmap>
|
||||
#include <QPainter>
|
||||
|
||||
class QMenu;
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
class QContextMenuEvent;
|
||||
class QPaintEvent;
|
||||
|
||||
class PrettyImage : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PrettyImage(const QUrl &url, QNetworkAccessManager *network, QWidget *parent = nullptr);
|
||||
|
||||
static const int kTotalHeight;
|
||||
static const int kReflectionHeight;
|
||||
static const int kImageHeight;
|
||||
|
||||
static const int kMaxImageWidth;
|
||||
|
||||
static const char *kSettingsGroup;
|
||||
|
||||
QSize sizeHint() const override;
|
||||
QSize image_size() const;
|
||||
|
||||
signals:
|
||||
void Loaded();
|
||||
|
||||
public slots:
|
||||
void LazyLoad();
|
||||
void SaveAs();
|
||||
void ShowFullsize();
|
||||
|
||||
protected:
|
||||
void contextMenuEvent(QContextMenuEvent*) override;
|
||||
void paintEvent(QPaintEvent*) override;
|
||||
|
||||
private slots:
|
||||
void ImageFetched(QNetworkReply *reply);
|
||||
void ImageScaled(QImage image);
|
||||
|
||||
private:
|
||||
enum State {
|
||||
State_WaitingForLazyLoad,
|
||||
State_Fetching,
|
||||
State_CreatingThumbnail,
|
||||
State_Finished,
|
||||
};
|
||||
|
||||
void DrawThumbnail(QPainter *p, const QRect &rect);
|
||||
|
||||
private:
|
||||
QNetworkAccessManager *network_;
|
||||
State state_;
|
||||
QUrl url_;
|
||||
|
||||
QImage image_;
|
||||
QPixmap thumbnail_;
|
||||
|
||||
QMenu *menu_;
|
||||
QString last_save_dir_;
|
||||
};
|
||||
|
||||
#endif // PRETTYIMAGE_H
|
||||
@@ -1,189 +0,0 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* 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 <QObject>
|
||||
#include <QUrl>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QPropertyAnimation>
|
||||
#include <QAbstractSlider>
|
||||
#include <QHBoxLayout>
|
||||
#include <QScrollArea>
|
||||
#include <QScrollBar>
|
||||
#include <QTimer>
|
||||
#include <QMouseEvent>
|
||||
#include <QResizeEvent>
|
||||
#include <QWheelEvent>
|
||||
|
||||
#include "core/networkaccessmanager.h"
|
||||
|
||||
#include "prettyimage.h"
|
||||
#include "prettyimageview.h"
|
||||
|
||||
PrettyImageView::PrettyImageView(QNetworkAccessManager *network, QWidget* parent)
|
||||
: QScrollArea(parent),
|
||||
network_(network),
|
||||
container_(new QWidget(this)),
|
||||
layout_(new QHBoxLayout(container_)),
|
||||
current_index_(-1),
|
||||
scroll_animation_(new QPropertyAnimation(horizontalScrollBar(), "value", this)),
|
||||
recursion_filter_(false) {
|
||||
|
||||
setWidget(container_);
|
||||
setWidgetResizable(true);
|
||||
setMinimumHeight(PrettyImage::kTotalHeight + 10);
|
||||
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
setFrameShape(QFrame::NoFrame);
|
||||
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
|
||||
scroll_animation_->setDuration(250);
|
||||
scroll_animation_->setEasingCurve(QEasingCurve::InOutCubic);
|
||||
connect(horizontalScrollBar(), SIGNAL(sliderReleased()), SLOT(ScrollBarReleased()));
|
||||
connect(horizontalScrollBar(), SIGNAL(actionTriggered(int)), SLOT(ScrollBarAction(int)));
|
||||
|
||||
layout_->setSizeConstraint(QLayout::SetMinAndMaxSize);
|
||||
layout_->setContentsMargins(6, 6, 6, 6);
|
||||
layout_->setSpacing(6);
|
||||
layout_->addSpacing(200);
|
||||
layout_->addSpacing(200);
|
||||
|
||||
container_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
|
||||
|
||||
}
|
||||
|
||||
bool PrettyImageView::eventFilter(QObject *obj, QEvent *event) {
|
||||
|
||||
// Work around infinite recursion in QScrollArea resizes.
|
||||
if (recursion_filter_) {
|
||||
return false;
|
||||
}
|
||||
recursion_filter_ = true;
|
||||
bool ret = QScrollArea::eventFilter(obj, event);
|
||||
recursion_filter_ = false;
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
void PrettyImageView::AddImage(const QUrl &url) {
|
||||
|
||||
PrettyImage *image = new PrettyImage(url, network_, container_);
|
||||
connect(image, SIGNAL(destroyed()), SLOT(ScrollToCurrent()));
|
||||
connect(image, SIGNAL(Loaded()), SLOT(ScrollToCurrent()));
|
||||
|
||||
layout_->insertWidget(layout_->count() - 1, image);
|
||||
if (current_index_ == -1) ScrollTo(0);
|
||||
|
||||
}
|
||||
|
||||
void PrettyImageView::mouseReleaseEvent(QMouseEvent *e) {
|
||||
|
||||
// Find the image that was clicked on
|
||||
QWidget *widget = container_->childAt(container_->mapFrom(this, e->pos()));
|
||||
if (!widget) return;
|
||||
|
||||
// Get the index of that image
|
||||
const int index = layout_->indexOf(widget) - 1;
|
||||
if (index == -1) return;
|
||||
|
||||
if (index == current_index_) {
|
||||
// Show the image fullsize
|
||||
PrettyImage* pretty_image = qobject_cast<PrettyImage*>(widget);
|
||||
if (pretty_image) {
|
||||
pretty_image->ShowFullsize();
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Scroll to the image
|
||||
ScrollTo(index);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PrettyImageView::ScrollTo(const int index, const bool smooth) {
|
||||
|
||||
current_index_ = qBound(0, index, layout_->count() - 3);
|
||||
const int layout_index = current_index_ + 1;
|
||||
|
||||
const QWidget *target_widget = layout_->itemAt(layout_index)->widget();
|
||||
if (!target_widget) return;
|
||||
|
||||
const int current_x = horizontalScrollBar()->value();
|
||||
const int target_x = target_widget->geometry().center().x() - width() / 2;
|
||||
|
||||
if (current_x == target_x) return;
|
||||
|
||||
if (smooth) {
|
||||
scroll_animation_->setStartValue(current_x);
|
||||
scroll_animation_->setEndValue(target_x);
|
||||
scroll_animation_->start();
|
||||
}
|
||||
else {
|
||||
scroll_animation_->stop();
|
||||
horizontalScrollBar()->setValue(target_x);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PrettyImageView::ScrollToCurrent() { ScrollTo(current_index_); }
|
||||
|
||||
void PrettyImageView::ScrollBarReleased() {
|
||||
|
||||
// Find the nearest widget to where the scroll bar was released
|
||||
const int current_x = horizontalScrollBar()->value() + width() / 2;
|
||||
int layout_index = 1;
|
||||
for (; layout_index < layout_->count() - 1; ++layout_index) {
|
||||
const QWidget *widget = layout_->itemAt(layout_index)->widget();
|
||||
if (widget && widget->geometry().right() > current_x) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ScrollTo(layout_index - 1);
|
||||
|
||||
}
|
||||
|
||||
void PrettyImageView::ScrollBarAction(const int action) {
|
||||
|
||||
switch (action) {
|
||||
case QAbstractSlider::SliderSingleStepAdd:
|
||||
case QAbstractSlider::SliderPageStepAdd:
|
||||
ScrollTo(current_index_ + 1);
|
||||
break;
|
||||
|
||||
case QAbstractSlider::SliderSingleStepSub:
|
||||
case QAbstractSlider::SliderPageStepSub:
|
||||
ScrollTo(current_index_ - 1);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PrettyImageView::resizeEvent(QResizeEvent *e) {
|
||||
|
||||
QScrollArea::resizeEvent(e);
|
||||
ScrollTo(current_index_, false);
|
||||
|
||||
}
|
||||
|
||||
void PrettyImageView::wheelEvent(QWheelEvent *e) {
|
||||
|
||||
const int d = e->angleDelta().x() > 0 ? -1 : 1;
|
||||
ScrollTo(current_index_ + d, true);
|
||||
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* 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 PRETTYIMAGEVIEW_H
|
||||
#define PRETTYIMAGEVIEW_H
|
||||
|
||||
#include <QScrollArea>
|
||||
#include <QMap>
|
||||
#include <QUrl>
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
class QMenu;
|
||||
class QHBoxLayout;
|
||||
class QPropertyAnimation;
|
||||
class QTimeLine;
|
||||
class QMouseEvent;
|
||||
class QResizeEvent;
|
||||
class QWheelEvent;
|
||||
|
||||
class PrettyImageView : public QScrollArea {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PrettyImageView(QNetworkAccessManager *network, QWidget *parent = nullptr);
|
||||
|
||||
static const char* kSettingsGroup;
|
||||
|
||||
public Q_SLOTS:
|
||||
void AddImage(const QUrl& url);
|
||||
|
||||
protected:
|
||||
void mouseReleaseEvent(QMouseEvent*) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
void wheelEvent(QWheelEvent *e) override;
|
||||
|
||||
private Q_SLOTS:
|
||||
void ScrollBarReleased();
|
||||
void ScrollBarAction(const int action);
|
||||
void ScrollTo(const int index, const bool smooth = true);
|
||||
void ScrollToCurrent();
|
||||
|
||||
private:
|
||||
bool eventFilter(QObject*, QEvent*) override;
|
||||
|
||||
QNetworkAccessManager *network_;
|
||||
|
||||
QWidget *container_;
|
||||
QHBoxLayout *layout_;
|
||||
|
||||
int current_index_;
|
||||
QPropertyAnimation *scroll_animation_;
|
||||
|
||||
bool recursion_filter_;
|
||||
};
|
||||
|
||||
#endif // PRETTYIMAGEVIEW_H
|
||||
@@ -1,187 +0,0 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* 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 <QObject>
|
||||
#include <QWidget>
|
||||
#include <QImage>
|
||||
#include <QPixmap>
|
||||
#include <QPainter>
|
||||
#include <QTimeLine>
|
||||
#include <QResizeEvent>
|
||||
#include <QEvent>
|
||||
|
||||
#include "widgetfadehelper.h"
|
||||
#include "core/qt_blurimage.h"
|
||||
|
||||
const int WidgetFadeHelper::kLoadingPadding = 9;
|
||||
const int WidgetFadeHelper::kLoadingBorderRadius = 10;
|
||||
|
||||
WidgetFadeHelper::WidgetFadeHelper(QWidget *parent, const int msec)
|
||||
: QWidget(parent),
|
||||
parent_(parent),
|
||||
blur_timeline_(new QTimeLine(msec, this)),
|
||||
fade_timeline_(new QTimeLine(msec, this)) {
|
||||
|
||||
parent->installEventFilter(this);
|
||||
|
||||
connect(blur_timeline_, SIGNAL(valueChanged(qreal)), SLOT(update()));
|
||||
connect(fade_timeline_, SIGNAL(valueChanged(qreal)), SLOT(update()));
|
||||
connect(fade_timeline_, SIGNAL(finished()), SLOT(FadeFinished()));
|
||||
|
||||
hide();
|
||||
|
||||
}
|
||||
|
||||
bool WidgetFadeHelper::eventFilter(QObject *obj, QEvent *event) {
|
||||
|
||||
// We're only interested in our parent's resize events
|
||||
if (obj != parent_ || event->type() != QEvent::Resize) return false;
|
||||
|
||||
// Don't care if we're hidden
|
||||
if (!isVisible()) return false;
|
||||
|
||||
QResizeEvent *re = static_cast<QResizeEvent*>(event);
|
||||
if (re->oldSize() == re->size()) {
|
||||
// Ignore phoney resize events
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get a new capture of the parent
|
||||
hide();
|
||||
CaptureParent();
|
||||
show();
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
void WidgetFadeHelper::StartBlur() {
|
||||
|
||||
CaptureParent();
|
||||
|
||||
// Cover the parent
|
||||
raise();
|
||||
show();
|
||||
|
||||
// Start the timeline
|
||||
blur_timeline_->stop();
|
||||
blur_timeline_->start();
|
||||
|
||||
setAttribute(Qt::WA_TransparentForMouseEvents, false);
|
||||
|
||||
}
|
||||
|
||||
void WidgetFadeHelper::CaptureParent() {
|
||||
|
||||
// Take a "screenshot" of the window
|
||||
original_pixmap_ = parent_->grab();
|
||||
QImage original_image = original_pixmap_.toImage();
|
||||
|
||||
// Blur it
|
||||
QImage blurred(original_image.size(), QImage::Format_ARGB32_Premultiplied);
|
||||
blurred.fill(Qt::transparent);
|
||||
|
||||
QPainter blur_painter(&blurred);
|
||||
blur_painter.save();
|
||||
qt_blurImage(&blur_painter, original_image, 10.0, true, false);
|
||||
blur_painter.restore();
|
||||
|
||||
// Draw some loading text over the top
|
||||
QFont loading_font(font());
|
||||
loading_font.setBold(true);
|
||||
QFontMetrics loading_font_metrics(loading_font);
|
||||
|
||||
const QString loading_text = tr("Loading...");
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
|
||||
const QSize loading_size(kLoadingPadding * 2 + loading_font_metrics.horizontalAdvance(loading_text), kLoadingPadding * 2 + loading_font_metrics.height());
|
||||
#else
|
||||
const QSize loading_size(kLoadingPadding * 2 + loading_font_metrics.width(loading_text), kLoadingPadding * 2 + loading_font_metrics.height());
|
||||
#endif
|
||||
const QRect loading_rect((blurred.width() - loading_size.width()) / 2, 100, loading_size.width(), loading_size.height());
|
||||
|
||||
blur_painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
blur_painter.translate(0.5, 0.5);
|
||||
blur_painter.setPen(QColor(200, 200, 200, 255));
|
||||
blur_painter.setBrush(QColor(200, 200, 200, 192));
|
||||
blur_painter.drawRoundedRect(loading_rect, kLoadingBorderRadius, kLoadingBorderRadius);
|
||||
|
||||
blur_painter.setPen(palette().brush(QPalette::Text).color());
|
||||
blur_painter.setFont(loading_font);
|
||||
blur_painter.drawText(loading_rect.translated(-1, -1), Qt::AlignCenter, loading_text);
|
||||
blur_painter.translate(-0.5, -0.5);
|
||||
|
||||
blur_painter.end();
|
||||
|
||||
blurred_pixmap_ = QPixmap::fromImage(blurred);
|
||||
|
||||
resize(parent_->size());
|
||||
|
||||
}
|
||||
|
||||
void WidgetFadeHelper::StartFade() {
|
||||
|
||||
if (blur_timeline_->state() == QTimeLine::Running) {
|
||||
// Blur timeline is still running, so we need render the current state
|
||||
// into a new pixmap.
|
||||
QPixmap pixmap(original_pixmap_);
|
||||
QPainter painter(&pixmap);
|
||||
painter.setOpacity(blur_timeline_->currentValue());
|
||||
painter.drawPixmap(0, 0, blurred_pixmap_);
|
||||
painter.end();
|
||||
blurred_pixmap_ = pixmap;
|
||||
}
|
||||
blur_timeline_->stop();
|
||||
original_pixmap_ = QPixmap();
|
||||
|
||||
// Start the timeline
|
||||
fade_timeline_->stop();
|
||||
fade_timeline_->start();
|
||||
|
||||
setAttribute(Qt::WA_TransparentForMouseEvents, true);
|
||||
|
||||
}
|
||||
|
||||
void WidgetFadeHelper::paintEvent(QPaintEvent *event) {
|
||||
|
||||
Q_UNUSED(event)
|
||||
|
||||
QPainter p(this);
|
||||
|
||||
if (fade_timeline_->state() != QTimeLine::Running) {
|
||||
// We're fading in the blur
|
||||
p.drawPixmap(0, 0, original_pixmap_);
|
||||
p.setOpacity(blur_timeline_->currentValue());
|
||||
}
|
||||
else {
|
||||
// Fading out the blur into the new image
|
||||
p.setOpacity(1.0 - fade_timeline_->currentValue());
|
||||
}
|
||||
|
||||
p.drawPixmap(0, 0, blurred_pixmap_);
|
||||
|
||||
}
|
||||
|
||||
void WidgetFadeHelper::FadeFinished() {
|
||||
|
||||
hide();
|
||||
original_pixmap_ = QPixmap();
|
||||
blurred_pixmap_ = QPixmap();
|
||||
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
*
|
||||
* 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 WIDGETFADEHELPER_H
|
||||
#define WIDGETFADEHELPER_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QPixmap>
|
||||
|
||||
class QTimeLine;
|
||||
class QPaintEvent;
|
||||
class QEvent;
|
||||
|
||||
class WidgetFadeHelper : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
WidgetFadeHelper(QWidget *parent, const int msec = 500);
|
||||
|
||||
public Q_SLOTS:
|
||||
void StartBlur();
|
||||
void StartFade();
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
|
||||
private Q_SLOTS:
|
||||
void FadeFinished();
|
||||
|
||||
private:
|
||||
void CaptureParent();
|
||||
|
||||
private:
|
||||
static const int kLoadingPadding;
|
||||
static const int kLoadingBorderRadius;
|
||||
|
||||
QWidget *parent_;
|
||||
QTimeLine *blur_timeline_;
|
||||
QTimeLine *fade_timeline_;
|
||||
|
||||
QPixmap original_pixmap_;
|
||||
QPixmap blurred_pixmap_;
|
||||
};
|
||||
|
||||
#endif // WIDGETFADEHELPER_H
|
||||
Reference in New Issue
Block a user