Compare commits
16
Commits
042988ffbb
...
0.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c174d8e18 | ||
|
|
383088054c | ||
|
|
7164a5a299 | ||
|
|
a60f430334 | ||
|
|
0a4c1fbe88 | ||
|
|
6b691a282b | ||
|
|
11f79a9b98 | ||
|
|
fbcda7a401 | ||
|
|
c6771e6747 | ||
|
|
6e87a4b13b | ||
|
|
6e9db8c1f9 | ||
|
|
af38103c0d | ||
|
|
094a83512e | ||
|
|
5f6c89673c | ||
|
|
010e7162fd | ||
|
|
097b32c70d |
@@ -8,7 +8,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- qt_ver: '6.9.1'
|
- qt_ver: '6.10.3'
|
||||||
vs: '2022'
|
vs: '2022'
|
||||||
aqt_arch: 'win64_msvc2022_64'
|
aqt_arch: 'win64_msvc2022_64'
|
||||||
msvc_arch: 'x64'
|
msvc_arch: 'x64'
|
||||||
@@ -16,13 +16,13 @@ jobs:
|
|||||||
runs-on: windows-2022
|
runs-on: windows-2022
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- name: Install Qt
|
- name: Install Qt
|
||||||
uses: jurplel/install-qt-action@v4
|
uses: jurplel/install-qt-action@v4
|
||||||
with:
|
with:
|
||||||
arch: ${{ matrix.aqt_arch }}
|
arch: ${{ matrix.aqt_arch }}
|
||||||
version: ${{ matrix.qt_ver }}
|
version: ${{ matrix.qt_ver }}
|
||||||
modules: 'qtmultimedia qt5compat'
|
modules: 'qtmultimedia'
|
||||||
- name: Build
|
- name: Build
|
||||||
shell: cmd
|
shell: cmd
|
||||||
run: |
|
run: |
|
||||||
@@ -59,16 +59,16 @@ jobs:
|
|||||||
cmake --build build_dependencies/taglib --config Release --target=install -j || goto :error
|
cmake --build build_dependencies/taglib --config Release --target=install -j || goto :error
|
||||||
echo ::endgroup::
|
echo ::endgroup::
|
||||||
:: ------ app ------
|
:: ------ app ------
|
||||||
cmake -Bbuild . -DUSE_QTEXTCODEC=ON -DCMAKE_INSTALL_PREFIX='%PWD%\build\' || goto :error
|
cmake -Bbuild . -DUSE_QTEXTCODEC=OFF -DCMAKE_INSTALL_PREFIX='%PWD%\build\' || goto :error
|
||||||
cmake --build build --config Release -j || goto :error
|
cmake --build build --config Release -j || goto :error
|
||||||
cmake --build build --config Release --target=install || goto :error
|
cmake --build build --config Release --target=install || goto :error
|
||||||
:: ------ pkg ------
|
:: ------ pkg ------
|
||||||
windeployqt --verbose=2 --no-quick-import --no-translations --no-opengl-sw --no-system-d3d-compiler --no-system-dxc-compiler --multimedia --core5compat --skip-plugin-types tls,networkinformation build\bin\pmusic.exe
|
windeployqt --verbose=2 --no-quick-import --no-translations --no-opengl-sw --no-system-d3d-compiler --no-system-dxc-compiler --multimedia --skip-plugin-types tls,networkinformation build\bin\pmusic.exe
|
||||||
robocopy ./dependencies_bin/bin build/bin *.dll
|
robocopy ./dependencies_bin/bin build/bin *.dll
|
||||||
if ErrorLevel 8 (exit /B 1)
|
if ErrorLevel 8 (exit /B 1)
|
||||||
copy LICENSE build/bin/
|
copy LICENSE build/bin/
|
||||||
exit /B 0
|
exit /B 0
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: "${{ matrix.aqt_arch }}-qt${{ matrix.qt_ver }}-cmake-package"
|
name: "${{ matrix.aqt_arch }}-qt${{ matrix.qt_ver }}-cmake-package"
|
||||||
path: build/bin/*
|
path: build/bin/*
|
||||||
|
|||||||
+16
-13
@@ -1,13 +1,16 @@
|
|||||||
# Common build folder
|
# Common build folder
|
||||||
[Bb]uild/
|
[Bb]uild/
|
||||||
build-*/
|
build-*/
|
||||||
|
|
||||||
# IDE folder
|
# IDE folder
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
# User config file
|
# Clangd cache
|
||||||
CMakeLists.txt.user*
|
.cache/
|
||||||
|
|
||||||
# Why, macOS, why?
|
# User config file
|
||||||
.DS_Store
|
CMakeLists.txt.user*
|
||||||
|
|
||||||
|
# Why, macOS, why?
|
||||||
|
.DS_Store
|
||||||
|
|||||||
+10
-1
@@ -1,6 +1,6 @@
|
|||||||
cmake_minimum_required(VERSION 3.12)
|
cmake_minimum_required(VERSION 3.12)
|
||||||
|
|
||||||
project(pineapple-music LANGUAGES CXX VERSION 0.3.0)
|
project(pineapple-music LANGUAGES CXX VERSION 0.5.0)
|
||||||
|
|
||||||
include (GNUInstallDirs)
|
include (GNUInstallDirs)
|
||||||
include (FeatureSummary)
|
include (FeatureSummary)
|
||||||
@@ -47,6 +47,8 @@ set (PMUSIC_CPP_FILES
|
|||||||
lyricsmanager.cpp
|
lyricsmanager.cpp
|
||||||
fftspectrum.cpp
|
fftspectrum.cpp
|
||||||
playbackprogressindicator.cpp
|
playbackprogressindicator.cpp
|
||||||
|
lyricswidget.cpp
|
||||||
|
winmediacontrol.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
set (PMUSIC_HEADER_FILES
|
set (PMUSIC_HEADER_FILES
|
||||||
@@ -59,6 +61,8 @@ set (PMUSIC_HEADER_FILES
|
|||||||
fftspectrum.h
|
fftspectrum.h
|
||||||
playbackprogressindicator.h
|
playbackprogressindicator.h
|
||||||
taskbarmanager.h
|
taskbarmanager.h
|
||||||
|
winmediacontrol.h
|
||||||
|
lyricswidget.h
|
||||||
)
|
)
|
||||||
|
|
||||||
set (PMUSIC_UI_FILES
|
set (PMUSIC_UI_FILES
|
||||||
@@ -86,6 +90,7 @@ TS_FILES
|
|||||||
if (WIN32)
|
if (WIN32)
|
||||||
target_sources(${EXE_NAME} PRIVATE assets/pineapple-music.rc)
|
target_sources(${EXE_NAME} PRIVATE assets/pineapple-music.rc)
|
||||||
target_sources(${EXE_NAME} PRIVATE taskbarmanager.cpp)
|
target_sources(${EXE_NAME} PRIVATE taskbarmanager.cpp)
|
||||||
|
target_link_libraries(${EXE_NAME} PRIVATE runtimeobject ole32)
|
||||||
else ()
|
else ()
|
||||||
target_sources(${EXE_NAME} PRIVATE taskbarmanager_dummy.cpp)
|
target_sources(${EXE_NAME} PRIVATE taskbarmanager_dummy.cpp)
|
||||||
endif ()
|
endif ()
|
||||||
@@ -113,6 +118,10 @@ if (USE_QTEXTCODEC)
|
|||||||
target_link_libraries(${EXE_NAME} PRIVATE Qt6::Core5Compat)
|
target_link_libraries(${EXE_NAME} PRIVATE Qt6::Core5Compat)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
target_compile_definitions(${EXE_NAME} PRIVATE
|
||||||
|
PMUSIC_VERSION_STRING="${CMAKE_PROJECT_VERSION}"
|
||||||
|
)
|
||||||
|
|
||||||
# Install settings
|
# Install settings
|
||||||
if (WIN32)
|
if (WIN32)
|
||||||
set_target_properties(${EXE_NAME} PROPERTIES
|
set_target_properties(${EXE_NAME} PROPERTIES
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ We have the following features:
|
|||||||
- PotPlayer `.pbf` file, `[Bookmark]`s as chapters
|
- PotPlayer `.pbf` file, `[Bookmark]`s as chapters
|
||||||
- Built-in chapter support (require build with FFmpeg)
|
- Built-in chapter support (require build with FFmpeg)
|
||||||
- Auto-load all audio files in the same folder of the file that you attempted to play, into a playlist
|
- Auto-load all audio files in the same folder of the file that you attempted to play, into a playlist
|
||||||
|
- [SMTC](https://learn.microsoft.com/en-us/uwp/api/windows.media.systemmediatransportcontrols) support on Windows
|
||||||
|
|
||||||
These features are not available, some of them are TBD and others are not planned:
|
These features are not available, some of them are TBD and others are not planned:
|
||||||
|
|
||||||
@@ -21,9 +22,7 @@ These features are not available, some of them are TBD and others are not planne
|
|||||||
- No music library management support and there won't be one!
|
- 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.
|
- 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 system integration:
|
- Limited system integration:
|
||||||
- No [SMTC](https://learn.microsoft.com/en-us/uwp/api/windows.media.systemmediatransportcontrols) support under Windows for now
|
|
||||||
- No [MPRIS](https://www.freedesktop.org/wiki/Specifications/mpris-spec/) support under Linux desktop for now
|
- No [MPRIS](https://www.freedesktop.org/wiki/Specifications/mpris-spec/) support under Linux desktop for now
|
||||||
- No "playback progress on taskbar icon" and "taskbar thumbnail buttons" support whatever on Windows or Linux desktop for now
|
|
||||||
- Limited lyrics (`.lrc`) loading support:
|
- Limited lyrics (`.lrc`) loading support:
|
||||||
- Currently no `.tlrc` (for translated lyrics) or `.rlrc` (for romanized lyrics) support.
|
- Currently no `.tlrc` (for translated lyrics) or `.rlrc` (for romanized lyrics) support.
|
||||||
- Multi-line lyrics and duplicated timestamps are not supported
|
- Multi-line lyrics and duplicated timestamps are not supported
|
||||||
|
|||||||
+3
-3
@@ -4,9 +4,9 @@ environment:
|
|||||||
CMAKE_INSTALL_ROOT: C:\projects\cmake
|
CMAKE_INSTALL_ROOT: C:\projects\cmake
|
||||||
PACKAGE_INSTALL_ROOT: C:\projects\pir
|
PACKAGE_INSTALL_ROOT: C:\projects\pir
|
||||||
matrix:
|
matrix:
|
||||||
- build_name: mingw1120_64_qt6_7
|
- build_name: mingw1120_64_qt6_9
|
||||||
QTPATH: C:\Qt\6.8\mingw_64
|
QTPATH: C:\Qt\6.9\mingw_64
|
||||||
MINGW64: C:\Qt\Tools\mingw1310_64
|
MINGW64: C:\Qt\mingw_64\Tools\mingw1310_64
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- mkdir %CMAKE_INSTALL_ROOT%
|
- mkdir %CMAKE_INSTALL_ROOT%
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!DOCTYPE TS>
|
||||||
|
<TS version="2.1" language="de">
|
||||||
|
<context>
|
||||||
|
<name>LrcBar</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../lrcbar.cpp" line="88"/>
|
||||||
|
<source>(Interlude...)</source>
|
||||||
|
<translation>(Zwischenspiel …)</translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>MainWindow</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="125"/>
|
||||||
|
<source>Mono</source>
|
||||||
|
<translation>Mono</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="127"/>
|
||||||
|
<source>Stereo</source>
|
||||||
|
<translation>Stereo</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="129"/>
|
||||||
|
<source>%1 Channels</source>
|
||||||
|
<translation>%1 Kanäle</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="298"/>
|
||||||
|
<source>Select songs to play</source>
|
||||||
|
<translation>Songs zum Spielen auswählen</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="300"/>
|
||||||
|
<source>Audio Files</source>
|
||||||
|
<translation>Audiodateien</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="685"/>
|
||||||
|
<source>Select image as background skin</source>
|
||||||
|
<translation>Bild als Hintergrundskin auswählen</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="687"/>
|
||||||
|
<source>Image files (*.jpg *.jpeg *.png *.gif)</source>
|
||||||
|
<translation>Bilddateien (*.jpg *.jpeg *.png *.gif)</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="764"/>
|
||||||
|
<source>Based on the following free software libraries:</source>
|
||||||
|
<translation>Basiert auf den folgenden freien Programmbibliotheken:</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.ui" line="23"/>
|
||||||
|
<location filename="../mainwindow.cpp" line="762"/>
|
||||||
|
<location filename="../lrcbar.cpp" line="89"/>
|
||||||
|
<source>Pineapple Music</source>
|
||||||
|
<translation>Pineapple Music</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.ui" line="327"/>
|
||||||
|
<source>No song loaded...</source>
|
||||||
|
<translation>Kein Song geladen …</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.ui" line="339"/>
|
||||||
|
<source>Drag and drop file to load</source>
|
||||||
|
<translation>Datei zum Laden ziehen und ablegen</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.ui" line="352"/>
|
||||||
|
<source>Lrc</source>
|
||||||
|
<comment>Lyrics</comment>
|
||||||
|
<translation>Lrc</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.ui" line="711"/>
|
||||||
|
<location filename="../mainwindow.ui" line="714"/>
|
||||||
|
<location filename="../mainwindow.cpp" line="759"/>
|
||||||
|
<source>About</source>
|
||||||
|
<translation>Über</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.ui" line="728"/>
|
||||||
|
<source>Open</source>
|
||||||
|
<translation>Öffnen</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="135"/>
|
||||||
|
<source>Sample Rate: %1 Hz</source>
|
||||||
|
<translation>Abtastrate: %1 Hz</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="140"/>
|
||||||
|
<source>Bitrate: %1 Kbps</source>
|
||||||
|
<translation>Bitrate: %1 kbps</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="145"/>
|
||||||
|
<source>Channel Count: %1</source>
|
||||||
|
<translation>Kanalanzahl: %1</translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>PlaybackProgressIndicator</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../playbackprogressindicator.cpp" line="85"/>
|
||||||
|
<source>Time</source>
|
||||||
|
<translation>Zeit</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../playbackprogressindicator.cpp" line="85"/>
|
||||||
|
<source>Chapter Name</source>
|
||||||
|
<translation>Kapitelname</translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>main</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../main.cpp" line="27"/>
|
||||||
|
<source>File list.</source>
|
||||||
|
<translation>Dateiliste.</translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
</TS>
|
||||||
@@ -12,82 +12,113 @@
|
|||||||
<context>
|
<context>
|
||||||
<name>MainWindow</name>
|
<name>MainWindow</name>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="93"/>
|
<location filename="../mainwindow.cpp" line="125"/>
|
||||||
<source>Mono</source>
|
<source>Mono</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="95"/>
|
<location filename="../mainwindow.cpp" line="127"/>
|
||||||
<source>Stereo</source>
|
<source>Stereo</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="97"/>
|
<location filename="../mainwindow.cpp" line="129"/>
|
||||||
<source>%1 Channels</source>
|
<source>%1 Channels</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="241"/>
|
<location filename="../mainwindow.cpp" line="298"/>
|
||||||
<source>Select songs to play</source>
|
<source>Select songs to play</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="243"/>
|
<location filename="../mainwindow.cpp" line="300"/>
|
||||||
<source>Audio Files</source>
|
<source>Audio Files</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="576"/>
|
<location filename="../mainwindow.cpp" line="685"/>
|
||||||
<source>Select image as background skin</source>
|
<source>Select image as background skin</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="578"/>
|
<location filename="../mainwindow.cpp" line="687"/>
|
||||||
<source>Image files (*.jpg *.jpeg *.png *.gif)</source>
|
<source>Image files (*.jpg *.jpeg *.png *.gif)</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="764"/>
|
||||||
|
<source>Based on the following free software libraries:</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.ui" line="23"/>
|
<location filename="../mainwindow.ui" line="23"/>
|
||||||
|
<location filename="../mainwindow.cpp" line="762"/>
|
||||||
<location filename="../lrcbar.cpp" line="89"/>
|
<location filename="../lrcbar.cpp" line="89"/>
|
||||||
<source>Pineapple Music</source>
|
<source>Pineapple Music</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.ui" line="313"/>
|
<location filename="../mainwindow.ui" line="327"/>
|
||||||
<source>No song loaded...</source>
|
<source>No song loaded...</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.ui" line="325"/>
|
<location filename="../mainwindow.ui" line="339"/>
|
||||||
<source>Drag and drop file to load</source>
|
<source>Drag and drop file to load</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.ui" line="338"/>
|
<location filename="../mainwindow.ui" line="352"/>
|
||||||
<source>Lrc</source>
|
<source>Lrc</source>
|
||||||
<comment>Lyrics</comment>
|
<comment>Lyrics</comment>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="103"/>
|
<location filename="../mainwindow.ui" line="711"/>
|
||||||
|
<location filename="../mainwindow.ui" line="714"/>
|
||||||
|
<location filename="../mainwindow.cpp" line="759"/>
|
||||||
|
<source>About</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.ui" line="728"/>
|
||||||
|
<source>Open</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="135"/>
|
||||||
<source>Sample Rate: %1 Hz</source>
|
<source>Sample Rate: %1 Hz</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="108"/>
|
<location filename="../mainwindow.cpp" line="140"/>
|
||||||
<source>Bitrate: %1 Kbps</source>
|
<source>Bitrate: %1 Kbps</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="113"/>
|
<location filename="../mainwindow.cpp" line="145"/>
|
||||||
<source>Channel Count: %1</source>
|
<source>Channel Count: %1</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>PlaybackProgressIndicator</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../playbackprogressindicator.cpp" line="85"/>
|
||||||
|
<source>Time</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../playbackprogressindicator.cpp" line="85"/>
|
||||||
|
<source>Chapter Name</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>main</name>
|
<name>main</name>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../main.cpp" line="28"/>
|
<location filename="../main.cpp" line="27"/>
|
||||||
<source>File list.</source>
|
<source>File list.</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!DOCTYPE TS>
|
||||||
|
<TS version="2.1" language="nl">
|
||||||
|
<context>
|
||||||
|
<name>LrcBar</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../lrcbar.cpp" line="88"/>
|
||||||
|
<source>(Interlude...)</source>
|
||||||
|
<translation>(Intermezzo…)</translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>MainWindow</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="125"/>
|
||||||
|
<source>Mono</source>
|
||||||
|
<translation>Mono</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="127"/>
|
||||||
|
<source>Stereo</source>
|
||||||
|
<translation>Stereo</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="129"/>
|
||||||
|
<source>%1 Channels</source>
|
||||||
|
<translation>%1 kanalen</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="298"/>
|
||||||
|
<source>Select songs to play</source>
|
||||||
|
<translation>Kies de af te spelen nummers</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="300"/>
|
||||||
|
<source>Audio Files</source>
|
||||||
|
<translation>Audiobestanden</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="685"/>
|
||||||
|
<source>Select image as background skin</source>
|
||||||
|
<translation>Kies een achtergrondafbeelding</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="687"/>
|
||||||
|
<source>Image files (*.jpg *.jpeg *.png *.gif)</source>
|
||||||
|
<translation>Afbeeldingsbestanden (*.jpg *.jpeg *.png *.gif)</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="764"/>
|
||||||
|
<source>Based on the following free software libraries:</source>
|
||||||
|
<translation>Gebaseerd op deze vrijesoftwarebibliotheken:</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.ui" line="23"/>
|
||||||
|
<location filename="../mainwindow.cpp" line="762"/>
|
||||||
|
<location filename="../lrcbar.cpp" line="89"/>
|
||||||
|
<source>Pineapple Music</source>
|
||||||
|
<translation>Pineapple-muziekspeler</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.ui" line="327"/>
|
||||||
|
<source>No song loaded...</source>
|
||||||
|
<translation>Geen nummer geladen…</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.ui" line="339"/>
|
||||||
|
<source>Drag and drop file to load</source>
|
||||||
|
<translation>Sleep een bestand hierheen</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.ui" line="352"/>
|
||||||
|
<source>Lrc</source>
|
||||||
|
<comment>Lyrics</comment>
|
||||||
|
<translation>Lrc</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.ui" line="711"/>
|
||||||
|
<location filename="../mainwindow.ui" line="714"/>
|
||||||
|
<location filename="../mainwindow.cpp" line="759"/>
|
||||||
|
<source>About</source>
|
||||||
|
<translation>Over</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.ui" line="728"/>
|
||||||
|
<source>Open</source>
|
||||||
|
<translation>Openen</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="135"/>
|
||||||
|
<source>Sample Rate: %1 Hz</source>
|
||||||
|
<translation>Samplesnelheid: %1 Hz</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="140"/>
|
||||||
|
<source>Bitrate: %1 Kbps</source>
|
||||||
|
<translation>Bitsnelheid: %1 Kbps</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="145"/>
|
||||||
|
<source>Channel Count: %1</source>
|
||||||
|
<translation>Aantal kanalen: %1</translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>PlaybackProgressIndicator</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../playbackprogressindicator.cpp" line="85"/>
|
||||||
|
<source>Time</source>
|
||||||
|
<translation>Tijdstip</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../playbackprogressindicator.cpp" line="85"/>
|
||||||
|
<source>Chapter Name</source>
|
||||||
|
<translation>Hoofdstuknaam</translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>main</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../main.cpp" line="27"/>
|
||||||
|
<source>File list.</source>
|
||||||
|
<translation>Bestandslijst.</translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
</TS>
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!DOCTYPE TS>
|
||||||
|
<TS version="2.1" language="pl">
|
||||||
|
<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="125"/>
|
||||||
|
<source>Mono</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="127"/>
|
||||||
|
<source>Stereo</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="129"/>
|
||||||
|
<source>%1 Channels</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="298"/>
|
||||||
|
<source>Select songs to play</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="300"/>
|
||||||
|
<source>Audio Files</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="685"/>
|
||||||
|
<source>Select image as background skin</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="687"/>
|
||||||
|
<source>Image files (*.jpg *.jpeg *.png *.gif)</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="764"/>
|
||||||
|
<source>Based on the following free software libraries:</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.ui" line="23"/>
|
||||||
|
<location filename="../mainwindow.cpp" line="762"/>
|
||||||
|
<location filename="../lrcbar.cpp" line="89"/>
|
||||||
|
<source>Pineapple Music</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.ui" line="327"/>
|
||||||
|
<source>No song loaded...</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.ui" line="339"/>
|
||||||
|
<source>Drag and drop file to load</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.ui" line="352"/>
|
||||||
|
<source>Lrc</source>
|
||||||
|
<comment>Lyrics</comment>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.ui" line="711"/>
|
||||||
|
<location filename="../mainwindow.ui" line="714"/>
|
||||||
|
<location filename="../mainwindow.cpp" line="759"/>
|
||||||
|
<source>About</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.ui" line="728"/>
|
||||||
|
<source>Open</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="135"/>
|
||||||
|
<source>Sample Rate: %1 Hz</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="140"/>
|
||||||
|
<source>Bitrate: %1 Kbps</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="145"/>
|
||||||
|
<source>Channel Count: %1</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>PlaybackProgressIndicator</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../playbackprogressindicator.cpp" line="85"/>
|
||||||
|
<source>Time</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../playbackprogressindicator.cpp" line="85"/>
|
||||||
|
<source>Chapter Name</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>main</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../main.cpp" line="27"/>
|
||||||
|
<source>File list.</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
</TS>
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!DOCTYPE TS>
|
||||||
|
<TS version="2.1" language="tr">
|
||||||
|
<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="125"/>
|
||||||
|
<source>Mono</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="127"/>
|
||||||
|
<source>Stereo</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="129"/>
|
||||||
|
<source>%1 Channels</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="298"/>
|
||||||
|
<source>Select songs to play</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="300"/>
|
||||||
|
<source>Audio Files</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="685"/>
|
||||||
|
<source>Select image as background skin</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="687"/>
|
||||||
|
<source>Image files (*.jpg *.jpeg *.png *.gif)</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="764"/>
|
||||||
|
<source>Based on the following free software libraries:</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.ui" line="23"/>
|
||||||
|
<location filename="../mainwindow.cpp" line="762"/>
|
||||||
|
<location filename="../lrcbar.cpp" line="89"/>
|
||||||
|
<source>Pineapple Music</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.ui" line="327"/>
|
||||||
|
<source>No song loaded...</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.ui" line="339"/>
|
||||||
|
<source>Drag and drop file to load</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.ui" line="352"/>
|
||||||
|
<source>Lrc</source>
|
||||||
|
<comment>Lyrics</comment>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.ui" line="711"/>
|
||||||
|
<location filename="../mainwindow.ui" line="714"/>
|
||||||
|
<location filename="../mainwindow.cpp" line="759"/>
|
||||||
|
<source>About</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.ui" line="728"/>
|
||||||
|
<source>Open</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="135"/>
|
||||||
|
<source>Sample Rate: %1 Hz</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="140"/>
|
||||||
|
<source>Bitrate: %1 Kbps</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="145"/>
|
||||||
|
<source>Channel Count: %1</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>PlaybackProgressIndicator</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../playbackprogressindicator.cpp" line="85"/>
|
||||||
|
<source>Time</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../playbackprogressindicator.cpp" line="85"/>
|
||||||
|
<source>Chapter Name</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>main</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../main.cpp" line="27"/>
|
||||||
|
<source>File list.</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
</TS>
|
||||||
@@ -12,82 +12,113 @@
|
|||||||
<context>
|
<context>
|
||||||
<name>MainWindow</name>
|
<name>MainWindow</name>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="93"/>
|
<location filename="../mainwindow.cpp" line="125"/>
|
||||||
<source>Mono</source>
|
<source>Mono</source>
|
||||||
<translation>单声道</translation>
|
<translation>单声道</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="95"/>
|
<location filename="../mainwindow.cpp" line="127"/>
|
||||||
<source>Stereo</source>
|
<source>Stereo</source>
|
||||||
<translation>立体声</translation>
|
<translation>立体声</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="97"/>
|
<location filename="../mainwindow.cpp" line="129"/>
|
||||||
<source>%1 Channels</source>
|
<source>%1 Channels</source>
|
||||||
<translation>%1 声道</translation>
|
<translation>%1 声道</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="241"/>
|
<location filename="../mainwindow.cpp" line="298"/>
|
||||||
<source>Select songs to play</source>
|
<source>Select songs to play</source>
|
||||||
<translation>选择要播放的曲目</translation>
|
<translation>选择要播放的曲目</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="243"/>
|
<location filename="../mainwindow.cpp" line="300"/>
|
||||||
<source>Audio Files</source>
|
<source>Audio Files</source>
|
||||||
<translation>音频文件</translation>
|
<translation>音频文件</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="576"/>
|
<location filename="../mainwindow.cpp" line="685"/>
|
||||||
<source>Select image as background skin</source>
|
<source>Select image as background skin</source>
|
||||||
<translation>选择图片作为背景皮肤</translation>
|
<translation>选择图片作为背景皮肤</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="578"/>
|
<location filename="../mainwindow.cpp" line="687"/>
|
||||||
<source>Image files (*.jpg *.jpeg *.png *.gif)</source>
|
<source>Image files (*.jpg *.jpeg *.png *.gif)</source>
|
||||||
<translation>图片文件 (*.jpg *.jpeg *.png *.gif)</translation>
|
<translation>图片文件 (*.jpg *.jpeg *.png *.gif)</translation>
|
||||||
</message>
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="764"/>
|
||||||
|
<source>Based on the following free software libraries:</source>
|
||||||
|
<translation>基于下列自由软件库:</translation>
|
||||||
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.ui" line="23"/>
|
<location filename="../mainwindow.ui" line="23"/>
|
||||||
|
<location filename="../mainwindow.cpp" line="762"/>
|
||||||
<location filename="../lrcbar.cpp" line="89"/>
|
<location filename="../lrcbar.cpp" line="89"/>
|
||||||
<source>Pineapple Music</source>
|
<source>Pineapple Music</source>
|
||||||
<translation>菠萝音乐</translation>
|
<translation>菠萝音乐</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.ui" line="313"/>
|
<location filename="../mainwindow.ui" line="327"/>
|
||||||
<source>No song loaded...</source>
|
<source>No song loaded...</source>
|
||||||
<translation>未加载曲目...</translation>
|
<translation>未加载曲目...</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.ui" line="325"/>
|
<location filename="../mainwindow.ui" line="339"/>
|
||||||
<source>Drag and drop file to load</source>
|
<source>Drag and drop file to load</source>
|
||||||
<translation>拖放文件来播放</translation>
|
<translation>拖放文件来播放</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.ui" line="338"/>
|
<location filename="../mainwindow.ui" line="352"/>
|
||||||
<source>Lrc</source>
|
<source>Lrc</source>
|
||||||
<comment>Lyrics</comment>
|
<comment>Lyrics</comment>
|
||||||
<translation>歌词</translation>
|
<translation>歌词</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="103"/>
|
<location filename="../mainwindow.ui" line="711"/>
|
||||||
|
<location filename="../mainwindow.ui" line="714"/>
|
||||||
|
<location filename="../mainwindow.cpp" line="759"/>
|
||||||
|
<source>About</source>
|
||||||
|
<translation>关于</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.ui" line="728"/>
|
||||||
|
<source>Open</source>
|
||||||
|
<translation>打开</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../mainwindow.cpp" line="135"/>
|
||||||
<source>Sample Rate: %1 Hz</source>
|
<source>Sample Rate: %1 Hz</source>
|
||||||
<translation>采样率: %1 Hz</translation>
|
<translation>采样率: %1 Hz</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="108"/>
|
<location filename="../mainwindow.cpp" line="140"/>
|
||||||
<source>Bitrate: %1 Kbps</source>
|
<source>Bitrate: %1 Kbps</source>
|
||||||
<translation>比特率: %1 Kbps</translation>
|
<translation>比特率: %1 Kbps</translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../mainwindow.cpp" line="113"/>
|
<location filename="../mainwindow.cpp" line="145"/>
|
||||||
<source>Channel Count: %1</source>
|
<source>Channel Count: %1</source>
|
||||||
<translation>声道数: %1</translation>
|
<translation>声道数: %1</translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>PlaybackProgressIndicator</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../playbackprogressindicator.cpp" line="85"/>
|
||||||
|
<source>Time</source>
|
||||||
|
<translation>时间</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../playbackprogressindicator.cpp" line="85"/>
|
||||||
|
<source>Chapter Name</source>
|
||||||
|
<translation>章节名称</translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>main</name>
|
<name>main</name>
|
||||||
<message>
|
<message>
|
||||||
<location filename="../main.cpp" line="28"/>
|
<location filename="../main.cpp" line="27"/>
|
||||||
<source>File list.</source>
|
<source>File list.</source>
|
||||||
<translation>文件列表。</translation>
|
<translation>文件列表。</translation>
|
||||||
</message>
|
</message>
|
||||||
|
|||||||
+9
-11
@@ -10,9 +10,9 @@
|
|||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QWindow>
|
#include <QWindow>
|
||||||
|
|
||||||
LrcBar::LrcBar(QWidget *parent)
|
LrcBar::LrcBar(QWidget *parent, LyricsManager *mgr)
|
||||||
: QWidget(parent, Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::Tool)
|
: QWidget(parent, Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::Tool)
|
||||||
, m_lrcMgr(new LyricsManager(this))
|
, m_lrcMgr(mgr)
|
||||||
{
|
{
|
||||||
m_font.setPointSize(30);
|
m_font.setPointSize(30);
|
||||||
m_font.setStyleStrategy(QFont::PreferAntialias);
|
m_font.setStyleStrategy(QFont::PreferAntialias);
|
||||||
@@ -32,9 +32,14 @@ LrcBar::LrcBar(QWidget *parent)
|
|||||||
|
|
||||||
setAttribute(Qt::WA_TranslucentBackground);
|
setAttribute(Qt::WA_TranslucentBackground);
|
||||||
setMouseTracking(true);
|
setMouseTracking(true);
|
||||||
setGeometry(QRect(QPoint((qApp->primaryScreen()->geometry().width() - windowSize.width()) / 2,
|
setGeometry(QRect(QPoint((qApp->primaryScreen()->availableGeometry().width() - windowSize.width()) / 2,
|
||||||
qApp->primaryScreen()->geometry().height() - windowSize.height() - 50),
|
qApp->primaryScreen()->availableGeometry().bottom() - windowSize.height() - 5),
|
||||||
windowSize));
|
windowSize));
|
||||||
|
|
||||||
|
connect(m_lrcMgr, &LyricsManager::lyricsLoaded, this, [this](bool){
|
||||||
|
m_currentTimeMs = 0;
|
||||||
|
update();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
LrcBar::~LrcBar()
|
LrcBar::~LrcBar()
|
||||||
@@ -42,18 +47,11 @@ LrcBar::~LrcBar()
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool LrcBar::loadLyrics(QString filepath)
|
|
||||||
{
|
|
||||||
m_currentTimeMs = 0;
|
|
||||||
return m_lrcMgr->loadLyrics(filepath);
|
|
||||||
}
|
|
||||||
|
|
||||||
void LrcBar::playbackPositionChanged(int timestampMs, int totalTimeMs)
|
void LrcBar::playbackPositionChanged(int timestampMs, int totalTimeMs)
|
||||||
{
|
{
|
||||||
if (!isVisible()) return;
|
if (!isVisible()) return;
|
||||||
|
|
||||||
m_currentTimeMs = timestampMs;
|
m_currentTimeMs = timestampMs;
|
||||||
m_lrcMgr->updateCurrentTimeMs(timestampMs, totalTimeMs);
|
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +1,35 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QLinearGradient>
|
#include <QLinearGradient>
|
||||||
#include <QMediaPlayer>
|
#include <QMediaPlayer>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
class LyricsManager;
|
class LyricsManager;
|
||||||
class LrcBar : public QWidget
|
class LrcBar : public QWidget
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit LrcBar(QWidget *parent);
|
explicit LrcBar(QWidget *parent, LyricsManager *mgr);
|
||||||
~LrcBar();
|
~LrcBar();
|
||||||
|
|
||||||
bool loadLyrics(QString filepath);
|
void playbackPositionChanged(int timestampMs, int totalTimeMs);
|
||||||
void playbackPositionChanged(int timestampMs, int totalTimeMs);
|
|
||||||
|
protected:
|
||||||
protected:
|
QSize sizeHint() const override;
|
||||||
QSize sizeHint() const override;
|
void mouseMoveEvent(QMouseEvent *event) override;
|
||||||
void mouseMoveEvent(QMouseEvent *event) override;
|
void paintEvent(QPaintEvent *) override;
|
||||||
void paintEvent(QPaintEvent *) override;
|
void enterEvent(QEnterEvent *) override;
|
||||||
void enterEvent(QEnterEvent *) override;
|
void leaveEvent(QEvent *) override;
|
||||||
void leaveEvent(QEvent *) override;
|
|
||||||
|
private:
|
||||||
private:
|
int m_currentTimeMs = 0;
|
||||||
int m_currentTimeMs = 0;
|
LyricsManager * m_lrcMgr;
|
||||||
LyricsManager * m_lrcMgr;
|
QFont m_font;
|
||||||
QFont m_font;
|
QLinearGradient m_baseLinearGradient;
|
||||||
QLinearGradient m_baseLinearGradient;
|
QLinearGradient m_maskLinearGradient;
|
||||||
QLinearGradient m_maskLinearGradient;
|
bool m_underMouse = false;
|
||||||
bool m_underMouse = false;
|
};
|
||||||
};
|
|
||||||
|
|||||||
+24
-1
@@ -43,10 +43,14 @@ bool LyricsManager::loadLyrics(QString filepath)
|
|||||||
if (!filepath.endsWith(".lrc", Qt::CaseInsensitive)) {
|
if (!filepath.endsWith(".lrc", Qt::CaseInsensitive)) {
|
||||||
fileInfo.setFile(fileInfo.dir().filePath(fileInfo.completeBaseName() + ".lrc"));
|
fileInfo.setFile(fileInfo.dir().filePath(fileInfo.completeBaseName() + ".lrc"));
|
||||||
}
|
}
|
||||||
if (!fileInfo.exists()) return false;
|
if (!fileInfo.exists()) {
|
||||||
|
emit lyricsLoaded(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
QFile file(fileInfo.absoluteFilePath());
|
QFile file(fileInfo.absoluteFilePath());
|
||||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
|
emit lyricsLoaded(false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
QByteArray fileContent(file.readAll());
|
QByteArray fileContent(file.readAll());
|
||||||
@@ -126,9 +130,11 @@ bool LyricsManager::loadLyrics(QString filepath)
|
|||||||
if (!m_lyricsMap.isEmpty()) {
|
if (!m_lyricsMap.isEmpty()) {
|
||||||
m_timestampList = m_lyricsMap.keys();
|
m_timestampList = m_lyricsMap.keys();
|
||||||
std::sort(m_timestampList.begin(), m_timestampList.end());
|
std::sort(m_timestampList.begin(), m_timestampList.end());
|
||||||
|
emit lyricsLoaded(true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emit lyricsLoaded(false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +157,22 @@ void LyricsManager::updateCurrentTimeMs(int curTimeMs, int totalTimeMs)
|
|||||||
iter--;
|
iter--;
|
||||||
}
|
}
|
||||||
m_currentLyricsTime = *iter;
|
m_currentLyricsTime = *iter;
|
||||||
|
m_currentLineIndex = std::distance(m_timestampList.begin(), iter);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QList<int>& LyricsManager::timestamps() const
|
||||||
|
{
|
||||||
|
return m_timestampList;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QHash<int, QString>& LyricsManager::lyricsMap() const
|
||||||
|
{
|
||||||
|
return m_lyricsMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
int LyricsManager::currentLineIndex() const
|
||||||
|
{
|
||||||
|
return m_currentLineIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString LyricsManager::lyrics(int lineOffset) const
|
QString LyricsManager::lyrics(int lineOffset) const
|
||||||
@@ -197,6 +219,7 @@ void LyricsManager::reset()
|
|||||||
{
|
{
|
||||||
m_currentLyricsTime = 0;
|
m_currentLyricsTime = 0;
|
||||||
m_nextLyricsTime = 0;
|
m_nextLyricsTime = 0;
|
||||||
|
m_currentLineIndex = -1;
|
||||||
m_timeOffset = 0;
|
m_timeOffset = 0;
|
||||||
m_lyricsMap.clear();
|
m_lyricsMap.clear();
|
||||||
m_timestampList.clear();
|
m_timestampList.clear();
|
||||||
|
|||||||
+52
-43
@@ -1,43 +1,52 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QLoggingCategory>
|
#include <QLoggingCategory>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
|
||||||
Q_DECLARE_LOGGING_CATEGORY(lcLyrics)
|
Q_DECLARE_LOGGING_CATEGORY(lcLyrics)
|
||||||
Q_DECLARE_LOGGING_CATEGORY(lcLyricsParser)
|
Q_DECLARE_LOGGING_CATEGORY(lcLyricsParser)
|
||||||
|
|
||||||
class LyricsManager : public QObject
|
class LyricsManager : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit LyricsManager(QObject *parent);
|
explicit LyricsManager(QObject *parent);
|
||||||
~LyricsManager();
|
~LyricsManager();
|
||||||
|
|
||||||
bool loadLyrics(QString filepath);
|
bool loadLyrics(QString filepath);
|
||||||
bool hasLyrics() const;
|
bool hasLyrics() const;
|
||||||
void updateCurrentTimeMs(int curTimeMs, int totalTimeMs);
|
void updateCurrentTimeMs(int curTimeMs, int totalTimeMs);
|
||||||
QString lyrics(int lineOffset = 0) const;
|
QString lyrics(int lineOffset = 0) const;
|
||||||
double maskPercent(int curTimeMs);
|
double maskPercent(int curTimeMs);
|
||||||
|
|
||||||
static int parseTimeToMilliseconds(const QString& timeString);
|
const QList<int>& timestamps() const;
|
||||||
|
const QHash<int, QString>& lyricsMap() const;
|
||||||
protected:
|
int currentLineIndex() const;
|
||||||
|
|
||||||
|
static int parseTimeToMilliseconds(const QString& timeString);
|
||||||
private:
|
|
||||||
void reset();
|
signals:
|
||||||
int currentLyricsTime() const;
|
void lyricsLoaded(bool success);
|
||||||
int nextLyricsTime() const;
|
|
||||||
|
|
||||||
QHash<int, QString> m_lyricsMap;
|
protected:
|
||||||
QList<int> m_timestampList;
|
|
||||||
int m_currentLyricsTime = 0;
|
|
||||||
int m_nextLyricsTime = 0;
|
private:
|
||||||
int m_timeOffset = 0;
|
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_currentLineIndex = -1;
|
||||||
|
int m_timeOffset = 0;
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026 Gary Wang <opensource@blumia.net>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#include "lyricswidget.h"
|
||||||
|
#include "lyricsmanager.h"
|
||||||
|
|
||||||
|
#include <QListWidget>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QListWidgetItem>
|
||||||
|
#include <QListWidgetItem>
|
||||||
|
#include <QFont>
|
||||||
|
#include <QPropertyAnimation>
|
||||||
|
#include <QScrollBar>
|
||||||
|
#include <QEasingCurve>
|
||||||
|
|
||||||
|
LyricsWidget::LyricsWidget(QWidget *parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
, m_listWidget(new QListWidget(this))
|
||||||
|
{
|
||||||
|
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||||
|
layout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
layout->addWidget(m_listWidget);
|
||||||
|
|
||||||
|
m_listWidget->setFrameShape(QFrame::NoFrame);
|
||||||
|
m_listWidget->setStyleSheet("background: transparent;");
|
||||||
|
m_listWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
||||||
|
m_listWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||||
|
m_listWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Hide scrollbar for cleaner look? Or Keep it? User didn't specify. Hiding it looks more like "Lyrics Mode"
|
||||||
|
// Enable word wrap
|
||||||
|
m_listWidget->setWordWrap(true);
|
||||||
|
m_listWidget->setTextElideMode(Qt::ElideNone);
|
||||||
|
m_listWidget->setSelectionMode(QAbstractItemView::NoSelection);
|
||||||
|
m_listWidget->setFocusPolicy(Qt::NoFocus);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LyricsWidget::setLyricsManager(LyricsManager *mgr)
|
||||||
|
{
|
||||||
|
if (m_lyricsManager) {
|
||||||
|
disconnect(m_lyricsManager, nullptr, this, nullptr);
|
||||||
|
}
|
||||||
|
m_lyricsManager = mgr;
|
||||||
|
if (m_lyricsManager) {
|
||||||
|
connect(m_lyricsManager, &LyricsManager::lyricsLoaded, this, &LyricsWidget::onLyricsLoaded);
|
||||||
|
// Load initial state if any
|
||||||
|
onLyricsLoaded(m_lyricsManager->hasLyrics());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LyricsWidget::updatePosition(qint64 position)
|
||||||
|
{
|
||||||
|
if (!m_lyricsManager || !m_lyricsManager->hasLyrics() || !isVisible()) return;
|
||||||
|
|
||||||
|
// Note: rely on LyricsManager::updateCurrentTimeMs()
|
||||||
|
int index = m_lyricsManager->currentLineIndex();
|
||||||
|
|
||||||
|
if (index != m_lastHighlightIndex) {
|
||||||
|
highlightCurrentLine(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LyricsWidget::onLyricsLoaded(bool success)
|
||||||
|
{
|
||||||
|
m_listWidget->clear();
|
||||||
|
m_lastHighlightIndex = -1;
|
||||||
|
|
||||||
|
if (!success || !m_lyricsManager) return;
|
||||||
|
|
||||||
|
const QList<int>& timestamps = m_lyricsManager->timestamps();
|
||||||
|
const QHash<int, QString>& lyricsMap = m_lyricsManager->lyricsMap();
|
||||||
|
|
||||||
|
for (int timestamp : timestamps) {
|
||||||
|
QListWidgetItem *item = new QListWidgetItem(lyricsMap.value(timestamp));
|
||||||
|
item->setTextAlignment(Qt::AlignCenter);
|
||||||
|
|
||||||
|
// Default style
|
||||||
|
QFont font = item->font();
|
||||||
|
font.setPointSize(10);
|
||||||
|
item->setFont(font);
|
||||||
|
item->setForeground(QColor(255, 255, 255, 150)); // Dimmed
|
||||||
|
|
||||||
|
m_listWidget->addItem(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LyricsWidget::highlightCurrentLine(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= m_listWidget->count()) return;
|
||||||
|
|
||||||
|
// Reset old highlight
|
||||||
|
if (m_lastHighlightIndex >= 0 && m_lastHighlightIndex < m_listWidget->count()) {
|
||||||
|
QListWidgetItem *oldItem = m_listWidget->item(m_lastHighlightIndex);
|
||||||
|
QFont font = oldItem->font();
|
||||||
|
font.setPointSize(10);
|
||||||
|
font.setBold(false);
|
||||||
|
oldItem->setFont(font);
|
||||||
|
oldItem->setForeground(QColor(255, 255, 255, 150));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new highlight
|
||||||
|
QListWidgetItem *newItem = m_listWidget->item(index);
|
||||||
|
QFont font = newItem->font();
|
||||||
|
font.setPointSize(14);
|
||||||
|
font.setBold(true);
|
||||||
|
newItem->setFont(font);
|
||||||
|
newItem->setForeground(QColor(255, 255, 255, 255));
|
||||||
|
|
||||||
|
// Smooth scroll
|
||||||
|
QScrollBar *vBar = m_listWidget->verticalScrollBar();
|
||||||
|
int startScroll = vBar->value();
|
||||||
|
|
||||||
|
m_listWidget->scrollToItem(newItem, QAbstractItemView::PositionAtCenter);
|
||||||
|
int endScroll = vBar->value();
|
||||||
|
|
||||||
|
// Restore and animate
|
||||||
|
if (startScroll != endScroll) {
|
||||||
|
vBar->setValue(startScroll);
|
||||||
|
|
||||||
|
if (!m_scrollAnimation) {
|
||||||
|
m_scrollAnimation = new QPropertyAnimation(vBar, "value", this);
|
||||||
|
m_scrollAnimation->setDuration(400);
|
||||||
|
m_scrollAnimation->setEasingCurve(QEasingCurve::OutCubic);
|
||||||
|
}
|
||||||
|
m_scrollAnimation->stop();
|
||||||
|
m_scrollAnimation->setStartValue(startScroll);
|
||||||
|
m_scrollAnimation->setEndValue(endScroll);
|
||||||
|
m_scrollAnimation->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_lastHighlightIndex = index;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026 Gary Wang <opensource@blumia.net>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
QT_BEGIN_NAMESPACE
|
||||||
|
class QListWidget;
|
||||||
|
class QListWidgetItem;
|
||||||
|
class QPropertyAnimation;
|
||||||
|
QT_END_NAMESPACE
|
||||||
|
|
||||||
|
class LyricsManager;
|
||||||
|
class LyricsWidget : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit LyricsWidget(QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
void setLyricsManager(LyricsManager *mgr);
|
||||||
|
void updatePosition(qint64 position);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onLyricsLoaded(bool success);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QListWidget *m_listWidget;
|
||||||
|
LyricsManager *m_lyricsManager = nullptr;
|
||||||
|
int m_lastHighlightIndex = -1;
|
||||||
|
QPropertyAnimation *m_scrollAnimation = nullptr;
|
||||||
|
|
||||||
|
void updateLyrics();
|
||||||
|
void highlightCurrentLine(int index);
|
||||||
|
};
|
||||||
|
|
||||||
@@ -13,8 +13,25 @@
|
|||||||
#include <QTranslator>
|
#include <QTranslator>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
#include <windows.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
typedef HRESULT(WINAPI *SetCurrentProcessExplicitAppUserModelIDProc)(PCWSTR);
|
||||||
|
HMODULE shell32 = LoadLibraryW(L"shell32.dll");
|
||||||
|
if (shell32) {
|
||||||
|
auto proc = reinterpret_cast<SetCurrentProcessExplicitAppUserModelIDProc>(
|
||||||
|
GetProcAddress(shell32, "SetCurrentProcessExplicitAppUserModelID"));
|
||||||
|
if (proc) {
|
||||||
|
proc(L"net.blumia.pineapple-music");
|
||||||
|
}
|
||||||
|
FreeLibrary(shell32);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
QApplication a(argc, argv);
|
QApplication a(argc, argv);
|
||||||
|
|
||||||
QTranslator translator;
|
QTranslator translator;
|
||||||
|
|||||||
+164
-29
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
// SPDX-FileCopyrightText: 2026 Gary Wang <opensource@blumia.net>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
@@ -9,6 +9,9 @@
|
|||||||
#include "fftspectrum.h"
|
#include "fftspectrum.h"
|
||||||
#include "lrcbar.h"
|
#include "lrcbar.h"
|
||||||
#include "taskbarmanager.h"
|
#include "taskbarmanager.h"
|
||||||
|
#include "winmediacontrol.h"
|
||||||
|
#include "lyricsmanager.h"
|
||||||
|
#include "lyricswidget.h"
|
||||||
|
|
||||||
// taglib
|
// taglib
|
||||||
#ifndef NO_TAGLIB
|
#ifndef NO_TAGLIB
|
||||||
@@ -27,6 +30,7 @@
|
|||||||
#include <QListView>
|
#include <QListView>
|
||||||
#include <QCollator>
|
#include <QCollator>
|
||||||
#include <QMimeData>
|
#include <QMimeData>
|
||||||
|
#include <QMenu>
|
||||||
#include <QWindow>
|
#include <QWindow>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
#include <QMediaDevices>
|
#include <QMediaDevices>
|
||||||
@@ -36,6 +40,7 @@
|
|||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
#include <QGraphicsDropShadowEffect>
|
#include <QGraphicsDropShadowEffect>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
#include <QFileInfo>
|
||||||
|
|
||||||
constexpr QSize miniSize(490, 160);
|
constexpr QSize miniSize(490, 160);
|
||||||
constexpr QSize fullSize(490, 420);
|
constexpr QSize fullSize(490, 420);
|
||||||
@@ -47,11 +52,17 @@ MainWindow::MainWindow(QWidget *parent)
|
|||||||
, m_mediaPlayer(new QMediaPlayer(this))
|
, m_mediaPlayer(new QMediaPlayer(this))
|
||||||
, m_audioOutput(new QAudioOutput(this))
|
, m_audioOutput(new QAudioOutput(this))
|
||||||
, m_fftSpectrum(new FFTSpectrum(this))
|
, m_fftSpectrum(new FFTSpectrum(this))
|
||||||
, m_lrcbar(new LrcBar(nullptr))
|
, m_lyricsManager(new LyricsManager(this))
|
||||||
|
, m_lrcbar(new LrcBar(nullptr, m_lyricsManager))
|
||||||
|
, m_lyricsWidget(new LyricsWidget(this))
|
||||||
, m_playlistManager(new PlaylistManager(this))
|
, m_playlistManager(new PlaylistManager(this))
|
||||||
, m_taskbarManager(new TaskBarManager(this))
|
, m_taskbarManager(new TaskBarManager(this))
|
||||||
|
, m_mediaControl(new WinMediaControl(this))
|
||||||
{
|
{
|
||||||
ui->setupUi(this);
|
ui->setupUi(this);
|
||||||
|
m_lyricsWidget->setLyricsManager(m_lyricsManager);
|
||||||
|
ui->pluginStackedWidget->addWidget(m_lyricsWidget);
|
||||||
|
|
||||||
m_playlistManager->setAutoLoadFilterSuffixes({
|
m_playlistManager->setAutoLoadFilterSuffixes({
|
||||||
"*.mp3", "*.wav", "*.aiff", "*.ape", "*.flac", "*.ogg", "*.oga", "*.mpga", "*.aac", "*.tta"
|
"*.mp3", "*.wav", "*.aiff", "*.ape", "*.flac", "*.ogg", "*.oga", "*.mpga", "*.aac", "*.tta"
|
||||||
});
|
});
|
||||||
@@ -60,6 +71,10 @@ MainWindow::MainWindow(QWidget *parent)
|
|||||||
m_mediaPlayer->setLoops(QMediaPlayer::Infinite);
|
m_mediaPlayer->setLoops(QMediaPlayer::Infinite);
|
||||||
ui->playlistView->setModel(m_playlistManager->model());
|
ui->playlistView->setModel(m_playlistManager->model());
|
||||||
|
|
||||||
|
ui->chapterNameBtn->setVisible(false);
|
||||||
|
ui->chapterlistView->setModel(ui->playbackProgressIndicator->chapterModel());
|
||||||
|
ui->chapterlistView->setRootIsDecorated(false);
|
||||||
|
|
||||||
ui->actionHelp->setShortcut(QKeySequence::HelpContents);
|
ui->actionHelp->setShortcut(QKeySequence::HelpContents);
|
||||||
addAction(ui->actionHelp);
|
addAction(ui->actionHelp);
|
||||||
ui->actionOpen->setShortcut(QKeySequence::Open);
|
ui->actionOpen->setShortcut(QKeySequence::Open);
|
||||||
@@ -86,7 +101,9 @@ MainWindow::MainWindow(QWidget *parent)
|
|||||||
centerWindow();
|
centerWindow();
|
||||||
|
|
||||||
QTimer::singleShot(1000, [this](){
|
QTimer::singleShot(1000, [this](){
|
||||||
m_taskbarManager->setWinId(window()->winId());
|
auto winId = window()->winId();
|
||||||
|
m_taskbarManager->setWinId(winId);
|
||||||
|
m_mediaControl->initialize(reinterpret_cast<void*>(winId));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,15 +168,23 @@ void MainWindow::setAudioMetadataForDisplay(QString title, QString artist, QStri
|
|||||||
{
|
{
|
||||||
Q_UNUSED(album);
|
Q_UNUSED(album);
|
||||||
|
|
||||||
|
if (title.isEmpty()) {
|
||||||
|
title = QFileInfo(m_mediaPlayer->source().fileName()).completeBaseName();
|
||||||
|
}
|
||||||
|
|
||||||
if (!title.isEmpty()) {
|
if (!title.isEmpty()) {
|
||||||
if (!artist.isEmpty()) {
|
if (!artist.isEmpty()) {
|
||||||
ui->titleLabel->setText(QString("%1 - %2").arg(artist, title));
|
title = QString("%1 - %2").arg(artist, title);
|
||||||
} else if (!album.isEmpty()) {
|
} else if (!album.isEmpty()) {
|
||||||
ui->titleLabel->setText(QString("%1 - %2").arg(album, title));
|
title = QString("%1 - %2").arg(album, title);
|
||||||
} else {
|
} else {
|
||||||
ui->titleLabel->setText(QString("%1").arg(title));
|
title = QString("%1").arg(title);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ui->titleLabel->setText(title);
|
||||||
|
|
||||||
|
m_mediaControl->setMetadata(title, artist, album);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::localSocketPlayAudioFiles(QVariant audioFilesVariant)
|
void MainWindow::localSocketPlayAudioFiles(QVariant audioFilesVariant)
|
||||||
@@ -250,7 +275,7 @@ void MainWindow::dropEvent(QDropEvent *e)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (fileName.endsWith(".lrc")) {
|
if (fileName.endsWith(".lrc")) {
|
||||||
m_lrcbar->loadLyrics(fileName);
|
m_lyricsManager->loadLyrics(fileName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,6 +285,15 @@ void MainWindow::dropEvent(QDropEvent *e)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fileName.endsWith(".m3u") || fileName.endsWith(".m3u8")) {
|
||||||
|
const QModelIndex & modelIndex = m_playlistManager->loadM3U8Playlist(urls.constFirst());
|
||||||
|
if (modelIndex.isValid()) {
|
||||||
|
loadByModelIndex(modelIndex);
|
||||||
|
play();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const QModelIndex & modelIndex = m_playlistManager->loadPlaylist(urls);
|
const QModelIndex & modelIndex = m_playlistManager->loadPlaylist(urls);
|
||||||
if (modelIndex.isValid()) {
|
if (modelIndex.isValid()) {
|
||||||
loadByModelIndex(modelIndex);
|
loadByModelIndex(modelIndex);
|
||||||
@@ -267,6 +301,16 @@ void MainWindow::dropEvent(QDropEvent *e)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MainWindow::contextMenuEvent(QContextMenuEvent *event)
|
||||||
|
{
|
||||||
|
QMenu * menu = new QMenu;
|
||||||
|
menu->addAction(ui->actionHelp);
|
||||||
|
menu->exec(mapToGlobal(event->pos()));
|
||||||
|
menu->deleteLater();
|
||||||
|
|
||||||
|
return QMainWindow::contextMenuEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
void MainWindow::loadFile()
|
void MainWindow::loadFile()
|
||||||
{
|
{
|
||||||
QStringList musicFolders(QStandardPaths::standardLocations(QStandardPaths::MusicLocation));
|
QStringList musicFolders(QStandardPaths::standardLocations(QStandardPaths::MusicLocation));
|
||||||
@@ -281,16 +325,15 @@ void MainWindow::loadFile()
|
|||||||
urlList.append(QUrl::fromLocalFile(fileName));
|
urlList.append(QUrl::fromLocalFile(fileName));
|
||||||
}
|
}
|
||||||
|
|
||||||
m_playlistManager->loadPlaylist(urlList);
|
const QModelIndex & modelIndex = m_playlistManager->loadPlaylist(urlList);
|
||||||
const QUrl & firstUrl = urlList.first();
|
loadByModelIndex(modelIndex);
|
||||||
loadFile(firstUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::loadFile(const QUrl &url)
|
void MainWindow::loadFile(const QUrl &url)
|
||||||
{
|
{
|
||||||
const QString filePath = url.toLocalFile();
|
const QString filePath = url.toLocalFile();
|
||||||
m_mediaPlayer->setSource(url);
|
m_mediaPlayer->setSource(url);
|
||||||
m_lrcbar->loadLyrics(filePath);
|
m_lyricsManager->loadLyrics(filePath);
|
||||||
QList<std::pair<qint64, QString>> chapters(PlaybackProgressIndicator::tryLoadChapters(filePath));
|
QList<std::pair<qint64, QString>> chapters(PlaybackProgressIndicator::tryLoadChapters(filePath));
|
||||||
ui->playbackProgressIndicator->setChapters(chapters);
|
ui->playbackProgressIndicator->setChapters(chapters);
|
||||||
}
|
}
|
||||||
@@ -349,16 +392,6 @@ void MainWindow::on_playBtn_clicked()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QString MainWindow::ms2str(qint64 ms)
|
|
||||||
{
|
|
||||||
QTime duaTime(QTime::fromMSecsSinceStartOfDay(ms));
|
|
||||||
if (duaTime.hour() > 0) {
|
|
||||||
return duaTime.toString("h:mm:ss");
|
|
||||||
} else {
|
|
||||||
return duaTime.toString("m:ss");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QList<QUrl> MainWindow::strlst2urllst(QStringList strlst)
|
QList<QUrl> MainWindow::strlst2urllst(QStringList strlst)
|
||||||
{
|
{
|
||||||
QList<QUrl> urlList;
|
QList<QUrl> urlList;
|
||||||
@@ -490,10 +523,14 @@ void MainWindow::initConnections()
|
|||||||
}
|
}
|
||||||
QVariant coverArt(metadata.value(QMediaMetaData::ThumbnailImage));
|
QVariant coverArt(metadata.value(QMediaMetaData::ThumbnailImage));
|
||||||
if (!coverArt.isNull()) {
|
if (!coverArt.isNull()) {
|
||||||
ui->coverLabel->setPixmap(QPixmap::fromImage(coverArt.value<QImage>()));
|
QImage coverImage = coverArt.value<QImage>();
|
||||||
|
ui->coverLabel->setPixmap(QPixmap::fromImage(coverImage));
|
||||||
|
m_mediaControl->setCoverArt(coverImage);
|
||||||
} else {
|
} else {
|
||||||
qDebug() << "No ThumbnailImage!" << metadata.keys();
|
qDebug() << "No ThumbnailImage!" << metadata.keys();
|
||||||
ui->coverLabel->setPixmap(QPixmap(":/icons/icons/media-album-cover.svg"));
|
ui->coverLabel->setPixmap(QPixmap(":/icons/icons/media-album-cover.svg"));
|
||||||
|
QIcon appIcon(QStringLiteral(":/icons/icons/app-icon.svg"));
|
||||||
|
m_mediaControl->setCoverArt(appIcon.pixmap(QSize(256, 256)).toImage());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
connect(m_playlistManager, &PlaylistManager::currentIndexChanged, this, [=](int index){
|
connect(m_playlistManager, &PlaylistManager::currentIndexChanged, this, [=](int index){
|
||||||
@@ -501,12 +538,34 @@ void MainWindow::initConnections()
|
|||||||
});
|
});
|
||||||
|
|
||||||
connect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, [=](qint64 pos) {
|
connect(m_mediaPlayer, &QMediaPlayer::positionChanged, this, [=](qint64 pos) {
|
||||||
ui->nowTimeLabel->setText(ms2str(pos));
|
ui->nowTimeLabel->setText(PlaybackProgressIndicator::formatTime(pos));
|
||||||
if (m_mediaPlayer->duration() != 0) {
|
if (m_mediaPlayer->duration() != 0) {
|
||||||
ui->playbackProgressIndicator->setPosition(pos);
|
ui->playbackProgressIndicator->setPosition(pos);
|
||||||
m_taskbarManager->setProgressValue(pos);
|
m_taskbarManager->setProgressValue(pos);
|
||||||
|
m_mediaControl->setPosition(pos, m_mediaPlayer->duration());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_lrcbar->isVisible() || m_lyricsWidget->isVisible()) {
|
||||||
|
m_lyricsManager->updateCurrentTimeMs(pos, m_mediaPlayer->duration());
|
||||||
|
m_lrcbar->playbackPositionChanged(pos, m_mediaPlayer->duration());
|
||||||
|
m_lyricsWidget->updatePosition(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString lastChapterName;
|
||||||
|
if (ui->playbackProgressIndicator->chapterModel()->rowCount() > 0) {
|
||||||
|
QString currentChapterName = ui->playbackProgressIndicator->currentChapterName();
|
||||||
|
if (currentChapterName != lastChapterName) {
|
||||||
|
ui->chapterNameBtn->setText(currentChapterName);
|
||||||
|
lastChapterName = currentChapterName;
|
||||||
|
}
|
||||||
|
ui->chapterNameBtn->setVisible(true);
|
||||||
|
} else {
|
||||||
|
if (!lastChapterName.isEmpty()) {
|
||||||
|
ui->chapterNameBtn->setText("");
|
||||||
|
lastChapterName.clear();
|
||||||
|
}
|
||||||
|
ui->chapterNameBtn->setVisible(false);
|
||||||
}
|
}
|
||||||
m_lrcbar->playbackPositionChanged(pos, m_mediaPlayer->duration());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(m_audioOutput, &QAudioOutput::mutedChanged, this, [=](bool muted) {
|
connect(m_audioOutput, &QAudioOutput::mutedChanged, this, [=](bool muted) {
|
||||||
@@ -520,17 +579,22 @@ void MainWindow::initConnections()
|
|||||||
connect(m_mediaPlayer, &QMediaPlayer::durationChanged, this, [=](qint64 dua) {
|
connect(m_mediaPlayer, &QMediaPlayer::durationChanged, this, [=](qint64 dua) {
|
||||||
ui->playbackProgressIndicator->setDuration(dua);
|
ui->playbackProgressIndicator->setDuration(dua);
|
||||||
m_taskbarManager->setProgressMaximum(dua);
|
m_taskbarManager->setProgressMaximum(dua);
|
||||||
ui->totalTimeLabel->setText(ms2str(dua));
|
ui->totalTimeLabel->setText(PlaybackProgressIndicator::formatTime(dua));
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(m_mediaPlayer, &QMediaPlayer::playbackStateChanged, this, [=](QMediaPlayer::PlaybackState newState) {
|
connect(m_mediaPlayer, &QMediaPlayer::playbackStateChanged, this, [=](QMediaPlayer::PlaybackState newState) {
|
||||||
switch (newState) {
|
switch (newState) {
|
||||||
case QMediaPlayer::PlayingState:
|
case QMediaPlayer::PlayingState:
|
||||||
ui->playBtn->setIcon(QIcon(":/icons/icons/media-playback-pause.png"));
|
ui->playBtn->setIcon(QIcon(":/icons/icons/media-playback-pause.png"));
|
||||||
|
m_mediaControl->setPlaybackState(1);
|
||||||
break;
|
break;
|
||||||
case QMediaPlayer::StoppedState:
|
case QMediaPlayer::StoppedState:
|
||||||
|
ui->playBtn->setIcon(QIcon(":/icons/icons/media-playback-start.png"));
|
||||||
|
m_mediaControl->setPlaybackState(0);
|
||||||
|
break;
|
||||||
case QMediaPlayer::PausedState:
|
case QMediaPlayer::PausedState:
|
||||||
ui->playBtn->setIcon(QIcon(":/icons/icons/media-playback-start.png"));
|
ui->playBtn->setIcon(QIcon(":/icons/icons/media-playback-start.png"));
|
||||||
|
m_mediaControl->setPlaybackState(2);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
m_taskbarManager->setPlaybackState(newState);
|
m_taskbarManager->setPlaybackState(newState);
|
||||||
@@ -586,6 +650,33 @@ void MainWindow::initConnections()
|
|||||||
on_nextBtn_clicked();
|
on_nextBtn_clicked();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
connect(m_mediaControl, &WinMediaControl::playRequested, this, [this](){
|
||||||
|
if (QList<QMediaPlayer::PlaybackState> {QMediaPlayer::PausedState, QMediaPlayer::StoppedState}
|
||||||
|
.contains(m_mediaPlayer->playbackState())) {
|
||||||
|
m_mediaPlayer->play();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(m_mediaControl, &WinMediaControl::pauseRequested, this, [this](){
|
||||||
|
m_mediaPlayer->pause();
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(m_mediaControl, &WinMediaControl::stopRequested, this, [this](){
|
||||||
|
m_mediaPlayer->stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(m_mediaControl, &WinMediaControl::nextRequested, this, [this](){
|
||||||
|
on_nextBtn_clicked();
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(m_mediaControl, &WinMediaControl::previousRequested, this, [this](){
|
||||||
|
on_prevBtn_clicked();
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(m_mediaControl, &WinMediaControl::seekRequested, this, [this](qint64 positionMs){
|
||||||
|
m_mediaPlayer->setPosition(positionMs);
|
||||||
|
});
|
||||||
|
|
||||||
connect(m_mediaPlayer, &QMediaPlayer::errorOccurred, this, [=](QMediaPlayer::Error error, const QString &errorString) {
|
connect(m_mediaPlayer, &QMediaPlayer::errorOccurred, this, [=](QMediaPlayer::Error error, const QString &errorString) {
|
||||||
qDebug() << error << errorString;
|
qDebug() << error << errorString;
|
||||||
});
|
});
|
||||||
@@ -668,7 +759,16 @@ void MainWindow::on_setSkinBtn_clicked()
|
|||||||
|
|
||||||
void MainWindow::on_playListBtn_clicked()
|
void MainWindow::on_playListBtn_clicked()
|
||||||
{
|
{
|
||||||
setFixedSize(size().height() < 200 ? fullSize : miniSize);
|
if (size().height() < 200) {
|
||||||
|
setFixedSize(fullSize);
|
||||||
|
ui->pluginStackedWidget->setCurrentWidget(ui->playlistViewPage);
|
||||||
|
} else {
|
||||||
|
if (ui->pluginStackedWidget->currentWidget() == ui->playlistViewPage) {
|
||||||
|
setFixedSize(miniSize);
|
||||||
|
} else {
|
||||||
|
ui->pluginStackedWidget->setCurrentWidget(ui->playlistViewPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::on_playlistView_activated(const QModelIndex &index)
|
void MainWindow::on_playlistView_activated(const QModelIndex &index)
|
||||||
@@ -680,6 +780,15 @@ void MainWindow::on_playlistView_activated(const QModelIndex &index)
|
|||||||
|
|
||||||
void MainWindow::on_lrcBtn_clicked()
|
void MainWindow::on_lrcBtn_clicked()
|
||||||
{
|
{
|
||||||
|
if (size().height() < 200) {
|
||||||
|
setFixedSize(fullSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ui->pluginStackedWidget->currentWidget() != m_lyricsWidget) {
|
||||||
|
ui->pluginStackedWidget->setCurrentWidget(m_lyricsWidget);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (m_lrcbar->isVisible()) {
|
if (m_lrcbar->isVisible()) {
|
||||||
m_lrcbar->hide();
|
m_lrcbar->hide();
|
||||||
} else {
|
} else {
|
||||||
@@ -687,6 +796,33 @@ void MainWindow::on_lrcBtn_clicked()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MainWindow::on_chapterlistView_activated(const QModelIndex &index)
|
||||||
|
{
|
||||||
|
if (!index.isValid()) return;
|
||||||
|
|
||||||
|
QModelIndex timeColumnIndex = index.sibling(index.row(), 0);
|
||||||
|
QStandardItem* timeItem = ui->playbackProgressIndicator->chapterModel()->itemFromIndex(timeColumnIndex);
|
||||||
|
if (!timeItem) return;
|
||||||
|
|
||||||
|
qint64 chapterStartTime = timeItem->data(PlaybackProgressIndicator::StartTimeMsRole).toLongLong();
|
||||||
|
m_mediaPlayer->setPosition(chapterStartTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::on_chapterNameBtn_clicked()
|
||||||
|
{
|
||||||
|
if (size().height() < 200) {
|
||||||
|
setFixedSize(fullSize);
|
||||||
|
}
|
||||||
|
ui->pluginStackedWidget->setCurrentWidget(ui->chaptersViewPage);
|
||||||
|
if (ui->playbackProgressIndicator->chapterModel()->rowCount() > 0) {
|
||||||
|
const QModelIndex & curChapterItem = ui->playbackProgressIndicator->currentChapterItem();
|
||||||
|
if (curChapterItem.isValid()) {
|
||||||
|
ui->chapterlistView->setCurrentIndex(curChapterItem);
|
||||||
|
ui->chapterlistView->scrollTo(curChapterItem, QAbstractItemView::EnsureVisible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void MainWindow::on_actionOpen_triggered()
|
void MainWindow::on_actionOpen_triggered()
|
||||||
{
|
{
|
||||||
loadFile();
|
loadFile();
|
||||||
@@ -699,7 +835,7 @@ void MainWindow::on_actionHelp_triggered()
|
|||||||
infoBox.setWindowTitle(tr("About"));
|
infoBox.setWindowTitle(tr("About"));
|
||||||
infoBox.setStandardButtons(QMessageBox::Ok);
|
infoBox.setStandardButtons(QMessageBox::Ok);
|
||||||
infoBox.setText(
|
infoBox.setText(
|
||||||
tr("Pineapple Music") %
|
QString("%1 %2").arg(tr("Pineapple Music")).arg(PMUSIC_VERSION_STRING) %
|
||||||
"\n\n" %
|
"\n\n" %
|
||||||
tr("Based on the following free software libraries:") %
|
tr("Based on the following free software libraries:") %
|
||||||
"\n\n" %
|
"\n\n" %
|
||||||
@@ -720,7 +856,7 @@ void MainWindow::on_actionHelp_triggered()
|
|||||||
"\n"
|
"\n"
|
||||||
"[Source Code](https://github.com/BLumia/pineapple-music)\n"
|
"[Source Code](https://github.com/BLumia/pineapple-music)\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Copyright © 2025 [BLumia](https://github.com/BLumia/)"
|
"Copyright © 2026 [BLumia](https://github.com/BLumia/)"
|
||||||
);
|
);
|
||||||
infoBox.setTextFormat(Qt::MarkdownText);
|
infoBox.setTextFormat(Qt::MarkdownText);
|
||||||
infoBox.exec();
|
infoBox.exec();
|
||||||
@@ -734,4 +870,3 @@ QGraphicsDropShadowEffect *MainWindow::createLabelShadowEffect()
|
|||||||
effect->setOffset(1, 1);
|
effect->setOffset(1, 1);
|
||||||
return effect;
|
return effect;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-1
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: 2024 Gary Wang <git@blumia.net>
|
// SPDX-FileCopyrightText: 2025 Gary Wang <opensource@blumia.net>
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
@@ -21,8 +21,11 @@ QT_END_NAMESPACE
|
|||||||
|
|
||||||
class FFTSpectrum;
|
class FFTSpectrum;
|
||||||
class LrcBar;
|
class LrcBar;
|
||||||
|
class LyricsManager;
|
||||||
|
class LyricsWidget;
|
||||||
class PlaylistManager;
|
class PlaylistManager;
|
||||||
class TaskBarManager;
|
class TaskBarManager;
|
||||||
|
class WinMediaControl;
|
||||||
class MainWindow : public QMainWindow
|
class MainWindow : public QMainWindow
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -55,6 +58,7 @@ protected:
|
|||||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||||
void dragEnterEvent(QDragEnterEvent *e) override;
|
void dragEnterEvent(QDragEnterEvent *e) override;
|
||||||
void dropEvent(QDropEvent *e) override;
|
void dropEvent(QDropEvent *e) override;
|
||||||
|
void contextMenuEvent(QContextMenuEvent *event) override;
|
||||||
|
|
||||||
void loadFile();
|
void loadFile();
|
||||||
void loadFile(const QUrl &url);
|
void loadFile(const QUrl &url);
|
||||||
@@ -79,6 +83,8 @@ private slots:
|
|||||||
void on_playListBtn_clicked();
|
void on_playListBtn_clicked();
|
||||||
void on_playlistView_activated(const QModelIndex &index);
|
void on_playlistView_activated(const QModelIndex &index);
|
||||||
void on_lrcBtn_clicked();
|
void on_lrcBtn_clicked();
|
||||||
|
void on_chapterlistView_activated(const QModelIndex &index);
|
||||||
|
void on_chapterNameBtn_clicked();
|
||||||
void on_actionOpen_triggered();
|
void on_actionOpen_triggered();
|
||||||
void on_actionHelp_triggered();
|
void on_actionHelp_triggered();
|
||||||
|
|
||||||
@@ -101,10 +107,13 @@ private:
|
|||||||
QMediaPlayer *m_mediaPlayer;
|
QMediaPlayer *m_mediaPlayer;
|
||||||
QAudioOutput *m_audioOutput;
|
QAudioOutput *m_audioOutput;
|
||||||
FFTSpectrum* m_fftSpectrum;
|
FFTSpectrum* m_fftSpectrum;
|
||||||
|
LyricsManager *m_lyricsManager;
|
||||||
LrcBar *m_lrcbar;
|
LrcBar *m_lrcbar;
|
||||||
|
LyricsWidget *m_lyricsWidget;
|
||||||
QPropertyAnimation *m_fadeOutAnimation;
|
QPropertyAnimation *m_fadeOutAnimation;
|
||||||
PlaylistManager *m_playlistManager;
|
PlaylistManager *m_playlistManager;
|
||||||
TaskBarManager *m_taskbarManager;
|
TaskBarManager *m_taskbarManager;
|
||||||
|
WinMediaControl *m_mediaControl;
|
||||||
|
|
||||||
void initUiAndAnimation();
|
void initUiAndAnimation();
|
||||||
void initConnections();
|
void initConnections();
|
||||||
|
|||||||
+65
-11
@@ -109,6 +109,18 @@ QLabel#coverLabel {
|
|||||||
QListView {
|
QListView {
|
||||||
color: white;
|
color: white;
|
||||||
background: rgba(0, 0, 0, 50);
|
background: rgba(0, 0, 0, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
/****** TreeView ******/
|
||||||
|
|
||||||
|
QTreeView {
|
||||||
|
color: white;
|
||||||
|
background: rgba(0, 0, 0, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
QHeaderView {
|
||||||
|
color: white;
|
||||||
|
background-color: rgba(200, 200, 200, 50);
|
||||||
}</string>
|
}</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="locale">
|
<property name="locale">
|
||||||
@@ -217,7 +229,7 @@ QListView {
|
|||||||
<item>
|
<item>
|
||||||
<spacer name="horizontalSpacer">
|
<spacer name="horizontalSpacer">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Orientation::Horizontal</enum>
|
<enum>Qt::Horizontal</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="sizeHint" stdset="0">
|
<property name="sizeHint" stdset="0">
|
||||||
<size>
|
<size>
|
||||||
@@ -295,7 +307,7 @@ QListView {
|
|||||||
<item>
|
<item>
|
||||||
<layout class="QVBoxLayout" name="playerPanelLayout">
|
<layout class="QVBoxLayout" name="playerPanelLayout">
|
||||||
<property name="sizeConstraint">
|
<property name="sizeConstraint">
|
||||||
<enum>QLayout::SizeConstraint::SetDefaultConstraint</enum>
|
<enum>QLayout::SetDefaultConstraint</enum>
|
||||||
</property>
|
</property>
|
||||||
<property name="rightMargin">
|
<property name="rightMargin">
|
||||||
<number>10</number>
|
<number>10</number>
|
||||||
@@ -358,13 +370,26 @@ QListView {
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="chapterNameBtn">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="totalTimeLabel">
|
<widget class="QLabel" name="totalTimeLabel">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string notr="true">0:00</string>
|
<string notr="true">0:00</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="alignment">
|
<property name="alignment">
|
||||||
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
|
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@@ -599,7 +624,7 @@ QListView {
|
|||||||
<number>100</number>
|
<number>100</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Orientation::Horizontal</enum>
|
<enum>Qt::Horizontal</enum>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@@ -626,7 +651,10 @@ QListView {
|
|||||||
<height>0</height>
|
<height>0</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QWidget" name="pluginStackedWidgetPage1">
|
<property name="currentIndex">
|
||||||
|
<number>1</number>
|
||||||
|
</property>
|
||||||
|
<widget class="QWidget" name="playlistViewPage">
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||||
<property name="spacing">
|
<property name="spacing">
|
||||||
<number>0</number>
|
<number>0</number>
|
||||||
@@ -648,22 +676,48 @@ QListView {
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
|
<widget class="QWidget" name="chaptersViewPage">
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||||
|
<property name="leftMargin">
|
||||||
|
<number>4</number>
|
||||||
|
</property>
|
||||||
|
<property name="topMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="rightMargin">
|
||||||
|
<number>4</number>
|
||||||
|
</property>
|
||||||
|
<property name="bottomMargin">
|
||||||
|
<number>4</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QTreeView" name="chapterlistView">
|
||||||
|
<property name="editTriggers">
|
||||||
|
<set>QAbstractItemView::NoEditTriggers</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<action name="actionHelp">
|
<action name="actionHelp">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset theme="system-help"/>
|
||||||
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Help</string>
|
<string>About</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>Help</string>
|
<string>About</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="shortcut">
|
<property name="shortcut">
|
||||||
<string>F1</string>
|
<string notr="true">F1</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="menuRole">
|
<property name="menuRole">
|
||||||
<enum>QAction::MenuRole::NoRole</enum>
|
<enum>QAction::AboutRole</enum>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="actionOpen">
|
<action name="actionOpen">
|
||||||
@@ -674,10 +728,10 @@ QListView {
|
|||||||
<string>Open</string>
|
<string>Open</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="shortcut">
|
<property name="shortcut">
|
||||||
<string>Ctrl+O</string>
|
<string notr="true">Ctrl+O</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="menuRole">
|
<property name="menuRole">
|
||||||
<enum>QAction::MenuRole::NoRole</enum>
|
<enum>QAction::NoRole</enum>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
|
|||||||
@@ -25,6 +25,37 @@ PlaybackProgressIndicator::PlaybackProgressIndicator(QWidget *parent) :
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QModelIndex PlaybackProgressIndicator::currentChapterItem() const
|
||||||
|
{
|
||||||
|
int currentChapterIndex = -1;
|
||||||
|
|
||||||
|
for (int i = 0; i < m_chapterModel.rowCount(); i++) {
|
||||||
|
QStandardItem* timeItem = m_chapterModel.item(i, 0);
|
||||||
|
qint64 chapterStartTime = timeItem->data(PlaybackProgressIndicator::StartTimeMsRole).toLongLong();
|
||||||
|
|
||||||
|
if (m_position >= chapterStartTime) {
|
||||||
|
currentChapterIndex = i;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentChapterIndex >= 0) {
|
||||||
|
return m_chapterModel.index(currentChapterIndex, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QString PlaybackProgressIndicator::currentChapterName() const
|
||||||
|
{
|
||||||
|
const QModelIndex timeIndex(currentChapterItem());
|
||||||
|
if (timeIndex.isValid()) {
|
||||||
|
return m_chapterModel.item(timeIndex.row(), 1)->text();
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
void PlaybackProgressIndicator::setPosition(qint64 pos)
|
void PlaybackProgressIndicator::setPosition(qint64 pos)
|
||||||
{
|
{
|
||||||
m_position = pos;
|
m_position = pos;
|
||||||
@@ -37,14 +68,34 @@ void PlaybackProgressIndicator::setDuration(qint64 dur)
|
|||||||
emit durationChanged(m_duration);
|
emit durationChanged(m_duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString PlaybackProgressIndicator::formatTime(qint64 milliseconds)
|
||||||
|
{
|
||||||
|
QTime duaTime(QTime::fromMSecsSinceStartOfDay(milliseconds));
|
||||||
|
if (duaTime.hour() > 0) {
|
||||||
|
return duaTime.toString("h:mm:ss");
|
||||||
|
} else {
|
||||||
|
return duaTime.toString("m:ss");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void PlaybackProgressIndicator::setChapters(QList<std::pair<qint64, QString> > chapters)
|
void PlaybackProgressIndicator::setChapters(QList<std::pair<qint64, QString> > chapters)
|
||||||
{
|
{
|
||||||
qDebug() << chapters;
|
|
||||||
m_chapterModel.clear();
|
m_chapterModel.clear();
|
||||||
|
|
||||||
|
m_chapterModel.setHorizontalHeaderLabels(QStringList() << tr("Time") << tr("Chapter Name"));
|
||||||
|
|
||||||
for (const std::pair<qint64, QString> & chapter : chapters) {
|
for (const std::pair<qint64, QString> & chapter : chapters) {
|
||||||
|
QList<QStandardItem*> row;
|
||||||
|
|
||||||
|
QStandardItem * timeItem = new QStandardItem(formatTime(chapter.first));
|
||||||
|
timeItem->setData(chapter.first, StartTimeMsRole);
|
||||||
|
row.append(timeItem);
|
||||||
|
|
||||||
QStandardItem * chapterItem = new QStandardItem(chapter.second);
|
QStandardItem * chapterItem = new QStandardItem(chapter.second);
|
||||||
chapterItem->setData(chapter.first, StartTimeMsRole);
|
chapterItem->setData(chapter.first, StartTimeMsRole);
|
||||||
m_chapterModel.appendRow(chapterItem);
|
row.append(chapterItem);
|
||||||
|
|
||||||
|
m_chapterModel.appendRow(row);
|
||||||
}
|
}
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,10 +24,15 @@ public:
|
|||||||
explicit PlaybackProgressIndicator(QWidget *parent = nullptr);
|
explicit PlaybackProgressIndicator(QWidget *parent = nullptr);
|
||||||
~PlaybackProgressIndicator() = default;
|
~PlaybackProgressIndicator() = default;
|
||||||
|
|
||||||
|
QStandardItemModel* chapterModel() { return &m_chapterModel; }
|
||||||
|
QModelIndex currentChapterItem() const;
|
||||||
|
QString currentChapterName() const;
|
||||||
|
|
||||||
void setPosition(qint64 pos);
|
void setPosition(qint64 pos);
|
||||||
void setDuration(qint64 dur);
|
void setDuration(qint64 dur);
|
||||||
void setChapters(QList<std::pair<qint64, QString>> chapters);
|
void setChapters(QList<std::pair<qint64, QString>> chapters);
|
||||||
|
|
||||||
|
static QString formatTime(qint64 milliseconds);
|
||||||
static QList<std::pair<qint64, QString>> tryLoadChapters(const QString & filePath);
|
static QList<std::pair<qint64, QString>> tryLoadChapters(const QString & filePath);
|
||||||
static QList<std::pair<qint64, QString>> tryLoadSidecarChapterFile(const QString & filePath);
|
static QList<std::pair<qint64, QString>> tryLoadSidecarChapterFile(const QString & filePath);
|
||||||
static QList<std::pair<qint64, QString>> tryLoadChaptersFromMetadata(const QString & filePath);
|
static QList<std::pair<qint64, QString>> tryLoadChaptersFromMetadata(const QString & filePath);
|
||||||
|
|||||||
+32
-2
@@ -186,6 +186,26 @@ QModelIndex PlaylistManager::loadPlaylist(const QUrl &url)
|
|||||||
return idx;
|
return idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QModelIndex PlaylistManager::loadM3U8Playlist(const QUrl &url)
|
||||||
|
{
|
||||||
|
QFile file(url.toLocalFile());
|
||||||
|
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
|
QList<QUrl> urls;
|
||||||
|
while (!file.atEnd()) {
|
||||||
|
QString line = file.readLine();
|
||||||
|
if (line.startsWith('#')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
QFileInfo fileInfo(file);
|
||||||
|
QUrl item = QUrl::fromUserInput(line, fileInfo.absolutePath());
|
||||||
|
urls.append(item);
|
||||||
|
}
|
||||||
|
return loadPlaylist(urls);
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int PlaylistManager::totalCount() const
|
int PlaylistManager::totalCount() const
|
||||||
{
|
{
|
||||||
return m_model.rowCount();
|
return m_model.rowCount();
|
||||||
@@ -196,7 +216,7 @@ QModelIndex PlaylistManager::previousIndex() const
|
|||||||
int count = totalCount();
|
int count = totalCount();
|
||||||
if (count == 0) return {};
|
if (count == 0) return {};
|
||||||
|
|
||||||
return m_model.index(m_currentIndex - 1 < 0 ? count - 1 : m_currentIndex - 1);
|
return m_model.index(isFirstIndex() ? count - 1 : m_currentIndex - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
QModelIndex PlaylistManager::nextIndex() const
|
QModelIndex PlaylistManager::nextIndex() const
|
||||||
@@ -204,7 +224,7 @@ QModelIndex PlaylistManager::nextIndex() const
|
|||||||
int count = totalCount();
|
int count = totalCount();
|
||||||
if (count == 0) return {};
|
if (count == 0) return {};
|
||||||
|
|
||||||
return m_model.index(m_currentIndex + 1 == count ? 0 : m_currentIndex + 1);
|
return m_model.index(isLastIndex() ? 0 : m_currentIndex + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
QModelIndex PlaylistManager::curIndex() const
|
QModelIndex PlaylistManager::curIndex() const
|
||||||
@@ -212,6 +232,16 @@ QModelIndex PlaylistManager::curIndex() const
|
|||||||
return m_model.index(m_currentIndex);
|
return m_model.index(m_currentIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool PlaylistManager::isFirstIndex() const
|
||||||
|
{
|
||||||
|
return m_currentIndex == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PlaylistManager::isLastIndex() const
|
||||||
|
{
|
||||||
|
return m_currentIndex + 1 == totalCount();
|
||||||
|
}
|
||||||
|
|
||||||
void PlaylistManager::setCurrentIndex(const QModelIndex &index)
|
void PlaylistManager::setCurrentIndex(const QModelIndex &index)
|
||||||
{
|
{
|
||||||
if (index.isValid() && index.row() >= 0 && index.row() < totalCount()) {
|
if (index.isValid() && index.row() >= 0 && index.row() < totalCount()) {
|
||||||
|
|||||||
@@ -61,11 +61,14 @@ public:
|
|||||||
void setPlaylist(const QList<QUrl> & url);
|
void setPlaylist(const QList<QUrl> & url);
|
||||||
Q_INVOKABLE QModelIndex loadPlaylist(const QList<QUrl> & urls);
|
Q_INVOKABLE QModelIndex loadPlaylist(const QList<QUrl> & urls);
|
||||||
Q_INVOKABLE QModelIndex loadPlaylist(const QUrl & url);
|
Q_INVOKABLE QModelIndex loadPlaylist(const QUrl & url);
|
||||||
|
Q_INVOKABLE QModelIndex loadM3U8Playlist(const QUrl & url);
|
||||||
|
|
||||||
int totalCount() const;
|
int totalCount() const;
|
||||||
QModelIndex previousIndex() const;
|
QModelIndex previousIndex() const;
|
||||||
QModelIndex nextIndex() const;
|
QModelIndex nextIndex() const;
|
||||||
QModelIndex curIndex() const;
|
QModelIndex curIndex() const;
|
||||||
|
bool isFirstIndex() const;
|
||||||
|
bool isLastIndex() const;
|
||||||
void setCurrentIndex(const QModelIndex & index);
|
void setCurrentIndex(const QModelIndex & index);
|
||||||
QUrl urlByIndex(const QModelIndex & index);
|
QUrl urlByIndex(const QModelIndex & index);
|
||||||
QString localFileByIndex(const QModelIndex & index);
|
QString localFileByIndex(const QModelIndex & index);
|
||||||
|
|||||||
@@ -0,0 +1,421 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026 Gary Wang <opensource@blumia.net>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#include "winmediacontrol.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QBuffer>
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN
|
||||||
|
|
||||||
|
#include <roapi.h>
|
||||||
|
#include <windows.media.h>
|
||||||
|
#include <windows.storage.streams.h>
|
||||||
|
#include <systemmediatransportcontrolsinterop.h>
|
||||||
|
|
||||||
|
#include <wrl.h>
|
||||||
|
#include <wrl/wrappers/corewrappers.h>
|
||||||
|
|
||||||
|
using namespace Microsoft::WRL;
|
||||||
|
using namespace Microsoft::WRL::Wrappers;
|
||||||
|
using namespace ABI::Windows::Media;
|
||||||
|
using namespace ABI::Windows::Foundation;
|
||||||
|
using namespace ABI::Windows::Storage::Streams;
|
||||||
|
|
||||||
|
using ButtonPressedHandler = ITypedEventHandler<SystemMediaTransportControls*,
|
||||||
|
SystemMediaTransportControlsButtonPressedEventArgs*>;
|
||||||
|
using PositionChangeHandler = ITypedEventHandler<SystemMediaTransportControls*,
|
||||||
|
PlaybackPositionChangeRequestedEventArgs*>;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// COM event handler for button presses
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
class ButtonDelegate : public ButtonPressedHandler
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ButtonDelegate(WinMediaControl* owner) : m_ref(1), m_owner(owner) {}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override
|
||||||
|
{
|
||||||
|
if (!ppv) return E_POINTER;
|
||||||
|
if (riid == __uuidof(IUnknown) || riid == __uuidof(ButtonPressedHandler)) {
|
||||||
|
*ppv = static_cast<ButtonPressedHandler*>(this);
|
||||||
|
AddRef();
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
*ppv = nullptr;
|
||||||
|
return E_NOINTERFACE;
|
||||||
|
}
|
||||||
|
|
||||||
|
ULONG STDMETHODCALLTYPE AddRef() override { return InterlockedIncrement(&m_ref); }
|
||||||
|
|
||||||
|
ULONG STDMETHODCALLTYPE Release() override
|
||||||
|
{
|
||||||
|
ULONG r = InterlockedDecrement(&m_ref);
|
||||||
|
if (r == 0) delete this;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE Invoke(
|
||||||
|
ISystemMediaTransportControls*,
|
||||||
|
ISystemMediaTransportControlsButtonPressedEventArgs* args) override
|
||||||
|
{
|
||||||
|
if (args) {
|
||||||
|
SystemMediaTransportControlsButton button;
|
||||||
|
if (SUCCEEDED(args->get_Button(&button))) {
|
||||||
|
switch (button) {
|
||||||
|
case SystemMediaTransportControlsButton_Play:
|
||||||
|
emit m_owner->playRequested();
|
||||||
|
break;
|
||||||
|
case SystemMediaTransportControlsButton_Pause:
|
||||||
|
emit m_owner->pauseRequested();
|
||||||
|
break;
|
||||||
|
case SystemMediaTransportControlsButton_Stop:
|
||||||
|
emit m_owner->stopRequested();
|
||||||
|
break;
|
||||||
|
case SystemMediaTransportControlsButton_Next:
|
||||||
|
emit m_owner->nextRequested();
|
||||||
|
break;
|
||||||
|
case SystemMediaTransportControlsButton_Previous:
|
||||||
|
emit m_owner->previousRequested();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
LONG m_ref;
|
||||||
|
WinMediaControl* m_owner;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// COM event handler for position change (seek) requests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
class PositionChangeDelegate : public PositionChangeHandler
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
PositionChangeDelegate(WinMediaControl* owner) : m_ref(1), m_owner(owner) {}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override
|
||||||
|
{
|
||||||
|
if (!ppv) return E_POINTER;
|
||||||
|
if (riid == __uuidof(IUnknown) || riid == __uuidof(PositionChangeHandler)) {
|
||||||
|
*ppv = static_cast<PositionChangeHandler*>(this);
|
||||||
|
AddRef();
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
*ppv = nullptr;
|
||||||
|
return E_NOINTERFACE;
|
||||||
|
}
|
||||||
|
|
||||||
|
ULONG STDMETHODCALLTYPE AddRef() override { return InterlockedIncrement(&m_ref); }
|
||||||
|
|
||||||
|
ULONG STDMETHODCALLTYPE Release() override
|
||||||
|
{
|
||||||
|
ULONG r = InterlockedDecrement(&m_ref);
|
||||||
|
if (r == 0) delete this;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE Invoke(
|
||||||
|
ISystemMediaTransportControls*,
|
||||||
|
IPlaybackPositionChangeRequestedEventArgs* args) override
|
||||||
|
{
|
||||||
|
if (args) {
|
||||||
|
TimeSpan ts;
|
||||||
|
if (SUCCEEDED(args->get_RequestedPlaybackPosition(&ts))) {
|
||||||
|
emit m_owner->seekRequested(ts.Duration / 10000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
LONG m_ref;
|
||||||
|
WinMediaControl* m_owner;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// WinMediaControlPrivate
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
class WinMediaControlPrivate
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ComPtr<ISystemMediaTransportControls> smtc;
|
||||||
|
ComPtr<ISystemMediaTransportControls2> smtc2;
|
||||||
|
ComPtr<ISystemMediaTransportControlsDisplayUpdater> updater;
|
||||||
|
ComPtr<IMusicDisplayProperties> musicProps;
|
||||||
|
EventRegistrationToken buttonToken{};
|
||||||
|
EventRegistrationToken positionChangeToken{};
|
||||||
|
bool initialized = false;
|
||||||
|
|
||||||
|
QString title;
|
||||||
|
QString artist;
|
||||||
|
QString album;
|
||||||
|
QImage coverArt;
|
||||||
|
};
|
||||||
|
|
||||||
|
WinMediaControl::WinMediaControl(QObject* parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, d(new WinMediaControlPrivate())
|
||||||
|
{
|
||||||
|
HRESULT hr = RoInitialize(RO_INIT_SINGLETHREADED);
|
||||||
|
if (FAILED(hr) && hr != RPC_E_CHANGED_MODE) {
|
||||||
|
qWarning() << "WinMediaControl: RoInitialize failed:" << Qt::hex << hr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WinMediaControl::~WinMediaControl()
|
||||||
|
{
|
||||||
|
shutdown();
|
||||||
|
delete d;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WinMediaControl::initialize(void* hwnd)
|
||||||
|
{
|
||||||
|
if (d->initialized || !hwnd) return;
|
||||||
|
|
||||||
|
HWND window = static_cast<HWND>(hwnd);
|
||||||
|
|
||||||
|
ComPtr<ISystemMediaTransportControlsInterop> interop;
|
||||||
|
HRESULT hr = RoGetActivationFactory(
|
||||||
|
HStringReference(RuntimeClass_Windows_Media_SystemMediaTransportControls).Get(),
|
||||||
|
IID_PPV_ARGS(&interop));
|
||||||
|
if (FAILED(hr) || !interop) {
|
||||||
|
qWarning() << "WinMediaControl: Failed to get interop factory:" << Qt::hex << hr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = interop->GetForWindow(window, IID_PPV_ARGS(&d->smtc));
|
||||||
|
if (FAILED(hr) || !d->smtc) {
|
||||||
|
qWarning() << "WinMediaControl: GetForWindow failed:" << Qt::hex << hr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
d->smtc->put_IsEnabled(true);
|
||||||
|
d->smtc->put_IsPlayEnabled(true);
|
||||||
|
d->smtc->put_IsPauseEnabled(true);
|
||||||
|
d->smtc->put_IsStopEnabled(true);
|
||||||
|
d->smtc->put_IsNextEnabled(true);
|
||||||
|
d->smtc->put_IsPreviousEnabled(true);
|
||||||
|
|
||||||
|
d->smtc.As(&d->smtc2);
|
||||||
|
|
||||||
|
d->smtc->get_DisplayUpdater(&d->updater);
|
||||||
|
if (d->updater) {
|
||||||
|
d->updater->put_Type(MediaPlaybackType_Music);
|
||||||
|
d->updater->get_MusicProperties(&d->musicProps);
|
||||||
|
d->updater->Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* btnHandler = new ButtonDelegate(this);
|
||||||
|
hr = d->smtc->add_ButtonPressed(btnHandler, &d->buttonToken);
|
||||||
|
btnHandler->Release();
|
||||||
|
|
||||||
|
if (d->smtc2) {
|
||||||
|
auto* posHandler = new PositionChangeDelegate(this);
|
||||||
|
hr = d->smtc2->add_PlaybackPositionChangeRequested(posHandler, &d->positionChangeToken);
|
||||||
|
posHandler->Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
d->initialized = true;
|
||||||
|
qDebug() << "WinMediaControl: SMTC initialized successfully";
|
||||||
|
}
|
||||||
|
|
||||||
|
void WinMediaControl::shutdown()
|
||||||
|
{
|
||||||
|
if (!d->initialized) return;
|
||||||
|
|
||||||
|
if (d->smtc) {
|
||||||
|
d->smtc->remove_ButtonPressed(d->buttonToken);
|
||||||
|
if (d->smtc2) {
|
||||||
|
d->smtc2->remove_PlaybackPositionChangeRequested(d->positionChangeToken);
|
||||||
|
}
|
||||||
|
d->smtc->put_IsEnabled(false);
|
||||||
|
}
|
||||||
|
d->smtc.Reset();
|
||||||
|
d->smtc2.Reset();
|
||||||
|
d->updater.Reset();
|
||||||
|
d->musicProps.Reset();
|
||||||
|
d->initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ComPtr<IRandomAccessStreamReference> imageToStreamReference(const QImage& image)
|
||||||
|
{
|
||||||
|
QByteArray imageData;
|
||||||
|
QBuffer buffer(&imageData);
|
||||||
|
buffer.open(QIODevice::WriteOnly);
|
||||||
|
image.save(&buffer, "PNG");
|
||||||
|
buffer.close();
|
||||||
|
|
||||||
|
ComPtr<IInspectable> insp;
|
||||||
|
HRESULT hr = RoActivateInstance(
|
||||||
|
HStringReference(RuntimeClass_Windows_Storage_Streams_InMemoryRandomAccessStream).Get(),
|
||||||
|
&insp);
|
||||||
|
if (FAILED(hr) || !insp) return nullptr;
|
||||||
|
|
||||||
|
ComPtr<IRandomAccessStream> stream;
|
||||||
|
insp.As(&stream);
|
||||||
|
if (!stream) return nullptr;
|
||||||
|
|
||||||
|
ComPtr<IOutputStream> outputStream;
|
||||||
|
stream->GetOutputStreamAt(0, &outputStream);
|
||||||
|
|
||||||
|
ComPtr<IDataWriterFactory> writerFactory;
|
||||||
|
RoGetActivationFactory(
|
||||||
|
HStringReference(RuntimeClass_Windows_Storage_Streams_DataWriter).Get(),
|
||||||
|
IID_PPV_ARGS(&writerFactory));
|
||||||
|
if (!writerFactory) return nullptr;
|
||||||
|
|
||||||
|
ComPtr<IDataWriter> writer;
|
||||||
|
writerFactory->CreateDataWriter(outputStream.Get(), &writer);
|
||||||
|
if (!writer) return nullptr;
|
||||||
|
|
||||||
|
writer->WriteBytes(
|
||||||
|
static_cast<UINT32>(imageData.size()),
|
||||||
|
reinterpret_cast<BYTE*>(imageData.data()));
|
||||||
|
|
||||||
|
ComPtr<IAsyncOperation<UINT32>> storeOp;
|
||||||
|
writer->StoreAsync(&storeOp);
|
||||||
|
if (storeOp) {
|
||||||
|
// For in-memory streams this completes synchronously; just drain it
|
||||||
|
UINT32 result = 0;
|
||||||
|
storeOp->GetResults(&result);
|
||||||
|
}
|
||||||
|
|
||||||
|
stream->Seek(0);
|
||||||
|
|
||||||
|
ComPtr<IRandomAccessStreamReferenceStatics> rars;
|
||||||
|
RoGetActivationFactory(
|
||||||
|
HStringReference(RuntimeClass_Windows_Storage_Streams_RandomAccessStreamReference).Get(),
|
||||||
|
IID_PPV_ARGS(&rars));
|
||||||
|
if (!rars) return nullptr;
|
||||||
|
|
||||||
|
ComPtr<IRandomAccessStreamReference> ref;
|
||||||
|
rars->CreateFromStream(stream.Get(), &ref);
|
||||||
|
return ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WinMediaControl::setMetadata(const QString& title, const QString& artist,
|
||||||
|
const QString& album)
|
||||||
|
{
|
||||||
|
d->title = title;
|
||||||
|
d->artist = artist;
|
||||||
|
d->album = album;
|
||||||
|
|
||||||
|
if (!d->initialized || !d->updater || !d->musicProps) return;
|
||||||
|
|
||||||
|
d->updater->put_Type(MediaPlaybackType_Music);
|
||||||
|
d->updater->get_MusicProperties(&d->musicProps);
|
||||||
|
|
||||||
|
{
|
||||||
|
std::wstring wstr = title.toStdWString();
|
||||||
|
d->musicProps->put_Title(HStringReference(wstr.c_str()).Get());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::wstring wstr = artist.toStdWString();
|
||||||
|
d->musicProps->put_Artist(HStringReference(wstr.c_str()).Get());
|
||||||
|
}
|
||||||
|
|
||||||
|
ComPtr<IMusicDisplayProperties2> musicProps2;
|
||||||
|
if (SUCCEEDED(d->musicProps.As(&musicProps2)) && musicProps2) {
|
||||||
|
std::wstring wstr = album.toStdWString();
|
||||||
|
musicProps2->put_AlbumTitle(HStringReference(wstr.c_str()).Get());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!d->coverArt.isNull()) {
|
||||||
|
auto thumbRef = imageToStreamReference(d->coverArt);
|
||||||
|
if (thumbRef) {
|
||||||
|
d->updater->put_Thumbnail(thumbRef.Get());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
d->updater->put_Thumbnail(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
d->updater->Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WinMediaControl::setCoverArt(const QImage& coverArt)
|
||||||
|
{
|
||||||
|
d->coverArt = coverArt;
|
||||||
|
if (d->initialized) {
|
||||||
|
setMetadata(d->title, d->artist, d->album);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WinMediaControl::setPlaybackState(int state)
|
||||||
|
{
|
||||||
|
if (!d->initialized || !d->smtc) return;
|
||||||
|
|
||||||
|
MediaPlaybackStatus status;
|
||||||
|
switch (state) {
|
||||||
|
case 0: status = MediaPlaybackStatus_Stopped; break;
|
||||||
|
case 1: status = MediaPlaybackStatus_Playing; break;
|
||||||
|
case 2: status = MediaPlaybackStatus_Paused; break;
|
||||||
|
default: status = MediaPlaybackStatus_Stopped; break;
|
||||||
|
}
|
||||||
|
d->smtc->put_PlaybackStatus(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WinMediaControl::setPosition(qint64 positionMs, qint64 durationMs)
|
||||||
|
{
|
||||||
|
if (!d->initialized || !d->smtc2) return;
|
||||||
|
|
||||||
|
ComPtr<IInspectable> insp;
|
||||||
|
HRESULT hr = RoActivateInstance(
|
||||||
|
HStringReference(RuntimeClass_Windows_Media_SystemMediaTransportControlsTimelineProperties).Get(),
|
||||||
|
&insp);
|
||||||
|
if (FAILED(hr) || !insp) return;
|
||||||
|
|
||||||
|
ComPtr<ISystemMediaTransportControlsTimelineProperties> timeline;
|
||||||
|
insp.As(&timeline);
|
||||||
|
if (!timeline) return;
|
||||||
|
|
||||||
|
timeline->put_StartTime({ 0 });
|
||||||
|
timeline->put_EndTime({ durationMs * 10000 });
|
||||||
|
timeline->put_Position({ positionMs * 10000 });
|
||||||
|
timeline->put_MinSeekTime({ 0 });
|
||||||
|
timeline->put_MaxSeekTime({ durationMs * 10000 });
|
||||||
|
|
||||||
|
d->smtc2->UpdateTimelineProperties(timeline.Get());
|
||||||
|
}
|
||||||
|
|
||||||
|
void WinMediaControl::setControlsEnabled(bool play, bool pause, bool stop,
|
||||||
|
bool next, bool previous)
|
||||||
|
{
|
||||||
|
if (!d->initialized || !d->smtc) return;
|
||||||
|
|
||||||
|
d->smtc->put_IsPlayEnabled(play);
|
||||||
|
d->smtc->put_IsPauseEnabled(pause);
|
||||||
|
d->smtc->put_IsStopEnabled(stop);
|
||||||
|
d->smtc->put_IsNextEnabled(next);
|
||||||
|
d->smtc->put_IsPreviousEnabled(previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
#else // Q_OS_WIN
|
||||||
|
|
||||||
|
// Non-Windows stub implementations
|
||||||
|
|
||||||
|
WinMediaControl::WinMediaControl(QObject* parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, d(nullptr)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
WinMediaControl::~WinMediaControl() = default;
|
||||||
|
|
||||||
|
void WinMediaControl::initialize(void*) {}
|
||||||
|
void WinMediaControl::shutdown() {}
|
||||||
|
|
||||||
|
void WinMediaControl::setMetadata(const QString&, const QString&, const QString&) {}
|
||||||
|
void WinMediaControl::setCoverArt(const QImage&) {}
|
||||||
|
void WinMediaControl::setPlaybackState(int) {}
|
||||||
|
void WinMediaControl::setPosition(qint64, qint64) {}
|
||||||
|
void WinMediaControl::setControlsEnabled(bool, bool, bool, bool, bool) {}
|
||||||
|
|
||||||
|
#endif // Q_OS_WIN
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026 Gary Wang <opensource@blumia.net>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QImage>
|
||||||
|
|
||||||
|
class WinMediaControlPrivate;
|
||||||
|
|
||||||
|
class WinMediaControl : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit WinMediaControl(QObject* parent = nullptr);
|
||||||
|
~WinMediaControl();
|
||||||
|
|
||||||
|
void initialize(void* hwnd);
|
||||||
|
void shutdown();
|
||||||
|
|
||||||
|
void setMetadata(const QString& title, const QString& artist,
|
||||||
|
const QString& album);
|
||||||
|
void setCoverArt(const QImage& coverArt);
|
||||||
|
void setPlaybackState(int state);
|
||||||
|
void setPosition(qint64 positionMs, qint64 durationMs);
|
||||||
|
void setControlsEnabled(bool play, bool pause, bool stop,
|
||||||
|
bool next, bool previous);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void playRequested();
|
||||||
|
void pauseRequested();
|
||||||
|
void stopRequested();
|
||||||
|
void nextRequested();
|
||||||
|
void previousRequested();
|
||||||
|
void seekRequested(qint64 positionMs);
|
||||||
|
|
||||||
|
private:
|
||||||
|
WinMediaControlPrivate* d = nullptr;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user