Compare commits
12 Commits
d28108f2e5
...
0.1.0
Author | SHA1 | Date | |
---|---|---|---|
54c604ee9d | |||
a2b9f94015 | |||
4b57dda342 | |||
04ed6d435c | |||
618a350e0d | |||
b88ee1d0f1 | |||
2a92f4ea7f | |||
f4374a0768 | |||
b01dfe17fd | |||
25eed8066b | |||
8ac558ebc6 | |||
a910e85d97 |
8
.github/workflows/windows.yml
vendored
8
.github/workflows/windows.yml
vendored
@ -9,7 +9,7 @@ jobs:
|
||||
matrix:
|
||||
vs: ['2022']
|
||||
msvc_arch: ['x64']
|
||||
qt_ver: ['6.7.2']
|
||||
qt_ver: ['6.7.3']
|
||||
|
||||
runs-on: windows-2022
|
||||
|
||||
@ -33,6 +33,12 @@ jobs:
|
||||
:: ------ dep ------
|
||||
set CMAKE_PREFIX_PATH=%PWD%/dependencies_bin
|
||||
mkdir dependencies_src
|
||||
:: ===== uchardet =====
|
||||
echo ::group::build uchardet
|
||||
git clone -q https://gitlab.freedesktop.org/BLumia/uchardet.git --branch msvc dependencies_src/uchardet
|
||||
cmake .\dependencies_src\uchardet -Bbuild_dependencies/uchardet -DBUILD_BINARY=OFF -DCMAKE_INSTALL_PREFIX="dependencies_bin" || goto :error
|
||||
cmake --build build_dependencies/uchardet --config Release --target=install -j || goto :error
|
||||
echo ::endgroup::
|
||||
:: ===== pkg-config =====
|
||||
choco install pkgconfiglite
|
||||
set PKG_CONFIG_PATH=%PWD%/dependencies_bin/lib/pkgconfig
|
||||
|
@ -3,6 +3,7 @@ cmake_minimum_required(VERSION 3.12)
|
||||
project(pineapple-music LANGUAGES CXX)
|
||||
|
||||
include (GNUInstallDirs)
|
||||
include (FeatureSummary)
|
||||
|
||||
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
||||
|
||||
@ -13,7 +14,8 @@ set(CMAKE_AUTORCC ON)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
find_package(Qt6 6.5.1 COMPONENTS Widgets Multimedia Network LinguistTools REQUIRED)
|
||||
find_package(Qt6 6.6 COMPONENTS Widgets Multimedia Network LinguistTools REQUIRED)
|
||||
find_package(uchardet)
|
||||
find_package(PkgConfig)
|
||||
|
||||
if (PKG_CONFIG_FOUND)
|
||||
@ -26,6 +28,8 @@ set (PMUSIC_CPP_FILES
|
||||
seekableslider.cpp
|
||||
playlistmanager.cpp
|
||||
singleapplicationmanager.cpp
|
||||
lrcbar.cpp
|
||||
lyricsmanager.cpp
|
||||
)
|
||||
|
||||
set (PMUSIC_HEADER_FILES
|
||||
@ -33,6 +37,8 @@ set (PMUSIC_HEADER_FILES
|
||||
seekableslider.h
|
||||
playlistmanager.h
|
||||
singleapplicationmanager.h
|
||||
lrcbar.h
|
||||
lyricsmanager.h
|
||||
)
|
||||
|
||||
set (PMUSIC_UI_FILES
|
||||
@ -45,7 +51,7 @@ set (EXE_NAME pmusic)
|
||||
file (GLOB PMUSIC_TS_FILES languages/*.ts)
|
||||
set (PMUSIC_CPP_FILES_FOR_I18N ${PMUSIC_CPP_FILES} ${PMUSIC_UI_FILES})
|
||||
|
||||
add_executable(${EXE_NAME}
|
||||
add_executable(${EXE_NAME} WIN32
|
||||
${PMUSIC_HEADER_FILES}
|
||||
${PMUSIC_CPP_FILES}
|
||||
${PMUSIC_UI_FILES}
|
||||
@ -57,25 +63,24 @@ TS_FILES
|
||||
${PMUSIC_TS_FILES}
|
||||
)
|
||||
|
||||
if (WIN32)
|
||||
target_sources(${EXE_NAME} PRIVATE assets/pineapple-music.rc)
|
||||
endif ()
|
||||
|
||||
if (NOT TagLib_FOUND)
|
||||
message (WARNING "TagLib not found!")
|
||||
target_compile_definitions(${EXE_NAME} PRIVATE
|
||||
NO_TAGLIB=1
|
||||
)
|
||||
target_compile_definitions(${EXE_NAME} PRIVATE NO_TAGLIB=1)
|
||||
else ()
|
||||
target_link_libraries(${EXE_NAME} PRIVATE PkgConfig::TagLib)
|
||||
endif ()
|
||||
|
||||
target_link_libraries(${EXE_NAME} PRIVATE Qt::Widgets Qt::Multimedia Qt::Network)
|
||||
|
||||
# Extra build settings
|
||||
if (WIN32)
|
||||
set_property (
|
||||
TARGET ${EXE_NAME}
|
||||
PROPERTY WIN32_EXECUTABLE true
|
||||
)
|
||||
if (NOT uchardet_FOUND)
|
||||
target_compile_definitions(${EXE_NAME} PRIVATE NO_UCHARDET=1)
|
||||
else ()
|
||||
target_link_libraries (${EXE_NAME} PRIVATE uchardet::libuchardet)
|
||||
endif ()
|
||||
|
||||
target_link_libraries(${EXE_NAME} PRIVATE Qt::Widgets Qt::Multimedia Qt::Network)
|
||||
|
||||
# Install settings
|
||||
if (WIN32)
|
||||
# FIXME: try to avoid install to a "bin" subfolder under windows...
|
||||
@ -86,15 +91,15 @@ elseif (UNIX)
|
||||
endif ()
|
||||
|
||||
# install icon
|
||||
install (
|
||||
install(
|
||||
FILES icons/app-icon.svg
|
||||
DESTINATION "${CMAKE_INSTALL_DATADIR}/icons/hicolor/48x48/apps"
|
||||
RENAME pineapple-music.svg
|
||||
RENAME net.blumia.pineapple-music.svg
|
||||
)
|
||||
|
||||
# install shortcut
|
||||
install (
|
||||
FILES pineapple-music.desktop
|
||||
install(
|
||||
FILES dist/net.blumia.pineapple-music.desktop
|
||||
DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/applications"
|
||||
)
|
||||
endif()
|
||||
@ -109,3 +114,5 @@ install (
|
||||
TARGETS ${EXE_NAME}
|
||||
${INSTALL_TARGETS_DEFAULT_ARGS}
|
||||
)
|
||||
|
||||
feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)
|
||||
|
37
README.md
37
README.md
@ -1,7 +1,18 @@
|
||||
_**This is a not ready to use, toy project**_
|
||||
## Read Before Use
|
||||
|
||||
Since **I** just need a simple player which *just works* right now, so I did many things in a dirty way. Don't feel so weird if you saw something I did in this project is using a bad approach.
|
||||
|
||||
### Feature Notice
|
||||
|
||||
- File format support will be limited by the [FFmpeg version that Qt 6 uses](https://doc.qt.io/qt-6/qtmultimedia-attribution-ffmpeg.html).
|
||||
- ...which if you use Qt's official binary, only contains the LGPLv2.1+ part. (already good enough, tho)
|
||||
- No music library management support and there won't be one!
|
||||
- It'll auto-load music files in the same folder of the file that you attempted to play, so organize your music files on a folder-basis.
|
||||
- Limited lyrics (`.lrc`) loading support:
|
||||
- Currently no `.tlrc` (for translated lyrics) or `.rlrc` (for romanized lyrics) support.
|
||||
- Multi-line lyrics and duplicated timestamps are not supported
|
||||
- Extensions (Walaoke and A2 extension) are not supported
|
||||
|
||||
## Build
|
||||
|
||||
Current state, we need:
|
||||
@ -13,27 +24,19 @@ Current state, we need:
|
||||
|
||||
Then we can build it with any proper c++ compiler like g++ or msvc.
|
||||
|
||||
### Linux
|
||||
Building it just requires normal cmake building steps:
|
||||
|
||||
Just normal build process as other program. Nothing special ;)
|
||||
|
||||
### Windows
|
||||
|
||||
Install the depts manually is a nightmare. I use [KDE Craft](https://community.kde.org/Craft) but MSYS2 should also works. FYI currently this project is not intended to works under Windows (it should works and I also did some simple test though).
|
||||
|
||||
### macOS
|
||||
|
||||
I don't have a mac, so no support at all.
|
||||
```shell
|
||||
$ cmake -Bbuild
|
||||
$ cmake --build build
|
||||
```
|
||||
|
||||
## Help Translation!
|
||||
|
||||
TODO: move to Codeberg's Weblate.
|
||||
[Translate this project on Codeberg's Weblate!](https://translate.codeberg.org/projects/pineapple-apps/pineapple-music/)
|
||||
|
||||
## About License
|
||||
|
||||
Since this is a toy repo, I don't spend much time about the license stuff. Currently this project use some assets and code from [ShadowPlayer](https://github.com/ShadowPower/ShadowPlayer), which have a very interesting license -- do whatever you want but cannot be used as homework -- obviously it's not a so called *free* license. I *may* do some license housecleaning works by replaceing the assets and code implementation when the code become reasonable, and the final codebase may probably released under MIT license.
|
||||
Pineapple Music as a whole is licensed under MIT license. Individual files may have a different, but compatible license.
|
||||
|
||||
Anyway here is a list of file which is in non-free state (with license: do whatever you want but cannot be used as homework):
|
||||
|
||||
- All png images inside `icons` folder.
|
||||
- seekableslider.{h,cpp}
|
||||
All *png* images inside `icons` folder are originally created by [@ShadowPower](https://github.com/ShadowPower/) for [ShadowPlayer](https://github.com/ShadowPower/ShadowPlayer). These images are licensed under [CC0](https://creativecommons.org/publicdomain/zero/1.0/legalcode) license, grant by the original author.
|
||||
|
11
appveyor.yml
11
appveyor.yml
@ -23,19 +23,24 @@ build_script:
|
||||
- mkdir 3rdparty
|
||||
- choco install ninja
|
||||
- choco install pkgconfiglite
|
||||
# build taglib
|
||||
- cd 3rdparty
|
||||
# build uchardet
|
||||
- git clone -q https://gitlab.freedesktop.org/uchardet/uchardet.git
|
||||
- cd uchardet
|
||||
- cmake -G "Ninja" . -DCMAKE_INSTALL_PREFIX=%PACKAGE_INSTALL_ROOT% -DBUILD_BINARY=OFF
|
||||
- cmake --build . --target install
|
||||
- cd %APPVEYOR_BUILD_FOLDER%
|
||||
# build taglib
|
||||
- git clone --recurse-submodules -q https://github.com/taglib/taglib.git
|
||||
- cd taglib
|
||||
- cmake -G "Ninja" . -DCMAKE_INSTALL_PREFIX=%PACKAGE_INSTALL_ROOT% -DBUILD_SHARED_LIBS=ON
|
||||
- cmake --build .
|
||||
- cmake --build . --target install
|
||||
- cd %APPVEYOR_BUILD_FOLDER%
|
||||
- tree %PACKAGE_INSTALL_ROOT% /f
|
||||
# finally...
|
||||
- mkdir build
|
||||
- cd build
|
||||
- cmake .. -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX='%cd%'
|
||||
- cmake .. -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=%PACKAGE_INSTALL_ROOT% -DCMAKE_INSTALL_PREFIX='%cd%'
|
||||
- cmake --build .
|
||||
- cmake --build . --target install
|
||||
# fixme: I don't know how to NOT make the binary installed to the ./bin/ folder...
|
||||
|
BIN
assets/icons/app-icon.ico
Normal file
BIN
assets/icons/app-icon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 62 KiB |
17
assets/pineapple-music.rc
Normal file
17
assets/pineapple-music.rc
Normal file
@ -0,0 +1,17 @@
|
||||
IDI_ICON1 ICON DISCARDABLE "icons/app-icon.ico"
|
||||
1 VERSIONINFO
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904E4"
|
||||
BEGIN
|
||||
VALUE "FileDescription", "Pineapple Music - Music Player"
|
||||
VALUE "LegalCopyright", "MIT/Expat License - Copyright (C) 2024 Gary Wang"
|
||||
VALUE "ProductName", "Pineapple Music"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x409, 1200
|
||||
END
|
||||
END
|
@ -3,7 +3,7 @@ Categories=Audio;Player;
|
||||
Comment=Pineapple Music Audio Player.
|
||||
Exec=pmusic %F
|
||||
GenericName=Music
|
||||
Icon=pineapple-music
|
||||
Icon=net.blumia.pineapple-music
|
||||
Keywords=Picture;Image;Viewer;Jpg;Jpeg;Png;
|
||||
MimeType=application/ogg;application/x-ogg;audio/ogg;audio/vorbis;audio/x-vorbis;audio/x-vorbis+ogg;video/ogg;video/x-ogm;video/x-ogm+ogg;video/x-theora+ogg;video/x-theora;audio/x-speex;audio/opus;application/x-flac;audio/flac;audio/x-flac;audio/x-ms-asf;audio/x-ms-asx;audio/x-ms-wax;audio/x-ms-wma;video/x-ms-asf;video/x-ms-asf-plugin;video/x-ms-asx;video/x-ms-wm;video/x-ms-wmv;video/x-ms-wmx;video/x-ms-wvx;video/x-msvideo;audio/x-pn-windows-acm;video/divx;video/msvideo;video/vnd.divx;video/avi;video/x-avi;application/vnd.rn-realmedia;application/vnd.rn-realmedia-vbr;audio/vnd.rn-realaudio;audio/x-pn-realaudio;audio/x-pn-realaudio-plugin;audio/x-real-audio;audio/x-realaudio;video/vnd.rn-realvideo;audio/mpeg;audio/mpg;audio/mp1;audio/mp2;audio/mp3;audio/x-mp1;audio/x-mp2;audio/x-mp3;audio/x-mpeg;audio/x-mpg;video/mp2t;video/mpeg;video/mpeg-system;video/x-mpeg;video/x-mpeg2;video/x-mpeg-system;application/mpeg4-iod;application/mpeg4-muxcodetable;application/x-extension-m4a;application/x-extension-mp4;audio/aac;audio/m4a;audio/mp4;audio/x-m4a;audio/x-aac;video/mp4;video/mp4v-es;video/x-m4v;application/x-quicktime-media-link;application/x-quicktimeplayer;video/quicktime;application/x-matroska;audio/x-matroska;video/x-matroska;video/webm;audio/webm;audio/3gpp;audio/3gpp2;audio/AMR;audio/AMR-WB;video/3gp;video/3gpp;video/3gpp2;x-scheme-handler/mms;x-scheme-handler/mmsh;x-scheme-handler/rtsp;x-scheme-handler/rtp;x-scheme-handler/rtmp;x-scheme-handler/icy;x-scheme-handler/icyx;application/x-cd-image;x-content/video-vcd;x-content/video-svcd;x-content/video-dvd;x-content/audio-cdda;x-content/audio-player;application/ram;application/xspf+xml;audio/mpegurl;audio/x-mpegurl;audio/scpls;audio/x-scpls;text/google-video-pointer;text/x-google-video-pointer;video/vnd.mpegurl;application/vnd.apple.mpegurl;application/vnd.ms-asf;application/vnd.ms-wpl;application/sdp;audio/dv;video/dv;audio/x-aiff;audio/x-pn-aiff;video/x-anim;video/x-nsv;video/fli;video/flv;video/x-flc;video/x-fli;video/x-flv;audio/wav;audio/x-pn-au;audio/x-pn-wav;audio/x-wav;audio/x-adpcm;audio/ac3;audio/eac3;audio/vnd.dts;audio/vnd.dts.hd;audio/vnd.dolby.heaac.1;audio/vnd.dolby.heaac.2;audio/vnd.dolby.mlp;audio/basic;audio/midi;audio/x-ape;audio/x-gsm;audio/x-musepack;audio/x-tta;audio/x-wavpack;audio/x-shorten;application/x-shockwave-flash;application/x-flash-video;misc/ultravox;image/vnd.rn-realpix;audio/x-it;audio/x-mod;audio/x-s3m;audio/x-xm;application/mxf;
|
||||
Name=Pineapple Music
|
BIN
icons/skin.png
Normal file
BIN
icons/skin.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
@ -1,71 +1,85 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE TS>
|
||||
<TS version="2.1">
|
||||
<context>
|
||||
<name>LrcBar</name>
|
||||
<message>
|
||||
<location filename="../lrcbar.cpp" line="88"/>
|
||||
<source>(Interlude...)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>MainWindow</name>
|
||||
<message>
|
||||
<location filename="../mainwindow.cpp" line="71"/>
|
||||
<location filename="../mainwindow.cpp" line="93"/>
|
||||
<source>Mono</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.cpp" line="73"/>
|
||||
<location filename="../mainwindow.cpp" line="95"/>
|
||||
<source>Stereo</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.cpp" line="75"/>
|
||||
<location filename="../mainwindow.cpp" line="97"/>
|
||||
<source>%1 Channels</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.cpp" line="200"/>
|
||||
<location filename="../mainwindow.cpp" line="241"/>
|
||||
<source>Select songs to play</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.cpp" line="202"/>
|
||||
<location filename="../mainwindow.cpp" line="243"/>
|
||||
<source>Audio Files</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.cpp" line="576"/>
|
||||
<source>Select image as background skin</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.cpp" line="578"/>
|
||||
<source>Image files (*.jpg *.jpeg *.png *.gif)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.ui" line="23"/>
|
||||
<source>Pineapple Player</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.ui" line="231"/>
|
||||
<source>^</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.ui" line="297"/>
|
||||
<source>No song loaded...</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.ui" line="304"/>
|
||||
<source>Drag and drop file to load</source>
|
||||
<location filename="../lrcbar.cpp" line="89"/>
|
||||
<source>Pineapple Music</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.ui" line="313"/>
|
||||
<location filename="../mainwindow.ui" line="320"/>
|
||||
<source>0:00</source>
|
||||
<source>No song loaded...</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.cpp" line="81"/>
|
||||
<location filename="../mainwindow.ui" line="325"/>
|
||||
<source>Drag and drop file to load</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.ui" line="338"/>
|
||||
<source>Lrc</source>
|
||||
<comment>Lyrics</comment>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.cpp" line="103"/>
|
||||
<source>Sample Rate: %1 Hz</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.cpp" line="86"/>
|
||||
<location filename="../mainwindow.cpp" line="108"/>
|
||||
<source>Bitrate: %1 Kbps</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.cpp" line="91"/>
|
||||
<location filename="../mainwindow.cpp" line="113"/>
|
||||
<source>Channel Count: %1</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
|
@ -1,71 +1,85 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE TS>
|
||||
<TS version="2.1" language="zh_CN">
|
||||
<context>
|
||||
<name>LrcBar</name>
|
||||
<message>
|
||||
<location filename="../lrcbar.cpp" line="88"/>
|
||||
<source>(Interlude...)</source>
|
||||
<translation>(间奏…)</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>MainWindow</name>
|
||||
<message>
|
||||
<location filename="../mainwindow.cpp" line="71"/>
|
||||
<location filename="../mainwindow.cpp" line="93"/>
|
||||
<source>Mono</source>
|
||||
<translation>单声道</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.cpp" line="73"/>
|
||||
<location filename="../mainwindow.cpp" line="95"/>
|
||||
<source>Stereo</source>
|
||||
<translation>立体声</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.cpp" line="75"/>
|
||||
<location filename="../mainwindow.cpp" line="97"/>
|
||||
<source>%1 Channels</source>
|
||||
<translation>%1 声道</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.cpp" line="200"/>
|
||||
<location filename="../mainwindow.cpp" line="241"/>
|
||||
<source>Select songs to play</source>
|
||||
<translation>选择要播放的曲目</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.cpp" line="202"/>
|
||||
<location filename="../mainwindow.cpp" line="243"/>
|
||||
<source>Audio Files</source>
|
||||
<translation>音频文件</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.cpp" line="576"/>
|
||||
<source>Select image as background skin</source>
|
||||
<translation>选择图片作为背景皮肤</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.cpp" line="578"/>
|
||||
<source>Image files (*.jpg *.jpeg *.png *.gif)</source>
|
||||
<translation>图片文件 (*.jpg *.jpeg *.png *.gif)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.ui" line="23"/>
|
||||
<source>Pineapple Player</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<location filename="../lrcbar.cpp" line="89"/>
|
||||
<source>Pineapple Music</source>
|
||||
<translation>菠萝音乐</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.ui" line="231"/>
|
||||
<source>^</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.ui" line="297"/>
|
||||
<location filename="../mainwindow.ui" line="313"/>
|
||||
<source>No song loaded...</source>
|
||||
<translation>未加载曲目...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.ui" line="304"/>
|
||||
<location filename="../mainwindow.ui" line="325"/>
|
||||
<source>Drag and drop file to load</source>
|
||||
<translation>拖放文件来播放</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.ui" line="313"/>
|
||||
<location filename="../mainwindow.ui" line="320"/>
|
||||
<source>0:00</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<location filename="../mainwindow.ui" line="338"/>
|
||||
<source>Lrc</source>
|
||||
<comment>Lyrics</comment>
|
||||
<translation>歌词</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.cpp" line="81"/>
|
||||
<location filename="../mainwindow.cpp" line="103"/>
|
||||
<source>Sample Rate: %1 Hz</source>
|
||||
<translation>采样率: %1 Hz</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.cpp" line="86"/>
|
||||
<location filename="../mainwindow.cpp" line="108"/>
|
||||
<source>Bitrate: %1 Kbps</source>
|
||||
<translation>比特率: %1 Kbps</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.cpp" line="91"/>
|
||||
<location filename="../mainwindow.cpp" line="113"/>
|
||||
<source>Channel Count: %1</source>
|
||||
<translation>声道数: %1</translation>
|
||||
</message>
|
||||
@ -75,7 +89,7 @@
|
||||
<message>
|
||||
<location filename="../main.cpp" line="28"/>
|
||||
<source>File list.</source>
|
||||
<translation>文件列表</translation>
|
||||
<translation>文件列表。</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
|
128
lrcbar.cpp
Normal file
128
lrcbar.cpp
Normal file
@ -0,0 +1,128 @@
|
||||
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "lrcbar.h"
|
||||
|
||||
#include "lyricsmanager.h"
|
||||
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QWindow>
|
||||
|
||||
LrcBar::LrcBar(QWidget *parent)
|
||||
: QWidget(parent, Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::Tool)
|
||||
, m_lrcMgr(new LyricsManager(this))
|
||||
{
|
||||
m_font.setPointSize(30);
|
||||
m_font.setStyleStrategy(QFont::PreferAntialias);
|
||||
|
||||
QSize windowSize(sizeHint());
|
||||
|
||||
QFontMetrics fm(m_font);
|
||||
m_baseLinearGradient.setColorAt(0, QColor(20, 100, 200));
|
||||
m_baseLinearGradient.setColorAt(1, QColor(0, 80, 255));
|
||||
m_baseLinearGradient.setStart(0, (windowSize.height() - fm.height()) / 2);
|
||||
m_baseLinearGradient.setFinalStop(0, (windowSize.height() + fm.height()) / 2);
|
||||
m_maskLinearGradient.setColorAt(0, QColor(255, 128, 0));
|
||||
m_maskLinearGradient.setColorAt(0.5, QColor(255, 255, 0));
|
||||
m_maskLinearGradient.setColorAt(1, QColor(255, 128, 0));
|
||||
m_maskLinearGradient.setStart(0, (windowSize.height() - fm.height()) / 2);
|
||||
m_maskLinearGradient.setFinalStop(0, (windowSize.height() + fm.height()) / 2);
|
||||
|
||||
setAttribute(Qt::WA_TranslucentBackground);
|
||||
setMouseTracking(true);
|
||||
setGeometry(QRect(QPoint((qApp->primaryScreen()->geometry().width() - windowSize.width()) / 2,
|
||||
qApp->primaryScreen()->geometry().height() - windowSize.height() - 50),
|
||||
windowSize));
|
||||
}
|
||||
|
||||
LrcBar::~LrcBar()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
bool LrcBar::loadLyrics(QString filepath)
|
||||
{
|
||||
m_currentTimeMs = 0;
|
||||
return m_lrcMgr->loadLyrics(filepath);
|
||||
}
|
||||
|
||||
void LrcBar::playbackPositionChanged(int timestampMs, int totalTimeMs)
|
||||
{
|
||||
if (!isVisible()) return;
|
||||
|
||||
m_currentTimeMs = timestampMs;
|
||||
m_lrcMgr->updateCurrentTimeMs(timestampMs, totalTimeMs);
|
||||
update();
|
||||
}
|
||||
|
||||
QSize LrcBar::sizeHint() const
|
||||
{
|
||||
return QSize(1000, 88);
|
||||
}
|
||||
|
||||
void LrcBar::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->buttons() & Qt::LeftButton) {
|
||||
window()->windowHandle()->startSystemMove();
|
||||
event->accept();
|
||||
}
|
||||
|
||||
return QWidget::mouseMoveEvent(event);
|
||||
}
|
||||
|
||||
void LrcBar::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QPainter painter(this);
|
||||
if (underMouse()) {
|
||||
painter.setBrush(QBrush(QColor(255, 255, 255, 120)));
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.drawRect(0, 0, width(), height());
|
||||
}
|
||||
painter.setFont(m_font);
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
|
||||
QString curLrc(m_lrcMgr->lyrics().trimmed());
|
||||
if (curLrc.isEmpty()) {
|
||||
curLrc = m_lrcMgr->hasLyrics() ? tr("(Interlude...)")
|
||||
: QCoreApplication::translate("MainWindow", "Pineapple Music", nullptr);
|
||||
}
|
||||
|
||||
QFontMetrics fm(m_font);
|
||||
int lrcWidth = fm.horizontalAdvance(curLrc);
|
||||
int maskWidth = lrcWidth * m_lrcMgr->maskPercent(m_currentTimeMs);
|
||||
int startOffsetX = 0;
|
||||
|
||||
if (fm.horizontalAdvance(curLrc) < width()) {
|
||||
startOffsetX = (width() - lrcWidth) / 2;
|
||||
} else {
|
||||
if (maskWidth < width() / 2) {
|
||||
startOffsetX = 0;
|
||||
} else if (lrcWidth - maskWidth < width() / 2) {
|
||||
startOffsetX = width() - lrcWidth;
|
||||
} else {
|
||||
startOffsetX = 0 - (maskWidth - width() / 2);
|
||||
}
|
||||
}
|
||||
|
||||
// shadow
|
||||
painter.setPen(QColor(0, 0, 0, 80));
|
||||
painter.drawText(startOffsetX + 2, 2, lrcWidth, this->height(), Qt::AlignVCenter, curLrc);
|
||||
// text itself
|
||||
painter.setPen(QPen(m_baseLinearGradient, 0));
|
||||
painter.drawText(startOffsetX, 0, lrcWidth, this->height(), Qt::AlignVCenter, curLrc);
|
||||
// mask
|
||||
painter.setPen(QPen(m_maskLinearGradient, 0));
|
||||
painter.drawText(startOffsetX, 0, maskWidth, this->height(), Qt::AlignVCenter, curLrc);
|
||||
}
|
||||
|
||||
void LrcBar::enterEvent(QEnterEvent *)
|
||||
{
|
||||
update();
|
||||
}
|
||||
|
||||
void LrcBar::leaveEvent(QEvent *)
|
||||
{
|
||||
update();
|
||||
}
|
36
lrcbar.h
Normal file
36
lrcbar.h
Normal file
@ -0,0 +1,36 @@
|
||||
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QLinearGradient>
|
||||
#include <QMediaPlayer>
|
||||
#include <QWidget>
|
||||
|
||||
class LyricsManager;
|
||||
class LrcBar : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit LrcBar(QWidget *parent);
|
||||
~LrcBar();
|
||||
|
||||
bool loadLyrics(QString filepath);
|
||||
void playbackPositionChanged(int timestampMs, int totalTimeMs);
|
||||
|
||||
protected:
|
||||
QSize sizeHint() const override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
void paintEvent(QPaintEvent *) override;
|
||||
void enterEvent(QEnterEvent *) override;
|
||||
void leaveEvent(QEvent *) override;
|
||||
|
||||
private:
|
||||
int m_currentTimeMs = 0;
|
||||
LyricsManager * m_lrcMgr;
|
||||
QFont m_font;
|
||||
QLinearGradient m_baseLinearGradient;
|
||||
QLinearGradient m_maskLinearGradient;
|
||||
bool m_underMouse = false;
|
||||
};
|
177
lyricsmanager.cpp
Normal file
177
lyricsmanager.cpp
Normal file
@ -0,0 +1,177 @@
|
||||
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "lyricsmanager.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QRegularExpression>
|
||||
#include <QStringConverter>
|
||||
|
||||
#ifndef NO_UCHARDET
|
||||
#include <uchardet/uchardet.h>
|
||||
#endif
|
||||
|
||||
Q_LOGGING_CATEGORY(lcLyrics, "pmusic.lyrics")
|
||||
Q_LOGGING_CATEGORY(lcLyricsParser, "pmusic.lyrics.parser")
|
||||
|
||||
LyricsManager::LyricsManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
LyricsManager::~LyricsManager()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
bool LyricsManager::loadLyrics(QString filepath)
|
||||
{
|
||||
// reset state
|
||||
reset();
|
||||
|
||||
// check and load file
|
||||
QFileInfo fileInfo(filepath);
|
||||
if (!filepath.endsWith(".lrc", Qt::CaseInsensitive)) {
|
||||
fileInfo.setFile(fileInfo.dir().filePath(fileInfo.completeBaseName() + ".lrc"));
|
||||
}
|
||||
if (!fileInfo.exists()) return false;
|
||||
|
||||
QFile file(fileInfo.absoluteFilePath());
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return false;
|
||||
}
|
||||
QByteArray fileContent(file.readAll());
|
||||
#ifndef NO_UCHARDET
|
||||
uchardet_t handle = uchardet_new();
|
||||
uchardet_handle_data(handle, fileContent.data(), fileContent.length());
|
||||
uchardet_data_end(handle);
|
||||
const char* encoding = uchardet_get_charset(handle);
|
||||
qCDebug(lcLyrics) << "Detected encoding:" << (encoding == NULL ? "unknown" : encoding);
|
||||
QStringList lines;
|
||||
if (QStringConverter::availableCodecs().contains(QString(encoding))) {
|
||||
auto toUtf16 = QStringDecoder(encoding);
|
||||
QString decodedResult = toUtf16(fileContent);
|
||||
lines = decodedResult.split('\n');
|
||||
} else {
|
||||
lines = QString(fileContent).split('\n');
|
||||
}
|
||||
uchardet_delete(handle);
|
||||
#else
|
||||
QStringList lines = QString(fileContent).split('\n');
|
||||
#endif
|
||||
file.close();
|
||||
|
||||
// parse lyrics timestamp
|
||||
QRegularExpression tagRegex(R"regex(\[(ti|ar|al|au|length|by|offset|tool|re|ve|#):\s?([^\]]*)\]$)regex");
|
||||
QRegularExpression lrcRegex(R"regex(\[(\d{2,3}:\d{2}\.\d{2,3})\](.*))regex");
|
||||
bool tagSectionPassed = false;
|
||||
|
||||
for (QString line : std::as_const(lines)) {
|
||||
line = line.trimmed();
|
||||
if (line.isEmpty()) continue;
|
||||
|
||||
if (!tagSectionPassed) {
|
||||
QRegularExpressionMatch tagMatch = tagRegex.match(line);
|
||||
if (tagMatch.hasMatch()) {
|
||||
QString tag(tagMatch.captured(1));
|
||||
if (tag == QLatin1String("offset")) {
|
||||
// The value is prefixed with either + or -, with + causing lyrics to appear sooner
|
||||
m_timeOffset = -tagMatch.captured(2).toInt();
|
||||
qCDebug(lcLyricsParser) << m_timeOffset;
|
||||
}
|
||||
qCDebug(lcLyricsParser) << "[tag]" << tagMatch.captured(1) << tagMatch.captured(2);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
QList<int> timestamps;
|
||||
QString currentLrc;
|
||||
QRegularExpressionMatch match = lrcRegex.match(line);
|
||||
while (match.hasMatch()) {
|
||||
tagSectionPassed = true;
|
||||
QTime timestamp(QTime::fromString(match.captured(1), "m:s.zz"));
|
||||
timestamps.append(timestamp.msecsSinceStartOfDay());
|
||||
currentLrc = match.captured(2);
|
||||
match = lrcRegex.match(currentLrc);
|
||||
}
|
||||
if (!timestamps.isEmpty()) {
|
||||
for (int timestamp : std::as_const(timestamps)) {
|
||||
m_lyricsMap.insert(timestamp, currentLrc);
|
||||
qCDebug(lcLyricsParser) << "[lrc]" << timestamp << currentLrc;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!m_lyricsMap.isEmpty()) {
|
||||
m_timestampList = m_lyricsMap.keys();
|
||||
std::sort(m_timestampList.begin(), m_timestampList.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool LyricsManager::hasLyrics() const
|
||||
{
|
||||
return !m_lyricsMap.isEmpty();
|
||||
}
|
||||
|
||||
void LyricsManager::updateCurrentTimeMs(int curTimeMs, int totalTimeMs)
|
||||
{
|
||||
if (!hasLyrics()) return;
|
||||
|
||||
// TODO: we don't need to find from the top everytime the time is updated
|
||||
auto iter = std::find_if(m_timestampList.begin(), m_timestampList.end(), [&curTimeMs, this](int timestamp) -> bool {
|
||||
return (timestamp + m_timeOffset) > curTimeMs;
|
||||
});
|
||||
|
||||
m_nextLyricsTime = iter == m_timestampList.end() ? totalTimeMs : *iter;
|
||||
if (iter != m_timestampList.begin()) {
|
||||
iter--;
|
||||
}
|
||||
m_currentLyricsTime = *iter;
|
||||
}
|
||||
|
||||
QString LyricsManager::lyrics(int lineOffset) const
|
||||
{
|
||||
if (!hasLyrics()) return QString();
|
||||
|
||||
int index = m_timestampList.indexOf(m_currentLyricsTime) + lineOffset;
|
||||
if (index >= 0 && index < m_timestampList.count()) {
|
||||
int timestamp = m_timestampList.at(index);
|
||||
return m_lyricsMap.value(timestamp);
|
||||
} else {
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
double LyricsManager::maskPercent(int curTimeMs)
|
||||
{
|
||||
if (!hasLyrics()) return 0;
|
||||
if (curTimeMs <= currentLyricsTime()) return 0;
|
||||
if (curTimeMs >= nextLyricsTime()) return 1;
|
||||
if (m_nextLyricsTime == currentLyricsTime()) return 1;
|
||||
|
||||
return (double)(curTimeMs - currentLyricsTime()) / (m_nextLyricsTime - m_currentLyricsTime);
|
||||
}
|
||||
|
||||
void LyricsManager::reset()
|
||||
{
|
||||
m_currentLyricsTime = 0;
|
||||
m_nextLyricsTime = 0;
|
||||
m_timeOffset = 0;
|
||||
m_lyricsMap.clear();
|
||||
m_timestampList.clear();
|
||||
}
|
||||
|
||||
int LyricsManager::currentLyricsTime() const
|
||||
{
|
||||
return m_currentLyricsTime + m_timeOffset;
|
||||
}
|
||||
|
||||
int LyricsManager::nextLyricsTime() const
|
||||
{
|
||||
return m_nextLyricsTime + m_timeOffset;
|
||||
}
|
41
lyricsmanager.h
Normal file
41
lyricsmanager.h
Normal file
@ -0,0 +1,41 @@
|
||||
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QHash>
|
||||
#include <QList>
|
||||
#include <QLoggingCategory>
|
||||
#include <QObject>
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(lcLyrics)
|
||||
Q_DECLARE_LOGGING_CATEGORY(lcLyricsParser)
|
||||
|
||||
class LyricsManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit LyricsManager(QObject *parent);
|
||||
~LyricsManager();
|
||||
|
||||
bool loadLyrics(QString filepath);
|
||||
bool hasLyrics() const;
|
||||
void updateCurrentTimeMs(int curTimeMs, int totalTimeMs);
|
||||
QString lyrics(int lineOffset = 0) const;
|
||||
double maskPercent(int curTimeMs);
|
||||
|
||||
protected:
|
||||
|
||||
|
||||
private:
|
||||
void reset();
|
||||
int currentLyricsTime() const;
|
||||
int nextLyricsTime() const;
|
||||
|
||||
QHash<int, QString> m_lyricsMap;
|
||||
QList<int> m_timestampList;
|
||||
int m_currentLyricsTime = 0;
|
||||
int m_nextLyricsTime = 0;
|
||||
int m_timeOffset = 0;
|
||||
};
|
4
main.cpp
4
main.cpp
@ -1,3 +1,7 @@
|
||||
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "mainwindow.h"
|
||||
|
||||
#include "singleapplicationmanager.h"
|
||||
|
222
mainwindow.cpp
222
mainwindow.cpp
@ -1,7 +1,12 @@
|
||||
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "mainwindow.h"
|
||||
#include "./ui_mainwindow.h"
|
||||
|
||||
#include "playlistmanager.h"
|
||||
#include "lrcbar.h"
|
||||
|
||||
// taglib
|
||||
#ifndef NO_TAGLIB
|
||||
@ -21,26 +26,39 @@
|
||||
#include <QCollator>
|
||||
#include <QMimeData>
|
||||
#include <QWindow>
|
||||
#include <QStandardPaths>
|
||||
#include <QMediaDevices>
|
||||
#include <QAudioDevice>
|
||||
#include <QMessageBox>
|
||||
#include <QStringBuilder>
|
||||
|
||||
constexpr QSize miniSize(490, 160);
|
||||
constexpr QSize fullSize(490, 420);
|
||||
|
||||
MainWindow::MainWindow(QWidget *parent)
|
||||
: QMainWindow(parent)
|
||||
, ui(new Ui::MainWindow)
|
||||
, m_mediaDevices(new QMediaDevices(this))
|
||||
, m_mediaPlayer(new QMediaPlayer(this))
|
||||
, m_audioOutput(new QAudioOutput(this))
|
||||
, m_lrcbar(new LrcBar(nullptr))
|
||||
, m_playlistManager(new PlaylistManager(this))
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
m_playlistManager->setAutoLoadFilterSuffixes({
|
||||
"*.mp3", "*.wav", "*.aiff", "*.ape", "*.flac", "*.ogg", "*.oga", "*.mpga"
|
||||
"*.mp3", "*.wav", "*.aiff", "*.ape", "*.flac", "*.ogg", "*.oga", "*.mpga", "*.aac"
|
||||
});
|
||||
m_mediaPlayer->setAudioOutput(m_audioOutput);
|
||||
m_mediaPlayer->setLoops(QMediaPlayer::Infinite);
|
||||
ui->playlistView->setModel(m_playlistManager->model());
|
||||
|
||||
this->setWindowFlags(Qt::FramelessWindowHint | Qt::WindowSystemMenuHint | Qt::WindowMinimizeButtonHint);
|
||||
this->setAttribute(Qt::WA_TranslucentBackground, true);
|
||||
ui->actionHelp->setShortcut(QKeySequence::HelpContents);
|
||||
addAction(ui->actionHelp);
|
||||
|
||||
setWindowFlags(Qt::FramelessWindowHint | Qt::WindowSystemMenuHint | Qt::WindowMinimizeButtonHint);
|
||||
setAttribute(Qt::WA_TranslucentBackground, true);
|
||||
|
||||
loadSkinData();
|
||||
initConnections();
|
||||
initUiAndAnimation();
|
||||
|
||||
@ -49,6 +67,7 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
|
||||
MainWindow::~MainWindow()
|
||||
{
|
||||
delete m_lrcbar;
|
||||
delete ui;
|
||||
}
|
||||
|
||||
@ -59,7 +78,7 @@ void MainWindow::commandlinePlayAudioFiles(QStringList audioFiles)
|
||||
if (!audioFileUrls.isEmpty()) {
|
||||
QModelIndex modelIndex = m_playlistManager->loadPlaylist(audioFileUrls);
|
||||
if (modelIndex.isValid()) {
|
||||
m_mediaPlayer->setSource(m_playlistManager->urlByIndex(modelIndex));
|
||||
loadByModelIndex(modelIndex);
|
||||
play();
|
||||
}
|
||||
}
|
||||
@ -138,10 +157,14 @@ void MainWindow::paintEvent(QPaintEvent * e)
|
||||
QPainter painter(this);
|
||||
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setRenderHint(QPainter::SmoothPixmapTransform);
|
||||
|
||||
// Temp bg
|
||||
painter.setBrush(QColor(20, 32, 83));
|
||||
painter.drawRect(0, 0, width(), height());
|
||||
if (m_skin.isNull()) {
|
||||
painter.setBrush(QColor(40, 50, 123));
|
||||
painter.drawRect(0, 0, width(), height());
|
||||
} else {
|
||||
painter.drawPixmap(0, 0, m_skin);
|
||||
}
|
||||
|
||||
painter.setBrush(QBrush(m_bgLinearGradient));
|
||||
painter.drawRect(0, 0, width(), height());
|
||||
@ -194,19 +217,33 @@ void MainWindow::dropEvent(QDropEvent *e)
|
||||
return;
|
||||
}
|
||||
|
||||
QModelIndex modelIndex = m_playlistManager->loadPlaylist(urls);
|
||||
if (fileName.endsWith(".png") || fileName.endsWith(".jpg") ||
|
||||
fileName.endsWith(".jpeg") || fileName.endsWith(".gif")) {
|
||||
setSkin(fileName, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (fileName.endsWith(".lrc")) {
|
||||
m_lrcbar->loadLyrics(fileName);
|
||||
return;
|
||||
}
|
||||
|
||||
const QModelIndex & modelIndex = m_playlistManager->loadPlaylist(urls);
|
||||
if (modelIndex.isValid()) {
|
||||
m_mediaPlayer->setSource(m_playlistManager->urlByIndex(modelIndex));
|
||||
loadByModelIndex(modelIndex);
|
||||
play();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::loadFile()
|
||||
{
|
||||
QStringList musicFolders(QStandardPaths::standardLocations(QStandardPaths::MusicLocation));
|
||||
musicFolders.append(QDir::homePath());
|
||||
QStringList files = QFileDialog::getOpenFileNames(this,
|
||||
tr("Select songs to play"),
|
||||
QDir::homePath(),
|
||||
musicFolders.first(),
|
||||
tr("Audio Files") + " (*.mp3 *.wav *.aiff *.ape *.flac *.ogg *.oga)");
|
||||
if (files.isEmpty()) return;
|
||||
QList<QUrl> urlList;
|
||||
for (const QString & fileName : files) {
|
||||
urlList.append(QUrl::fromLocalFile(fileName));
|
||||
@ -214,31 +251,28 @@ void MainWindow::loadFile()
|
||||
|
||||
m_playlistManager->loadPlaylist(urlList);
|
||||
m_mediaPlayer->setSource(urlList.first());
|
||||
m_lrcbar->loadLyrics(urlList.first().toLocalFile());
|
||||
}
|
||||
|
||||
void MainWindow::loadByModelIndex(const QModelIndex & index)
|
||||
{
|
||||
m_mediaPlayer->setSource(m_playlistManager->urlByIndex(index));
|
||||
m_lrcbar->loadLyrics(m_playlistManager->localFileByIndex(index));
|
||||
}
|
||||
|
||||
void MainWindow::play()
|
||||
{
|
||||
QUrl fileUrl(m_mediaPlayer->source());
|
||||
|
||||
m_mediaPlayer->play();
|
||||
}
|
||||
|
||||
ui->titleLabel->setText(fileUrl.fileName());
|
||||
ui->titleLabel->setToolTip(fileUrl.fileName());
|
||||
|
||||
if (fileUrl.isLocalFile()) {
|
||||
QString filePath(fileUrl.toLocalFile());
|
||||
QString suffix(filePath.mid(filePath.lastIndexOf('.') + 1));
|
||||
suffix = suffix.toUpper();
|
||||
|
||||
#ifndef NO_TAGLIB
|
||||
TagLib::FileRef fileRef(filePath.toLocal8Bit().data());
|
||||
|
||||
if (!fileRef.isNull() && fileRef.audioProperties()) {
|
||||
TagLib::AudioProperties *prop = fileRef.audioProperties();
|
||||
setAudioPropertyInfoForDisplay(prop->sampleRate(), prop->bitrate(), prop->channels(), suffix);
|
||||
}
|
||||
#endif // NO_TAGLIB
|
||||
void MainWindow::setSkin(QString imagePath, bool save)
|
||||
{
|
||||
m_skin = QPixmap(imagePath);
|
||||
if (save) {
|
||||
saveSkinData();
|
||||
}
|
||||
m_skin = m_skin.scaled(fullSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
|
||||
update();
|
||||
}
|
||||
|
||||
void MainWindow::centerWindow()
|
||||
@ -323,7 +357,7 @@ void MainWindow::on_prevBtn_clicked()
|
||||
{
|
||||
QModelIndex index(m_playlistManager->previousIndex());
|
||||
m_playlistManager->setCurrentIndex(index);
|
||||
m_mediaPlayer->setSource(m_playlistManager->urlByIndex(index));
|
||||
loadByModelIndex(index);
|
||||
play();
|
||||
}
|
||||
|
||||
@ -331,7 +365,7 @@ void MainWindow::on_nextBtn_clicked()
|
||||
{
|
||||
QModelIndex index(m_playlistManager->nextIndex());
|
||||
m_playlistManager->setCurrentIndex(index);
|
||||
m_mediaPlayer->setSource(m_playlistManager->urlByIndex(index));
|
||||
loadByModelIndex(index);
|
||||
play();
|
||||
}
|
||||
|
||||
@ -347,8 +381,8 @@ void MainWindow::on_minimumWindowBtn_clicked()
|
||||
|
||||
void MainWindow::initUiAndAnimation()
|
||||
{
|
||||
m_bgLinearGradient.setColorAt(0, QColor(255, 255, 255, 25)); // a:0
|
||||
m_bgLinearGradient.setColorAt(1, QColor(255, 255, 255, 75)); // a:200
|
||||
m_bgLinearGradient.setColorAt(0, QColor(0, 0, 0, 25));
|
||||
m_bgLinearGradient.setColorAt(1, QColor(0, 0, 0, 80));
|
||||
m_bgLinearGradient.setStart(0, 0);
|
||||
m_bgLinearGradient.setFinalStop(0, height());
|
||||
|
||||
@ -357,16 +391,54 @@ void MainWindow::initUiAndAnimation()
|
||||
m_fadeOutAnimation->setStartValue(1);
|
||||
m_fadeOutAnimation->setEndValue(0);
|
||||
connect(m_fadeOutAnimation, &QPropertyAnimation::finished, this, &QMainWindow::close);
|
||||
setFixedSize(490, 160);
|
||||
setFixedSize(miniSize);
|
||||
}
|
||||
|
||||
void MainWindow::initConnections()
|
||||
{
|
||||
connect(m_mediaDevices, &QMediaDevices::audioOutputsChanged, this, [=]{
|
||||
m_audioOutput->setDevice(m_mediaDevices->defaultAudioOutput());
|
||||
});
|
||||
|
||||
connect(m_mediaPlayer, &QMediaPlayer::sourceChanged, this, [=](){
|
||||
QUrl fileUrl(m_mediaPlayer->source());
|
||||
|
||||
ui->titleLabel->setText(fileUrl.fileName());
|
||||
ui->titleLabel->setToolTip(fileUrl.fileName());
|
||||
|
||||
if (fileUrl.isLocalFile()) {
|
||||
QString filePath(fileUrl.toLocalFile());
|
||||
QString suffix(filePath.mid(filePath.lastIndexOf('.') + 1));
|
||||
suffix = suffix.toUpper();
|
||||
|
||||
#ifndef NO_TAGLIB
|
||||
TagLib::FileRef fileRef(filePath.toLocal8Bit().data());
|
||||
|
||||
if (!fileRef.isNull() && fileRef.audioProperties()) {
|
||||
TagLib::AudioProperties *prop = fileRef.audioProperties();
|
||||
setAudioPropertyInfoForDisplay(prop->sampleRate(), prop->bitrate(), prop->channels(), suffix);
|
||||
}
|
||||
|
||||
if (!fileRef.isNull() && fileRef.tag()) {
|
||||
TagLib::Tag * tag = fileRef.tag();
|
||||
setAudioMetadataForDisplay(QString::fromStdString(tag->title().to8Bit(true)),
|
||||
QString::fromStdString(tag->artist().to8Bit(true)),
|
||||
QString::fromStdString(tag->album().to8Bit(true)));
|
||||
}
|
||||
#endif // NO_TAGLIB
|
||||
}
|
||||
});
|
||||
|
||||
connect(m_mediaPlayer, &QMediaPlayer::metaDataChanged, this, [=](){
|
||||
QMediaMetaData metadata(m_mediaPlayer->metaData());
|
||||
// it's known in some cases QMediaMetaData using the incorrect text codec for metadata
|
||||
// see `02 Yoiyami Hanabi.mp3`'s Title. So we don't use Qt's one if tablib is available.
|
||||
qDebug() << metadata.stringValue(QMediaMetaData::Title) << metadata.stringValue(QMediaMetaData::Author);
|
||||
#ifdef NO_TAGLIB
|
||||
setAudioMetadataForDisplay(metadata.stringValue(QMediaMetaData::Title),
|
||||
metadata.stringValue(QMediaMetaData::Author),
|
||||
metadata.stringValue(QMediaMetaData::AlbumTitle));
|
||||
#endif // NO_TAGLIB
|
||||
QVariant coverArt(metadata.value(QMediaMetaData::ThumbnailImage));
|
||||
if (!coverArt.isNull()) {
|
||||
ui->coverLabel->setPixmap(QPixmap::fromImage(coverArt.value<QImage>()));
|
||||
@ -384,6 +456,7 @@ void MainWindow::initConnections()
|
||||
if (m_mediaPlayer->duration() != 0) {
|
||||
ui->playbackSlider->setSliderPosition(ui->playbackSlider->maximum() * pos / m_mediaPlayer->duration());
|
||||
}
|
||||
m_lrcbar->playbackPositionChanged(pos, m_mediaPlayer->duration());
|
||||
});
|
||||
|
||||
connect(m_audioOutput, &QAudioOutput::mutedChanged, this, [=](bool muted) {
|
||||
@ -453,6 +526,34 @@ void MainWindow::initConnections()
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::loadSkinData()
|
||||
{
|
||||
QFile file(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation) + "/skin.dat");
|
||||
bool canOpen = file.open(QIODevice::ReadOnly);
|
||||
if (!canOpen) return;
|
||||
QDataStream stream(&file);
|
||||
quint32 magic;
|
||||
stream >> magic;
|
||||
if (magic == 0x78297000) {
|
||||
stream >> m_skin;
|
||||
m_skin = m_skin.scaled(fullSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
|
||||
void MainWindow::saveSkinData()
|
||||
{
|
||||
QDir configDir(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation));
|
||||
if (!configDir.exists()) {
|
||||
configDir.mkpath(".");
|
||||
}
|
||||
QFile file(configDir.absoluteFilePath("skin.dat"));
|
||||
file.open(QIODevice::WriteOnly);
|
||||
QDataStream stream(&file);
|
||||
stream << (quint32)0x78297000 << m_skin;
|
||||
file.close();
|
||||
}
|
||||
|
||||
void MainWindow::on_playbackModeBtn_clicked()
|
||||
{
|
||||
switch (m_playbackMode) {
|
||||
@ -470,15 +571,64 @@ void MainWindow::on_playbackModeBtn_clicked()
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::on_setSkinBtn_clicked()
|
||||
{
|
||||
QStringList imageFolders(QStandardPaths::standardLocations(QStandardPaths::PicturesLocation));
|
||||
imageFolders.append(QDir::homePath());
|
||||
QString image = QFileDialog::getOpenFileName(this, tr("Select image as background skin"),
|
||||
imageFolders.first(),
|
||||
tr("Image files (*.jpg *.jpeg *.png *.gif)"));
|
||||
if(!image.isEmpty()) {
|
||||
setSkin(image, true);
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::on_playListBtn_clicked()
|
||||
{
|
||||
setFixedHeight(size().height() < 200 ? 420 : 160);
|
||||
setFixedSize(size().height() < 200 ? fullSize : miniSize);
|
||||
}
|
||||
|
||||
void MainWindow::on_playlistView_activated(const QModelIndex &index)
|
||||
{
|
||||
m_playlistManager->setCurrentIndex(index);
|
||||
m_mediaPlayer->setSource(m_playlistManager->urlByIndex(index));
|
||||
loadByModelIndex(index);
|
||||
play();
|
||||
}
|
||||
|
||||
void MainWindow::on_lrcBtn_clicked()
|
||||
{
|
||||
if (m_lrcbar->isVisible()) {
|
||||
m_lrcbar->hide();
|
||||
} else {
|
||||
m_lrcbar->show();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void MainWindow::on_actionHelp_triggered()
|
||||
{
|
||||
QMessageBox infoBox(this);
|
||||
infoBox.setIcon(QMessageBox::Information);
|
||||
infoBox.setWindowTitle(tr("About"));
|
||||
infoBox.setStandardButtons(QMessageBox::Ok);
|
||||
infoBox.setText(
|
||||
tr("Pineapple Music") %
|
||||
"\n\n" %
|
||||
tr("Based on the following free software libraries:") %
|
||||
"\n\n" %
|
||||
QStringLiteral("- [Qt](https://www.qt.io/) %1\n").arg(QT_VERSION_STR) %
|
||||
#ifndef NO_TAGLIB
|
||||
QStringLiteral("- [TagLib](https://github.com/taglib/taglib)\n") %
|
||||
#endif // NO_TAGLIB
|
||||
#ifndef NO_UCHARDET
|
||||
QStringLiteral("- [uchardet](https://www.freedesktop.org/wiki/Software/uchardet/)\n") %
|
||||
#endif // NO_TAGLIB
|
||||
"\n"
|
||||
"[Source Code](https://github.com/BLumia/pineapple-music)\n"
|
||||
"\n"
|
||||
"Copyright © 2024 [BLumia](https://github.com/BLumia/)"
|
||||
);
|
||||
infoBox.setTextFormat(Qt::MarkdownText);
|
||||
infoBox.exec();
|
||||
}
|
||||
|
||||
|
20
mainwindow.h
20
mainwindow.h
@ -1,3 +1,7 @@
|
||||
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef MAINWINDOW_H
|
||||
#define MAINWINDOW_H
|
||||
|
||||
@ -7,11 +11,13 @@
|
||||
QT_BEGIN_NAMESPACE
|
||||
namespace Ui { class MainWindow; }
|
||||
|
||||
class QMediaDevices;
|
||||
class QMediaPlayer;
|
||||
class QAudioOutput;
|
||||
class QPropertyAnimation;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
class LrcBar;
|
||||
class PlaylistManager;
|
||||
class MainWindow : public QMainWindow
|
||||
{
|
||||
@ -47,8 +53,11 @@ protected:
|
||||
void dropEvent(QDropEvent *e) override;
|
||||
|
||||
void loadFile();
|
||||
void loadByModelIndex(const QModelIndex &index);
|
||||
void play();
|
||||
|
||||
void setSkin(QString imagePath, bool save);
|
||||
|
||||
void centerWindow();
|
||||
|
||||
private slots:
|
||||
@ -62,10 +71,11 @@ private slots:
|
||||
void on_nextBtn_clicked();
|
||||
void on_volumeBtn_clicked();
|
||||
void on_minimumWindowBtn_clicked();
|
||||
|
||||
void on_setSkinBtn_clicked();
|
||||
void on_playListBtn_clicked();
|
||||
|
||||
void on_playlistView_activated(const QModelIndex &index);
|
||||
void on_lrcBtn_clicked();
|
||||
void on_actionHelp_triggered();
|
||||
|
||||
signals:
|
||||
void playbackModeChanged(enum PlaybackMode mode);
|
||||
@ -74,18 +84,24 @@ private:
|
||||
bool m_clickedOnWindow = false;
|
||||
bool m_playbackSliderPressed = false;
|
||||
QLinearGradient m_bgLinearGradient;
|
||||
QPixmap m_skin;
|
||||
enum PlaybackMode m_playbackMode = CurrentItemInLoop;
|
||||
|
||||
Ui::MainWindow *ui;
|
||||
|
||||
QMediaDevices *m_mediaDevices;
|
||||
QMediaPlayer *m_mediaPlayer;
|
||||
QAudioOutput *m_audioOutput;
|
||||
LrcBar *m_lrcbar;
|
||||
QPropertyAnimation *m_fadeOutAnimation;
|
||||
PlaylistManager *m_playlistManager;
|
||||
|
||||
void initUiAndAnimation();
|
||||
void initConnections();
|
||||
|
||||
void loadSkinData();
|
||||
void saveSkinData();
|
||||
|
||||
static QString ms2str(qint64 ms);
|
||||
static QList<QUrl> strlst2urllst(QStringList strlst);
|
||||
};
|
||||
|
@ -20,7 +20,7 @@
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Pineapple Player</string>
|
||||
<string>Pineapple Music</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset resource="resources.qrc">
|
||||
@ -77,11 +77,11 @@ QPushButton {
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: rgba(255, 255, 255, 220);
|
||||
background-color: rgba(0, 85, 255, 120);
|
||||
}
|
||||
|
||||
QPushButton:pressed {
|
||||
background-color: rgba(255, 255, 255, 250);
|
||||
background-color: rgba(0, 85, 255, 250);
|
||||
}
|
||||
|
||||
QPushButton#closeWindowBtn {
|
||||
@ -101,6 +101,12 @@ QLabel {
|
||||
|
||||
QLabel#coverLabel {
|
||||
border: 1px solid grey;
|
||||
}
|
||||
|
||||
/****** ListView ******/
|
||||
|
||||
QListView {
|
||||
background: rgba(0, 0, 0, 50);
|
||||
}</string>
|
||||
</property>
|
||||
<property name="locale">
|
||||
@ -115,7 +121,7 @@ QLabel#coverLabel {
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0">
|
||||
<property name="spacing">
|
||||
<number>7</number>
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
@ -220,7 +226,7 @@ QLabel#coverLabel {
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="miniModeBtn">
|
||||
<widget class="QPushButton" name="setSkinBtn">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>25</width>
|
||||
@ -228,7 +234,17 @@ QLabel#coverLabel {
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>^</string>
|
||||
<string/>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="resources.qrc">
|
||||
<normaloff>:/icons/icons/skin.png</normaloff>:/icons/icons/skin.png</iconset>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>24</width>
|
||||
<height>24</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@ -299,25 +315,51 @@ QLabel#coverLabel {
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="propLabel">
|
||||
<property name="text">
|
||||
<string>Drag and drop file to load</string>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
<item>
|
||||
<widget class="QLabel" name="propLabel">
|
||||
<property name="text">
|
||||
<string>Drag and drop file to load</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="lrcBtn">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string comment="Lyrics">Lrc</string>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="playbackTimeLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="nowTimeLabel">
|
||||
<property name="text">
|
||||
<string>0:00</string>
|
||||
<string notr="true">0:00</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="totalTimeLabel">
|
||||
<property name="text">
|
||||
<string>0:00</string>
|
||||
<string notr="true">0:00</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
|
||||
@ -615,6 +657,17 @@ QLabel#coverLabel {
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<action name="actionHelp">
|
||||
<property name="text">
|
||||
<string>help</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>F1</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::MenuRole::NoRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
|
@ -16,6 +16,7 @@
|
||||
<file>icons/media-playlist-repeat-song.png</file>
|
||||
<file>icons/media-playlist-normal.png</file>
|
||||
<file>icons/media-repeat-single.png</file>
|
||||
<file>icons/skin.png</file>
|
||||
<file>icons/media-album-cover.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
@ -1,29 +1,18 @@
|
||||
#include "seekableslider.h"
|
||||
|
||||
SeekableSlider::SeekableSlider(QWidget *parent) :
|
||||
QSlider(parent)
|
||||
{
|
||||
//关闭分段移动
|
||||
setSingleStep(0);
|
||||
setPageStep(0);
|
||||
}
|
||||
|
||||
//点击Slider即可调节Value
|
||||
//只写了横向模式的Slider……
|
||||
void SeekableSlider::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->buttons() & Qt::LeftButton) {
|
||||
QSlider::mousePressEvent(event);
|
||||
double pos = event->pos().x() / (double)width();
|
||||
setValue(pos * (maximum() - minimum()) + minimum());
|
||||
}
|
||||
}
|
||||
|
||||
void SeekableSlider::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->buttons() & Qt::LeftButton) {
|
||||
QSlider::mousePressEvent(event);
|
||||
double pos = event->pos().x() / (double)width();
|
||||
setValue(pos * (maximum() - minimum()) + minimum());
|
||||
}
|
||||
}
|
||||
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "seekableslider.h"
|
||||
|
||||
SeekableSlider::SeekableSlider(QWidget *parent) :
|
||||
QSlider(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void SeekableSlider::mouseReleaseEvent(QMouseEvent *event)
|
||||
{
|
||||
double pos = event->pos().x() / (double)width();
|
||||
setValue(pos * (maximum() - minimum()) + minimum());
|
||||
emit sliderReleased();
|
||||
return QSlider::mouseReleaseEvent(event);
|
||||
}
|
||||
|
@ -1,3 +1,7 @@
|
||||
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QSlider>
|
||||
@ -8,13 +12,13 @@ class SeekableSlider : public QSlider
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SeekableSlider(QWidget *parent = nullptr);
|
||||
~SeekableSlider() = default;
|
||||
|
||||
signals:
|
||||
|
||||
public slots:
|
||||
|
||||
protected:
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||
};
|
||||
|
||||
|
@ -1,3 +1,7 @@
|
||||
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "singleapplicationmanager.h"
|
||||
|
||||
#include <QVariant>
|
||||
|
@ -1,3 +1,7 @@
|
||||
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef SINGLEAPPLICATIONMANAGER_H
|
||||
#define SINGLEAPPLICATIONMANAGER_H
|
||||
|
||||
|
Reference in New Issue
Block a user