├── .github ├── FUNDING.yml └── workflows │ └── build-deb.yml ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── LICENSE-3RD-PARTY.md ├── README.md ├── assets ├── balanceHandle.png ├── balanceHandle_p.png ├── bignumbers.ttf ├── eq_off.png ├── eq_off_p.png ├── eq_on.png ├── eq_on_p.png ├── fb_folderIcon.png ├── fb_folderIcon_selected.png ├── fb_musicIcon.png ├── fb_musicIcon_selected.png ├── filesIcon.png ├── logoButton.png ├── menu-icon-x.png ├── next.png ├── next_p.png ├── open.png ├── open_p.png ├── pause.png ├── pause_p.png ├── pl_add.png ├── pl_addIcon.png ├── pl_add_p.png ├── pl_close.png ├── pl_close_p.png ├── pl_homeIcon.png ├── pl_off.png ├── pl_off_p.png ├── pl_on.png ├── pl_on_p.png ├── pl_playerIcon.png ├── pl_upIcon.png ├── play.png ├── play_p.png ├── playlistsIcon.png ├── posHandle.png ├── posHandle_p.png ├── prev.png ├── prev_p.png ├── repeat_off.png ├── repeat_off_p.png ├── repeat_on.png ├── repeat_on_p.png ├── scroll_handle.png ├── scroll_handle_p.png ├── shuffle_off.png ├── shuffle_off_p.png ├── shuffle_on.png ├── shuffle_on_p.png ├── source-icon-bluetooth.png ├── source-icon-cd.png ├── source-icon-file.png ├── source-icon-spotify.png ├── spotifyIcon.png ├── status_paused.png ├── status_playing.png ├── status_stopped.png ├── stop.png ├── stop_p.png ├── visualizationBackground.png ├── volumeHandle.png ├── volumeHandle_p.png ├── windowClose.png ├── windowMaximize.png └── windowMinimize.png ├── debian ├── changelog ├── control ├── copyright ├── rules └── source │ └── format ├── install.sh ├── linamp.desktop ├── python ├── linamp-mock │ ├── __init__.py │ └── mock_cdplayer.py ├── linamp │ ├── __init__.py │ ├── baseplayer │ │ ├── __init__.py │ │ └── baseplayer.py │ ├── btplayer │ │ ├── __init__.py │ │ ├── btadapter.py │ │ └── btplayer.py │ ├── cdplayer.py │ └── spotifyplayer │ │ ├── __init__.py │ │ ├── librespot-event-handler.sh │ │ ├── spotifyadapter.py │ │ └── spotifyplayer.py └── requirements.txt ├── scale-skin.sh ├── setup.sh ├── shutdown.sh ├── skin ├── balanceHandle.png ├── balanceHandle_p.png ├── eq_off.png ├── eq_off_p.png ├── eq_on.png ├── eq_on_p.png ├── logoButton.png ├── next.png ├── next_p.png ├── open.png ├── open_p.png ├── pause.png ├── pause_p.png ├── pl_add.png ├── pl_add_p.png ├── pl_close.png ├── pl_close_p.png ├── pl_off.png ├── pl_off_p.png ├── pl_on.png ├── pl_on_p.png ├── play.png ├── play_p.png ├── posHandle.png ├── posHandle_p.png ├── prev.png ├── prev_p.png ├── repeat_off.png ├── repeat_off_p.png ├── repeat_on.png ├── repeat_on_p.png ├── scroll_handle.png ├── scroll_handle_p.png ├── shuffle_off.png ├── shuffle_off_p.png ├── shuffle_on.png ├── shuffle_on_p.png ├── status_paused.png ├── status_playing.png ├── status_stopped.png ├── stop.png ├── stop_p.png ├── visualizationBackground.png ├── volumeHandle.png └── volumeHandle_p.png ├── src ├── audiosource-base │ ├── audiosource.cpp │ ├── audiosource.h │ ├── audiosourcewspectrumcapture.cpp │ └── audiosourcewspectrumcapture.h ├── audiosource-coordinator │ ├── audiosourcecoordinator.cpp │ └── audiosourcecoordinator.h ├── audiosourcecd │ ├── audiosourcecd.cpp │ └── audiosourcecd.h ├── audiosourcefile │ ├── audiosourcefile.cpp │ ├── audiosourcefile.h │ ├── mediaplayer.cpp │ └── mediaplayer.h ├── audiosourcepython │ ├── audiosourcepython.cpp │ └── audiosourcepython.h ├── main.cpp ├── shared │ ├── fft.cpp │ ├── fft.h │ ├── linampslider.cpp │ ├── linampslider.h │ ├── scale.cpp │ ├── scale.h │ ├── systemaudiocontrol.cpp │ ├── systemaudiocontrol.h │ ├── util.cpp │ └── util.h ├── view-basewindow │ ├── desktopbasewindow.cpp │ ├── desktopbasewindow.h │ ├── desktopbasewindow.ui │ ├── desktopplayerwindow.cpp │ ├── desktopplayerwindow.h │ ├── desktopplayerwindow.ui │ ├── embeddedbasewindow.cpp │ ├── embeddedbasewindow.h │ ├── embeddedbasewindow.ui │ ├── mainwindow.cpp │ ├── mainwindow.h │ ├── titlebar.cpp │ ├── titlebar.h │ └── titlebar.ui ├── view-menu │ ├── mainmenuview.cpp │ ├── mainmenuview.h │ └── mainmenuview.ui ├── view-player │ ├── controlbuttonswidget.cpp │ ├── controlbuttonswidget.h │ ├── controlbuttonswidget.ui │ ├── playerview.cpp │ ├── playerview.h │ ├── playerview.ui │ ├── scrolltext.cpp │ ├── scrolltext.h │ ├── spectrumwidget.cpp │ └── spectrumwidget.h └── view-playlist │ ├── filebrowsericonprovider.cpp │ ├── filebrowsericonprovider.h │ ├── playlistmodel.cpp │ ├── playlistmodel.h │ ├── playlistview.cpp │ ├── playlistview.h │ ├── playlistview.ui │ ├── qmediaplaylist.cpp │ ├── qmediaplaylist.h │ ├── qmediaplaylist_p.cpp │ ├── qmediaplaylist_p.h │ ├── qplaylistfileparser.cpp │ └── qplaylistfileparser.h ├── start.sh ├── styles ├── controlbuttonswidget.repeatButton.1x.qss ├── controlbuttonswidget.repeatButton.2x.qss ├── controlbuttonswidget.repeatButton.3x.qss ├── controlbuttonswidget.repeatButton.4x.qss ├── controlbuttonswidget.shuffleButton.1x.qss ├── controlbuttonswidget.shuffleButton.2x.qss ├── controlbuttonswidget.shuffleButton.3x.qss ├── controlbuttonswidget.shuffleButton.4x.qss ├── desktopbasewindow.1x.qss ├── desktopbasewindow.2x.qss ├── desktopbasewindow.3x.qss ├── desktopbasewindow.4x.qss ├── playerview.balanceSlider.1x.qss ├── playerview.balanceSlider.2x.qss ├── playerview.balanceSlider.3x.qss ├── playerview.balanceSlider.4x.qss ├── playerview.codecDetailsContainer.1x.qss ├── playerview.codecDetailsContainer.2x.qss ├── playerview.codecDetailsContainer.3x.qss ├── playerview.codecDetailsContainer.4x.qss ├── playerview.eqButton.1x.qss ├── playerview.eqButton.2x.qss ├── playerview.eqButton.3x.qss ├── playerview.eqButton.4x.qss ├── playerview.kbpsFrame.1x.qss ├── playerview.kbpsFrame.2x.qss ├── playerview.kbpsFrame.3x.qss ├── playerview.kbpsFrame.4x.qss ├── playerview.khzFrame.1x.qss ├── playerview.khzFrame.2x.qss ├── playerview.khzFrame.3x.qss ├── playerview.khzFrame.4x.qss ├── playerview.playlistButton.1x.qss ├── playerview.playlistButton.2x.qss ├── playerview.playlistButton.3x.qss ├── playerview.playlistButton.4x.qss ├── playerview.posBar.1x.qss ├── playerview.posBar.2x.qss ├── playerview.posBar.3x.qss ├── playerview.posBar.4x.qss ├── playerview.songInfoContainer.1x.qss ├── playerview.songInfoContainer.2x.qss ├── playerview.songInfoContainer.3x.qss ├── playerview.songInfoContainer.4x.qss ├── playerview.visualizationFrame.1x.qss ├── playerview.visualizationFrame.2x.qss ├── playerview.visualizationFrame.3x.qss ├── playerview.visualizationFrame.4x.qss ├── playerview.volumeSlider.1x.qss ├── playerview.volumeSlider.2x.qss ├── playerview.volumeSlider.3x.qss └── playerview.volumeSlider.4x.qss └── uiassets.qrc /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: ['https://store.linamp.org/'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.github/workflows/build-deb.yml: -------------------------------------------------------------------------------- 1 | name: Build Deb package 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | # Sequence of patterns matched against refs/tags 7 | tags: 8 | - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 9 | 10 | jobs: 11 | build-x86_64: 12 | runs-on: ubuntu-22.04 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Build Debian packages 17 | uses: jtdor/build-deb-action@v1.8.0 18 | with: 19 | # Name of a Docker image or path of a Dockerfile to use for the build container 20 | docker-image: debian:bookworm-slim 21 | # Extra packages to be installed as build dependencies 22 | extra-build-deps: dh-python dh-cmake 23 | - name: Archive build artifacts 24 | uses: actions/upload-artifact@v4 25 | with: 26 | name: build-x86_64 27 | path: debian/artifacts/* 28 | 29 | build-arm64: 30 | runs-on: ubuntu-22.04 31 | 32 | steps: 33 | - uses: actions/checkout@v4 34 | - name: Set up QEMU 35 | uses: docker/setup-qemu-action@v3 36 | with: 37 | platforms: arm64 38 | - name: Build Debian packages 39 | uses: jtdor/build-deb-action@v1.8.0 40 | with: 41 | # Name of a Docker image or path of a Dockerfile to use for the build container 42 | docker-image: debian:bookworm-slim 43 | # Extra packages to be installed as build dependencies 44 | extra-build-deps: dh-python dh-cmake 45 | extra-docker-args: --platform linux/arm64 46 | - name: Archive build artifacts 47 | uses: actions/upload-artifact@v4 48 | with: 49 | name: build-arm64 50 | path: debian/artifacts/* 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Environments 2 | venv/ 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # Installer logs 10 | pip-log.txt 11 | pip-delete-this-directory.txt 12 | 13 | # Build artifacts 14 | CMakeFiles 15 | build 16 | CMakeLists.txt.user 17 | *.pro.user* 18 | .qt 19 | .rcc 20 | CMakeCache.txt 21 | Makefile 22 | cmake_install.cmake 23 | player_autogen 24 | Testing 25 | CMakeCache.txt.prev 26 | .cmake 27 | 28 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | project(player VERSION 1.0 LANGUAGES C CXX) 3 | 4 | set(CMAKE_INCLUDE_CURRENT_DIR ON) 5 | set(CMAKE_AUTOUIC ON) 6 | set(CMAKE_AUTORCC ON) 7 | set(CMAKE_AUTOMOC ON) 8 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/build) 9 | 10 | find_package(QT NAMES Qt5 Qt6 REQUIRED COMPONENTS Core) 11 | find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Concurrent DBus Gui Multimedia MultimediaWidgets Network Widgets) 12 | 13 | qt_standard_project_setup() 14 | 15 | include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src) 16 | include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/audiosource-base) 17 | include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/audiosource-coordinator) 18 | include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/audiosourcepython) 19 | include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/audiosourcecd) 20 | include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/audiosourcefile) 21 | include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/shared) 22 | include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/view-basewindow) 23 | include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/view-menu) 24 | include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/view-player) 25 | include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/view-playlist) 26 | 27 | qt_add_executable(player WIN32 MACOSX_BUNDLE 28 | src/audiosource-base/audiosource.cpp 29 | src/audiosource-base/audiosource.h 30 | src/audiosource-base/audiosourcewspectrumcapture.cpp 31 | src/audiosource-base/audiosourcewspectrumcapture.h 32 | src/audiosourcecd/audiosourcecd.cpp 33 | src/audiosourcecd/audiosourcecd.h 34 | src/audiosourcepython/audiosourcepython.cpp 35 | src/audiosourcepython/audiosourcepython.h 36 | src/audiosource-coordinator/audiosourcecoordinator.cpp 37 | src/audiosource-coordinator/audiosourcecoordinator.h 38 | src/audiosourcefile/audiosourcefile.cpp 39 | src/audiosourcefile/audiosourcefile.h 40 | src/audiosourcefile/mediaplayer.cpp 41 | src/audiosourcefile/mediaplayer.h 42 | src/view-player/controlbuttonswidget.cpp 43 | src/view-player/controlbuttonswidget.h 44 | src/view-player/controlbuttonswidget.ui 45 | src/view-player/scrolltext.cpp 46 | src/view-player/scrolltext.h 47 | src/view-player/spectrumwidget.cpp 48 | src/view-player/spectrumwidget.h 49 | src/view-player/playerview.cpp 50 | src/view-player/playerview.h 51 | src/view-player/playerview.ui 52 | src/view-basewindow/desktopbasewindow.cpp 53 | src/view-basewindow/desktopbasewindow.h 54 | src/view-basewindow/desktopbasewindow.ui 55 | src/view-basewindow/desktopplayerwindow.cpp 56 | src/view-basewindow/desktopplayerwindow.h 57 | src/view-basewindow/desktopplayerwindow.ui 58 | src/view-basewindow/embeddedbasewindow.cpp 59 | src/view-basewindow/embeddedbasewindow.h 60 | src/view-basewindow/embeddedbasewindow.ui 61 | src/view-basewindow/mainwindow.cpp 62 | src/view-basewindow/mainwindow.h 63 | src/view-basewindow/titlebar.cpp 64 | src/view-basewindow/titlebar.h 65 | src/view-basewindow/titlebar.ui 66 | src/view-playlist/filebrowsericonprovider.cpp 67 | src/view-playlist/filebrowsericonprovider.h 68 | src/view-playlist/playlistmodel.cpp 69 | src/view-playlist/playlistmodel.h 70 | src/view-playlist/playlistview.cpp 71 | src/view-playlist/playlistview.h 72 | src/view-playlist/playlistview.ui 73 | src/view-playlist/qmediaplaylist.cpp 74 | src/view-playlist/qmediaplaylist.h 75 | src/view-playlist/qmediaplaylist_p.cpp 76 | src/view-playlist/qmediaplaylist_p.h 77 | src/view-playlist/qplaylistfileparser.cpp 78 | src/view-playlist/qplaylistfileparser.h 79 | src/view-menu/mainmenuview.cpp 80 | src/view-menu/mainmenuview.h 81 | src/view-menu/mainmenuview.ui 82 | src/shared/scale.cpp 83 | src/shared/scale.h 84 | src/shared/systemaudiocontrol.cpp 85 | src/shared/systemaudiocontrol.h 86 | src/shared/fft.cpp 87 | src/shared/fft.h 88 | src/shared/util.cpp 89 | src/shared/util.h 90 | src/shared/linampslider.h 91 | src/shared/linampslider.cpp 92 | src/main.cpp 93 | uiassets.qrc 94 | ) 95 | 96 | target_include_directories(player PRIVATE 97 | /usr/include/pipewire-0.3 98 | /usr/include/python3.11 99 | /usr/include/spa-0.2 100 | ) 101 | 102 | target_link_libraries(player PRIVATE 103 | # Remove: L/usr/lib/python3.11/config-3.11-x86_64-linux-gnu/ 104 | Qt::Concurrent 105 | Qt::Core 106 | Qt::DBus 107 | Qt::Gui 108 | Qt::Multimedia 109 | Qt::MultimediaWidgets 110 | Qt::Network 111 | Qt::Widgets 112 | asound 113 | pipewire-0.3 114 | pulse 115 | pulse-simple 116 | python3.11 117 | tag 118 | ) 119 | 120 | install(TARGETS player 121 | BUNDLE DESTINATION . 122 | RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} 123 | ) 124 | 125 | qt_generate_deploy_app_script( 126 | TARGET player 127 | FILENAME_VARIABLE deploy_script 128 | NO_UNSUPPORTED_PLATFORM_ERROR 129 | ) 130 | install(SCRIPT ${deploy_script}) 131 | -------------------------------------------------------------------------------- /LICENSE-3RD-PARTY.md: -------------------------------------------------------------------------------- 1 | # THIRD PARTY LICENCES 2 | 3 | linamp-player includes the following third-party software/licensing: 4 | 5 | 6 | ## boxicons - https://boxicons.com/ 7 | 8 | The MIT License (MIT) 9 | 10 | Copyright (c) 2015-2021 Aniket Suvarna 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | 19 | ## decorator-operations debounce - https://github.com/salesforce/decorator-operations/blob/master/decoratorOperations/debounce_functions/debounce.py 20 | 21 | Copyright (c) 2020, Salesforce.com, Inc. 22 | All rights reserved. 23 | 24 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 25 | 26 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 27 | 28 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 29 | 30 | * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 31 | 32 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | 34 | 35 | ## RpiPythonCDPlayer - https://github.com/trybula/RpiPythonCDPlayer 36 | 37 | GPL v3: https://github.com/trybula/RpiPythonCDPlayer/blob/main/LICENSE 38 | 39 | 40 | ## QtMixer - https://github.com/Znurre/QtMixer 41 | 42 | GPL v2: https://github.com/Znurre/QtMixer/blob/master/LICENSE 43 | 44 | 45 | ## audacious fft.cc - https://github.com/audacious-media-player/audacious/blob/master/src/libaudcore/fft.cc 46 | 47 | Copyright 2011 John Lindgren 48 | 49 | Redistribution and use in source and binary forms, with or without 50 | modification, are permitted provided that the following conditions are met: 51 | 52 | 1. Redistributions of source code must retain the above copyright notice, 53 | this list of conditions, and the following disclaimer. 54 | 55 | 2. Redistributions in binary form must reproduce the above copyright notice, 56 | this list of conditions, and the following disclaimer in the documentation 57 | provided with the distribution. 58 | 59 | This software is provided "as is" and without any warranty, express or 60 | implied. In no event shall the authors be liable for any damages arising from 61 | the use of this software. -------------------------------------------------------------------------------- /assets/balanceHandle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/balanceHandle.png -------------------------------------------------------------------------------- /assets/balanceHandle_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/balanceHandle_p.png -------------------------------------------------------------------------------- /assets/bignumbers.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/bignumbers.ttf -------------------------------------------------------------------------------- /assets/eq_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/eq_off.png -------------------------------------------------------------------------------- /assets/eq_off_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/eq_off_p.png -------------------------------------------------------------------------------- /assets/eq_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/eq_on.png -------------------------------------------------------------------------------- /assets/eq_on_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/eq_on_p.png -------------------------------------------------------------------------------- /assets/fb_folderIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/fb_folderIcon.png -------------------------------------------------------------------------------- /assets/fb_folderIcon_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/fb_folderIcon_selected.png -------------------------------------------------------------------------------- /assets/fb_musicIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/fb_musicIcon.png -------------------------------------------------------------------------------- /assets/fb_musicIcon_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/fb_musicIcon_selected.png -------------------------------------------------------------------------------- /assets/filesIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/filesIcon.png -------------------------------------------------------------------------------- /assets/logoButton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/logoButton.png -------------------------------------------------------------------------------- /assets/menu-icon-x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/menu-icon-x.png -------------------------------------------------------------------------------- /assets/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/next.png -------------------------------------------------------------------------------- /assets/next_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/next_p.png -------------------------------------------------------------------------------- /assets/open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/open.png -------------------------------------------------------------------------------- /assets/open_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/open_p.png -------------------------------------------------------------------------------- /assets/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/pause.png -------------------------------------------------------------------------------- /assets/pause_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/pause_p.png -------------------------------------------------------------------------------- /assets/pl_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/pl_add.png -------------------------------------------------------------------------------- /assets/pl_addIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/pl_addIcon.png -------------------------------------------------------------------------------- /assets/pl_add_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/pl_add_p.png -------------------------------------------------------------------------------- /assets/pl_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/pl_close.png -------------------------------------------------------------------------------- /assets/pl_close_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/pl_close_p.png -------------------------------------------------------------------------------- /assets/pl_homeIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/pl_homeIcon.png -------------------------------------------------------------------------------- /assets/pl_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/pl_off.png -------------------------------------------------------------------------------- /assets/pl_off_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/pl_off_p.png -------------------------------------------------------------------------------- /assets/pl_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/pl_on.png -------------------------------------------------------------------------------- /assets/pl_on_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/pl_on_p.png -------------------------------------------------------------------------------- /assets/pl_playerIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/pl_playerIcon.png -------------------------------------------------------------------------------- /assets/pl_upIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/pl_upIcon.png -------------------------------------------------------------------------------- /assets/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/play.png -------------------------------------------------------------------------------- /assets/play_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/play_p.png -------------------------------------------------------------------------------- /assets/playlistsIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/playlistsIcon.png -------------------------------------------------------------------------------- /assets/posHandle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/posHandle.png -------------------------------------------------------------------------------- /assets/posHandle_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/posHandle_p.png -------------------------------------------------------------------------------- /assets/prev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/prev.png -------------------------------------------------------------------------------- /assets/prev_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/prev_p.png -------------------------------------------------------------------------------- /assets/repeat_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/repeat_off.png -------------------------------------------------------------------------------- /assets/repeat_off_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/repeat_off_p.png -------------------------------------------------------------------------------- /assets/repeat_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/repeat_on.png -------------------------------------------------------------------------------- /assets/repeat_on_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/repeat_on_p.png -------------------------------------------------------------------------------- /assets/scroll_handle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/scroll_handle.png -------------------------------------------------------------------------------- /assets/scroll_handle_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/scroll_handle_p.png -------------------------------------------------------------------------------- /assets/shuffle_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/shuffle_off.png -------------------------------------------------------------------------------- /assets/shuffle_off_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/shuffle_off_p.png -------------------------------------------------------------------------------- /assets/shuffle_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/shuffle_on.png -------------------------------------------------------------------------------- /assets/shuffle_on_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/shuffle_on_p.png -------------------------------------------------------------------------------- /assets/source-icon-bluetooth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/source-icon-bluetooth.png -------------------------------------------------------------------------------- /assets/source-icon-cd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/source-icon-cd.png -------------------------------------------------------------------------------- /assets/source-icon-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/source-icon-file.png -------------------------------------------------------------------------------- /assets/source-icon-spotify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/source-icon-spotify.png -------------------------------------------------------------------------------- /assets/spotifyIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/spotifyIcon.png -------------------------------------------------------------------------------- /assets/status_paused.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/status_paused.png -------------------------------------------------------------------------------- /assets/status_playing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/status_playing.png -------------------------------------------------------------------------------- /assets/status_stopped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/status_stopped.png -------------------------------------------------------------------------------- /assets/stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/stop.png -------------------------------------------------------------------------------- /assets/stop_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/stop_p.png -------------------------------------------------------------------------------- /assets/visualizationBackground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/visualizationBackground.png -------------------------------------------------------------------------------- /assets/volumeHandle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/volumeHandle.png -------------------------------------------------------------------------------- /assets/volumeHandle_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/volumeHandle_p.png -------------------------------------------------------------------------------- /assets/windowClose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/windowClose.png -------------------------------------------------------------------------------- /assets/windowMaximize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/windowMaximize.png -------------------------------------------------------------------------------- /assets/windowMinimize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/assets/windowMinimize.png -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | linamp (1.4.0) bookworm; urgency=medium 2 | 3 | * Feature: Spotify Source, use Linamp as a Spotify Connect receiver 4 | * UI: New design for Sources menu 5 | * UI: Volume and balance sliders background color changes according to value 6 | * Refactor: Unify Bluetooth and Spotify sources code as generic audiosourcepython 7 | 8 | -- Rodrigo Méndez Mon, 30 Dec 2024 12:04:00 -0600 9 | 10 | linamp (1.3.0) bookworm; urgency=medium 11 | 12 | * Feature: Bluetooth Source, use Linamp as a Bluetooth audio receiver 13 | 14 | -- Rodrigo Méndez Tue, 20 Dec 2024 22:37:00 -0600 15 | 16 | linamp (1.2.1) bookworm; urgency=medium 17 | 18 | * Bugfix: Fixed glitches on spectrum analyzer for views that use audiospectrumcatpure class (CD player, Bluetooth player) 19 | * Bugfix: Fixed spectrum analyzer not showing spectrum of right channel on file player 20 | * Bugfix: Fix crashes on CD player when a request to musicbrainz fails 21 | 22 | -- Rodrigo Méndez Tue, 10 Dec 2024 23:22:00 -0600 23 | 24 | linamp (1.2.0) bookworm; urgency=medium 25 | 26 | * Bugfix: After boot and loading the first file into the playlist, playback fails. 27 | * Bugfix: Fix issue caused by playing and sopping the same file repeatedly (it glitched and refused to play). 28 | * Refactor: new folder structure for better code maintainability. 29 | 30 | -- Rodrigo Méndez Tue, 17 Sep 2024 21:54:00 -0600 31 | 32 | linamp (1.1.2) bookworm; urgency=medium 33 | 34 | * Trim trailing whitespace. 35 | * Upgrade to newer source format 3.0 (native). 36 | 37 | -- Rodrigo Méndez Mon, 08 Jul 2024 18:19:34 -0600 38 | 39 | linamp (1.1.1) bookworm; urgency=medium 40 | 41 | * Improvements for debian packaging. 42 | 43 | -- Rodrigo Méndez Mon, 8 Jul 2024 18:00:00 -0600 44 | 45 | linamp (1.1.0) bookworm; urgency=medium 46 | 47 | * Initial release. 48 | 49 | -- Taneli Leppä Tue, 18 Jun 2024 14:00:21 +0200 50 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: linamp 2 | Section: sound 3 | Priority: optional 4 | Maintainer: Rodrigo Méndez 5 | Rules-Requires-Root: no 6 | Build-Depends: 7 | debhelper-compat (= 13), 8 | qt6-base-dev, 9 | qt6-base-dev-tools, 10 | qt6-multimedia-dev, 11 | qtcreator, 12 | cmake, 13 | libtag1-dev, 14 | libasound2-dev, 15 | libpulse-dev, 16 | libpipewire-0.3-dev, 17 | libdbus-1-dev, 18 | libiso9660-dev, 19 | libcdio-dev, 20 | libcdio-utils, 21 | swig, 22 | python3-dev, 23 | dh-python 24 | Standards-Version: 4.6.2 25 | Homepage: https://github.com/Rodmg/linamp 26 | Vcs-Browser: https://github.com/Rodmg/linamp 27 | Vcs-Git: https://github.com/Rodmg/linamp.git 28 | 29 | Package: linamp 30 | Architecture: any 31 | Depends: 32 | ${shlibs:Depends}, 33 | ${misc:Depends}, 34 | ${python3:Depends}, 35 | vlc, 36 | python3-full, 37 | python3-libdiscid, 38 | python3-musicbrainzngs, 39 | python3-cdio, 40 | python3-vlc, 41 | python3-dbus-next 42 | Description: Your favorite music player of the 90s, but in real life 43 | Music player app for Linamp - Your favorite music player of the 90s, 44 | but in real life. 45 | Built with QT6. 46 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Source: 3 | Upstream-Name: linamp 4 | Upstream-Contact: Rodrigo Méndez 5 | 6 | Files: * 7 | Copyright: Copyright (C) 2024 Rodrigo Méndez 8 | License: GPL-3 9 | 10 | Files: debian/* 11 | Copyright: Copyright (C) 2024 Taneli Leppä 12 | License: GPL-3 13 | 14 | License: GPL-3 15 | See LICENSE in parent directory 16 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | %: 4 | dh $@ --with python3 --buildsystem=cmake 5 | 6 | override_dh_auto_install: 7 | mkdir -p debian/linamp/usr/bin 8 | cp build/player debian/linamp/usr/bin/linamp-player 9 | 10 | mkdir -p debian/linamp/usr/lib/python3/dist-packages/linamp 11 | cp -r python/linamp debian/linamp/usr/lib/python3/dist-packages/ 12 | 13 | mkdir -p debian/linamp/usr/share/applications/ 14 | cp linamp.desktop debian/linamp/usr/share/applications/ 15 | 16 | mkdir -p debian/linamp/usr/share/icons/hicolor/72x72/apps/ 17 | cp assets/logoButton.png debian/linamp/usr/share/icons/hicolor/72x72/apps/linamp-player.png 18 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | IFS=$'\n\t' 5 | 6 | echo "This script re-installs linamp executable files in linamp-os. This should only be run inside linamp-os" 7 | 8 | # Assumes the build files are on the same folder as this script 9 | rm -rf ~/linamp/* 10 | cp -r build ~/linamp/ 11 | cp shutdown.sh ~/linamp/ 12 | cp start.sh ~/linamp/ 13 | cp -r venv ~/linamp/ 14 | cp -r python ~/linamp/ 15 | 16 | echo "Done!" 17 | -------------------------------------------------------------------------------- /linamp.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=1.0 3 | Name=Linamp 4 | Comment=Music player app for Linamp - Your favorite music player of the 90s, but in real life. 5 | Exec=/usr/bin/linamp-player %U 6 | Icon=/usr/share/icons/hicolor/72x72/apps/linamp-player.png 7 | Terminal=false 8 | #StartupWMClass=name of application 9 | Type=Application 10 | Categories=Audio 11 | Keywords=linamp;music;sound;audio;player; -------------------------------------------------------------------------------- /python/linamp-mock/__init__.py: -------------------------------------------------------------------------------- 1 | from .mock_cdplayer import * 2 | -------------------------------------------------------------------------------- /python/linamp-mock/mock_cdplayer.py: -------------------------------------------------------------------------------- 1 | import vlc 2 | import time 3 | 4 | 5 | class CDPlayer: 6 | 7 | def __init__(self, device="/dev/cdrom") -> None: 8 | self._device = device 9 | self.vlc_instance = vlc.Instance() 10 | self.player = None 11 | self.media_list = None 12 | self.list_player = None 13 | self.disc_loaded = False 14 | # Array of tuples with format (tracknumber: int, artist, album, title, duration: int, is_data_track: bool) 15 | self.track_info = [] 16 | self.shuffle = False 17 | self.repeat = False 18 | 19 | self._mock_status = "no-disc" 20 | self._mock_current_track_idx = 0 21 | 22 | def _set_track_info(self, artists, track_titles, album, durations, is_data_tracks): 23 | tracks = [] 24 | for i in range(0, len(track_titles)): 25 | track = ( 26 | i + 1, 27 | artists[i], 28 | album, 29 | track_titles[i], 30 | durations[i], 31 | is_data_tracks[i], 32 | ) 33 | tracks.append(track) 34 | self.track_info = tracks 35 | 36 | def _do_set_repeat(self): 37 | if self.list_player is not None: 38 | if self.repeat: 39 | self.list_player.set_playback_mode(vlc.PlaybackMode.loop) 40 | else: 41 | self.list_player.set_playback_mode(vlc.PlaybackMode.default) 42 | 43 | def _do_set_shuffle(self): 44 | pass 45 | 46 | # -------- Control Functions -------- 47 | 48 | def load(self): 49 | 50 | # simulate slow data fetch 51 | time.sleep(5) 52 | 53 | artists = ["Test Artist"] * 4 54 | track_titles = ["Track 1", "Track 2", "Track 3", "Track 4"] 55 | album = "Test Album" 56 | durations = [180000, 182000, 185000, 184000] 57 | is_data_tracks = [False] * 4 58 | 59 | self._set_track_info(artists, track_titles, album, durations, is_data_tracks) 60 | 61 | self.disc_loaded = True 62 | self._mock_status = "stopped" 63 | 64 | def unload(self): 65 | self.player = None 66 | self.media_list = None 67 | self.list_player = None 68 | self.disc_loaded = False 69 | self.track_info = [] 70 | 71 | def play(self): 72 | self._mock_status = "playing" 73 | 74 | def stop(self): 75 | self._mock_status = "stopped" 76 | 77 | def pause(self): 78 | self._mock_status = "paused" 79 | 80 | def next(self): 81 | if self._mock_current_track_idx < len(self.track_info) - 1: 82 | self._mock_current_track_idx = self._mock_current_track_idx + 1 83 | 84 | def prev(self): 85 | if self._mock_current_track_idx > 0: 86 | self._mock_current_track_idx = self._mock_current_track_idx - 1 87 | 88 | # Jump to a specific track 89 | def jump(self, index): 90 | self._mock_current_track_idx = index 91 | 92 | # Go to a specific time in a track while playing 93 | def seek(self, ms): 94 | if self.player is None: 95 | return 96 | 97 | track_duration = self.player.get_media().get_duration() 98 | percentage = ms / track_duration 99 | self.player.set_position(percentage) 100 | 101 | def set_shuffle(self, enabled): 102 | # self.shuffle = enabled 103 | print("WARNING: Shuffle not supported by vlc backend") 104 | self._do_set_shuffle() 105 | 106 | def set_repeat(self, enabled): 107 | self.repeat = enabled 108 | self._do_set_repeat() 109 | 110 | def eject(self): 111 | if self.disc_loaded: 112 | self.stop() 113 | self._mock_status = "no-disc" 114 | self.unload() 115 | else: 116 | self.load() 117 | self._mock_status = "stopped" 118 | 119 | # -------- Status Functions -------- 120 | 121 | def get_postition(self): 122 | if self.player is None: 123 | return 0 124 | time = self.player.get_time() 125 | if time < 0: 126 | time = 0 127 | return time 128 | 129 | def get_shuffle(self): 130 | return self.shuffle 131 | 132 | def get_repeat(self): 133 | return self.repeat 134 | 135 | def get_status(self): 136 | return self._mock_status 137 | 138 | def get_all_tracks_info(self): 139 | # Filter data tracks 140 | tracks = [] 141 | for track in self.track_info: 142 | if track[5] == False: 143 | tracks.append(track) 144 | return tracks 145 | 146 | def get_track_info(self, index): 147 | if index >= len(self.track_info) or index < 0: 148 | raise Exception("Invalid track number") 149 | return self.track_info[index] 150 | 151 | def get_current_track_info(self): 152 | if not self.disc_loaded: 153 | raise Exception("Not playing") 154 | index = self._mock_current_track_idx 155 | if index < 0: 156 | index = 0 157 | return self.get_track_info(index) 158 | 159 | # -------- Events to be called by a timer -------- 160 | 161 | def detect_disc_insertion(self): 162 | if not self.disc_loaded: 163 | self.load() 164 | return True 165 | 166 | return False 167 | -------------------------------------------------------------------------------- /python/linamp/__init__.py: -------------------------------------------------------------------------------- 1 | from linamp.cdplayer import * 2 | from linamp.btplayer import * 3 | from linamp.spotifyplayer import * 4 | -------------------------------------------------------------------------------- /python/linamp/baseplayer/__init__.py: -------------------------------------------------------------------------------- 1 | from linamp.baseplayer.baseplayer import * 2 | -------------------------------------------------------------------------------- /python/linamp/baseplayer/baseplayer.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class PlayerStatus(Enum): 4 | Idle = 'idle' 5 | Playing = 'playing' 6 | Stopped = 'stopped' 7 | Paused = 'paused' 8 | Error = 'error' 9 | Loading = 'loading' 10 | 11 | 12 | # Base Player abstract class 13 | class BasePlayer: 14 | # Called when the source is selected in the UI or whenever the full track info needs to be refreshed 15 | def load(self) -> None: 16 | pass 17 | 18 | # Called when the source is deselected in the UI 19 | def unload(self) -> None: 20 | pass 21 | 22 | # Basic playback control funcions called on button press 23 | def play(self) -> None: 24 | pass 25 | 26 | def stop(self) -> None: 27 | pass 28 | 29 | def pause(self) -> None: 30 | pass 31 | 32 | def next(self) -> None: 33 | pass 34 | 35 | def prev(self) -> None: 36 | pass 37 | 38 | # Go to a specific time in a track while playing 39 | def seek(self, ms: int) -> None: 40 | pass 41 | 42 | # Playback mode toggles 43 | def set_shuffle(self, enabled: bool) -> None: 44 | pass 45 | 46 | def set_repeat(self, enabled: bool) -> None: 47 | pass 48 | 49 | # Eject button 50 | def eject(self) -> None: 51 | pass 52 | 53 | # -------- Status Functions -------- 54 | 55 | # Called constantly during playback to update the time progress 56 | def get_postition(self) -> int: 57 | return 0 58 | 59 | def get_shuffle(self) -> bool: 60 | return False 61 | 62 | def get_repeat(self) -> bool: 63 | return False 64 | 65 | # Returns the str representation of PlayerStatus enum 66 | def get_status(self) -> str: 67 | status = PlayerStatus.Idle 68 | return status.value 69 | 70 | # tuple with format (tracknumber: int, artist, album, title, duration_ms: int, codec: str, bitrate_bps: int, samplerate_hz: int) 71 | def get_track_info(self) -> tuple[int, str, str, str, int, str, int, int]: 72 | return (1, '', '', '', 1, 'AAC', 256000, 44100) 73 | 74 | # Return any message you want to show to the user. tuple with format: (show_message: bool, message: str, message_timeout_ms: int) 75 | def get_message(self) -> tuple[bool, str, int]: 76 | return (False, '', 0) 77 | 78 | # Called whenever the UI wants to force clear the message 79 | def clear_message(self) -> None: 80 | pass 81 | 82 | # -------- Polling functions -------- 83 | 84 | # Called by the UI before calling getter to update the view (currently once each second) 85 | # Return True if you want to request focus to this source 86 | def poll_events(self) -> bool: 87 | return False 88 | 89 | # If your source requires asyncio or other event loop, run it here 90 | # This will be run in a new thread when linamp starts 91 | def run_loop(self): 92 | pass -------------------------------------------------------------------------------- /python/linamp/btplayer/__init__.py: -------------------------------------------------------------------------------- 1 | from linamp.btplayer.btplayer import * 2 | -------------------------------------------------------------------------------- /python/linamp/btplayer/btplayer.py: -------------------------------------------------------------------------------- 1 | from linamp.baseplayer import BasePlayer, PlayerStatus 2 | from linamp.btplayer.btadapter import BTPlayerAdapter, is_empty_player_track 3 | 4 | EMPTY_TRACK_INFO = ( 5 | 0, 6 | '', 7 | '', 8 | '', 9 | 0, 10 | '', 11 | 0, 12 | 44100 13 | ) 14 | 15 | class BTPlayer(BasePlayer): 16 | 17 | message: str 18 | show_message: bool 19 | message_timeout: int 20 | 21 | player: BTPlayerAdapter 22 | track_info: tuple[int, str, str, str, int, str, int, int] 23 | 24 | def __init__(self) -> None: 25 | self.player = BTPlayerAdapter() 26 | # tuple with format (tracknumber: int, artist, album, title, duration: int, codec: str, bitrate_bps: int, samplerate_hz: int) 27 | self.track_info = EMPTY_TRACK_INFO 28 | 29 | self.clear_message() 30 | 31 | def _display_connection_info(self): 32 | if self.player.connected: 33 | self.message = f'CONNNECTED TO: {self.player.device_alias}' 34 | self.show_message = True 35 | self.message_timeout = 5000 36 | else: 37 | self.message = 'DISCONNECTED' 38 | self.show_message = True 39 | self.message_timeout = 5000 40 | 41 | # -------- Control Functions -------- 42 | 43 | def load(self) -> None: 44 | self.player.find_player_sync() 45 | if self.player.connected: 46 | track = self.player.track 47 | if not track or is_empty_player_track(track): 48 | self._display_connection_info() 49 | return 50 | self.track_info = ( 51 | track.track_number, 52 | track.artist, 53 | track.album, 54 | track.title, 55 | track.duration, 56 | self.player.get_codec_str(), 57 | 0, # No simple way to know bitrate from BT 58 | 44100 59 | ) 60 | else: 61 | self.track_info = EMPTY_TRACK_INFO 62 | self._display_connection_info() 63 | 64 | def unload(self) -> None: 65 | self.track_info = EMPTY_TRACK_INFO 66 | self.clear_message() 67 | 68 | def play(self) -> None: 69 | self.player.play() 70 | 71 | def stop(self) -> None: 72 | self.player.stop() 73 | 74 | def pause(self) -> None: 75 | self.player.pause() 76 | 77 | def next(self) -> None: 78 | self.player.next() 79 | 80 | def prev(self) -> None: 81 | self.player.previous() 82 | 83 | # Go to a specific time in a track while playing 84 | def seek(self, ms: int) -> None: 85 | self.message = 'NOT SUPPORTED' 86 | self.show_message = True 87 | self.message_timeout = 3000 88 | 89 | def set_shuffle(self, enabled: bool) -> None: 90 | self.player.set_shuffle(enabled) 91 | 92 | def set_repeat(self, enabled: bool) -> None: 93 | self.player.set_repeat(enabled) 94 | 95 | def eject(self) -> None: 96 | self.message = 'NOT SUPPORTED' 97 | self.show_message = True 98 | self.message_timeout = 3000 99 | 100 | # -------- Status Functions -------- 101 | 102 | def get_postition(self) -> int: 103 | return self.player.position 104 | 105 | def get_shuffle(self) -> bool: 106 | return self.player.shuffle != 'off' 107 | 108 | def get_repeat(self) -> bool: 109 | return self.player.repeat != 'off' 110 | 111 | # Returns the str representation of PlayerStatus enum 112 | def get_status(self) -> str: 113 | status = PlayerStatus.Idle 114 | btstatus = self.player.status 115 | if btstatus == 'playing': 116 | status = PlayerStatus.Playing 117 | if btstatus == 'stopped': 118 | status = PlayerStatus.Stopped 119 | if btstatus == 'paused': 120 | status = PlayerStatus.Paused 121 | if btstatus == 'error': 122 | status = PlayerStatus.Error 123 | if btstatus == 'forward-seek': 124 | status = PlayerStatus.Loading 125 | if btstatus == 'reverse-seek': 126 | status = PlayerStatus.Loading 127 | return status.value 128 | 129 | def get_track_info(self) -> tuple[int, str, str, str, int, str, int, int]: 130 | return self.track_info 131 | 132 | # Return any message you want to show to the user. tuple with format: (show_message: bool, message: str, message_timeout_ms: int) 133 | def get_message(self) -> tuple[bool, str, int]: 134 | return (self.show_message, self.message, self.message_timeout) 135 | 136 | def clear_message(self) -> None: 137 | self.show_message = False 138 | self.message = '' 139 | self.message_timeout = 0 140 | 141 | # -------- Events to be called by a timer -------- 142 | 143 | def poll_events(self) -> bool: 144 | was_connected = self.player.connected 145 | self.load() 146 | 147 | # Should tell UI to refresh if we are connected and were not connected before 148 | return self.player.connected and not was_connected 149 | 150 | # This will be run in a new thread when linamp starts 151 | def run_loop(self): 152 | self.player.setup_sync() -------------------------------------------------------------------------------- /python/linamp/spotifyplayer/__init__.py: -------------------------------------------------------------------------------- 1 | from linamp.spotifyplayer.spotifyplayer import * 2 | -------------------------------------------------------------------------------- /python/linamp/spotifyplayer/librespot-event-handler.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | IFS=$'\n\t' 5 | 6 | if [[ $PLAYER_EVENT == 'session_connected' || $PLAYER_EVENT == 'session_disconnected' ]]; then 7 | dbus-send --type=method_call --dest=org.linamp.Librespot /org/linamp/librespot org.linamp.LibrespotInterface.send_event dict:string:string:'event',"$PLAYER_EVENT",'user_name',"$USER_NAME",'connection_id',"$CONNECTION_ID" 8 | fi 9 | 10 | if [[ $PLAYER_EVENT == 'shuffle_changed' ]]; then 11 | dbus-send --type=method_call --dest=org.linamp.Librespot /org/linamp/librespot org.linamp.LibrespotInterface.send_event dict:string:string:'event',"$PLAYER_EVENT",'shuffle',"$SHUFFLE" 12 | fi 13 | 14 | if [[ $PLAYER_EVENT == 'repeat_changed' ]]; then 15 | dbus-send --type=method_call --dest=org.linamp.Librespot /org/linamp/librespot org.linamp.LibrespotInterface.send_event dict:string:string:'event',"$PLAYER_EVENT",'repeat',"$REPEAT" 16 | fi 17 | 18 | if [[ $PLAYER_EVENT == 'track_changed' ]]; then 19 | dbus-send --type=method_call --dest=org.linamp.Librespot /org/linamp/librespot org.linamp.LibrespotInterface.send_event dict:string:string:'event',"$PLAYER_EVENT",\ 20 | 'item_type',"${ITEM_TYPE:-''}",\ 21 | 'track_id',"${TRACK_ID:-''}",\ 22 | 'uri',"${URI:-''}",\ 23 | 'name',"${NAME:-''}",\ 24 | 'duration_ms',"${DURATION_MS:-''}",\ 25 | 'is_explicit',"${IS_EXPLICIT:-''}",\ 26 | 'language',"${LANGUAGE:-''}",\ 27 | 'covers',"${COVERS:-''}",\ 28 | 'number',"${NUMBER:-''}",\ 29 | 'disc_number',"${DISC_NUMBER:-''}",\ 30 | 'popularity',"${POPULARITY:-''}",\ 31 | 'album',"${ALBUM:-''}",\ 32 | 'artists',"${ARTISTS:-''}",\ 33 | 'album_artists',"${ALBUM_ARTISTS:-''}",\ 34 | 'show_name',"${SHOW_NAME:-''}",\ 35 | 'publish_time',"${PUBLISH_TIME:-''}",\ 36 | 'description',"${DESCRIPTION:-''}" 37 | fi 38 | 39 | if [[ $PLAYER_EVENT == 'playing' || $PLAYER_EVENT == 'paused' || $PLAYER_EVENT == 'seeked' || $PLAYER_EVENT == 'position_correction' ]]; then 40 | dbus-send --type=method_call --dest=org.linamp.Librespot /org/linamp/librespot org.linamp.LibrespotInterface.send_event dict:string:string:'event',"$PLAYER_EVENT",'track_id',"$TRACK_ID",'position_ms',"$POSITION_MS" 41 | fi -------------------------------------------------------------------------------- /python/linamp/spotifyplayer/spotifyadapter.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | 4 | from dbus_next.aio import MessageBus 5 | from dbus_next.service import ServiceInterface, method 6 | 7 | DBUS_INTERFACE = 'org.linamp.LibrespotInterface' 8 | 9 | class SpotifyTrackInfo(): 10 | def __init__(self, title: str = '', track_number: int = 0, number_of_tracks: int = 0, duration: int = 0, album: str = '', artist: str = ''): 11 | self.title = title 12 | self.track_number = track_number 13 | self.number_of_tracks = number_of_tracks 14 | self.duration = duration 15 | self.album = album 16 | self.artist = artist 17 | 18 | def __str__(self): 19 | repr = '\n' 20 | repr = repr + f' Title: {self.title}\n' 21 | repr = repr + f' Album: {self.album}\n' 22 | repr = repr + f' Artist: {self.artist}\n' 23 | 24 | seconds_total = self.duration/1000 25 | minutes = int(seconds_total/60) 26 | seconds = int(seconds_total - (minutes * 60)) 27 | 28 | repr = repr + f' Duration: {str(minutes).zfill(2)}:{str(seconds).zfill(2)} ({self.duration})\n' 29 | repr = repr + f' Track Number: {self.track_number}\n' 30 | repr = repr + f' Number of Tracks: {self.number_of_tracks}\n' 31 | 32 | return repr 33 | 34 | def is_empty_player_track(track: SpotifyTrackInfo) -> bool: 35 | return track.duration <= 0 36 | 37 | 38 | # Posible values for status: 39 | # - stopped 40 | # - playing 41 | # - paused 42 | 43 | class SpotifyPlayerAdapter(ServiceInterface): 44 | bus = None 45 | 46 | connected = False 47 | status = 'stopped' 48 | track = SpotifyTrackInfo() 49 | position = 0 50 | last_updated_position = None # time 51 | repeat = 'off' 52 | shuffle = 'off' 53 | 54 | def __init__(self): 55 | super().__init__(DBUS_INTERFACE) 56 | 57 | @method() 58 | def send_event(self, data: 'a{ss}'): 59 | event = data.get('event') 60 | if event == 'session_connected': 61 | self.connected = True 62 | if event == 'session_disconnected': 63 | self.connected = False 64 | self.status = 'stopped' 65 | self.track = SpotifyTrackInfo() 66 | self._set_position(0) 67 | self.repeat = 'off' 68 | self.shuffle = 'off' 69 | if event == 'shuffle_changed': 70 | shuffle_str = data.get('shuffle', '') 71 | self.shuffle = 'on' if shuffle_str == 'true' else 'off' 72 | if event == 'repeat_changed': 73 | repeat_str = data.get('repeat', '') 74 | self.repeat = 'on' if repeat_str == 'true' else 'off' 75 | if event == 'playing' or event == 'paused': 76 | self.connected = True 77 | self.status = event 78 | self._set_position(int(data.get('position_ms', '0'))) 79 | if event == 'seeked' or event == 'position_correction': 80 | self._set_position(int(data.get('position_ms', '0'))) 81 | if event == 'track_changed': 82 | item_type = data.get('item_type') 83 | 84 | title = data.get('name', '') 85 | number_of_tracks = 0 86 | duration = int(data.get('duration_ms', '0')) 87 | 88 | track_number = int(data.get('number', '0')) 89 | album = data.get('album', '') 90 | artist = data.get('artists', '') 91 | 92 | if item_type == 'Episode': 93 | # Special case for podcasts 94 | track_number = 0 95 | album = '' 96 | artist = data.get('show_name', '') 97 | 98 | self.track = SpotifyTrackInfo(title, track_number, number_of_tracks, duration, album, artist) 99 | 100 | def _set_position(self, pos: int): 101 | self.position = pos 102 | self.last_updated_position = time.time_ns() 103 | 104 | def get_postition(self) -> int: 105 | """Updates position when playing if it hasn't been updated by an event""" 106 | now = time.time_ns() 107 | then = self.last_updated_position 108 | if self.status == 'playing' and then is not None: 109 | diff_ms = (now - then)/1000000 110 | self._set_position(int(self.position + diff_ms)) 111 | 112 | return self.position 113 | 114 | async def setup(self): 115 | """Initialize DBus""" 116 | self.bus = await MessageBus().connect() 117 | self.bus.export('/org/linamp/librespot', self) 118 | 119 | async def loop(self): 120 | await self.setup() 121 | await self.bus.request_name('org.linamp.Librespot') 122 | await self.bus.wait_for_disconnect() 123 | 124 | def print_state(self): 125 | print(f'Connected: {self.connected}') 126 | print(f'Status: {self.status}') 127 | 128 | seconds_total = self.position/1000 129 | minutes = int(seconds_total/60) 130 | seconds = int(seconds_total - (minutes * 60)) 131 | 132 | print(f'Position: {str(minutes).zfill(2)}:{str(seconds).zfill(2)} ({self.position})') 133 | print(f'Repeat: {self.repeat}') 134 | print(f'Shuffle: {self.shuffle}') 135 | print(f'Track: {self.track}') 136 | 137 | # Runs the asyncio event loop, should be called from a new thread 138 | def run_loop(self): 139 | loop = asyncio.new_event_loop() 140 | asyncio.set_event_loop(loop) 141 | loop.run_until_complete(self.loop()) 142 | -------------------------------------------------------------------------------- /python/linamp/spotifyplayer/spotifyplayer.py: -------------------------------------------------------------------------------- 1 | from linamp.baseplayer import BasePlayer, PlayerStatus 2 | from linamp.spotifyplayer.spotifyadapter import SpotifyPlayerAdapter, is_empty_player_track 3 | 4 | EMPTY_TRACK_INFO = ( 5 | 0, 6 | '', 7 | '', 8 | '', 9 | 0, 10 | '', 11 | 0, 12 | 44100 13 | ) 14 | 15 | class SpotifyPlayer(BasePlayer): 16 | 17 | message: str 18 | show_message: bool 19 | message_timeout: int 20 | 21 | player: SpotifyPlayerAdapter 22 | track_info: tuple[int, str, str, str, int, str, int, int] 23 | 24 | was_connected = False 25 | 26 | def __init__(self) -> None: 27 | self.player = SpotifyPlayerAdapter() 28 | # tuple with format (tracknumber: int, artist, album, title, duration: int, codec: str, bitrate_bps: int, samplerate_hz: int) 29 | self.track_info = EMPTY_TRACK_INFO 30 | 31 | self.clear_message() 32 | 33 | def _display_connection_info(self): 34 | if self.player.connected: 35 | self.message = 'CONNNECTED' 36 | self.show_message = True 37 | self.message_timeout = 5000 38 | else: 39 | self.message = 'DISCONNECTED' 40 | self.show_message = True 41 | self.message_timeout = 5000 42 | 43 | def _not_supported(self): 44 | self.message = 'NOT SUPPORTED' 45 | self.show_message = True 46 | self.message_timeout = 3000 47 | 48 | # -------- Control Functions -------- 49 | 50 | def load(self) -> None: 51 | if self.player.connected: 52 | track = self.player.track 53 | if not track or is_empty_player_track(track): 54 | self._display_connection_info() 55 | return 56 | self.track_info = ( 57 | track.track_number, 58 | track.artist, 59 | track.album, 60 | track.title, 61 | track.duration, 62 | '', 63 | 320000, 64 | 44100 65 | ) 66 | else: 67 | self.track_info = EMPTY_TRACK_INFO 68 | self._display_connection_info() 69 | 70 | def unload(self) -> None: 71 | self.track_info = EMPTY_TRACK_INFO 72 | self.clear_message() 73 | 74 | def play(self) -> None: 75 | self._not_supported() 76 | 77 | def stop(self) -> None: 78 | self._not_supported() 79 | 80 | def pause(self) -> None: 81 | self._not_supported() 82 | 83 | def next(self) -> None: 84 | self._not_supported() 85 | 86 | def prev(self) -> None: 87 | self._not_supported() 88 | 89 | # Go to a specific time in a track while playing 90 | def seek(self, ms: int) -> None: 91 | self._not_supported() 92 | 93 | def set_shuffle(self, enabled: bool) -> None: 94 | self._not_supported() 95 | 96 | def set_repeat(self, enabled: bool) -> None: 97 | self._not_supported() 98 | 99 | def eject(self) -> None: 100 | self._not_supported() 101 | 102 | # -------- Status Functions -------- 103 | 104 | def get_postition(self) -> int: 105 | return self.player.get_postition() 106 | 107 | def get_shuffle(self) -> bool: 108 | return self.player.shuffle != 'off' 109 | 110 | def get_repeat(self) -> bool: 111 | return self.player.repeat != 'off' 112 | 113 | # Returns the str representation of PlayerStatus enum 114 | def get_status(self) -> str: 115 | status = PlayerStatus.Idle 116 | spotstatus = self.player.status 117 | if spotstatus == 'playing': 118 | status = PlayerStatus.Playing 119 | if spotstatus == 'stopped': 120 | status = PlayerStatus.Stopped 121 | if spotstatus == 'paused': 122 | status = PlayerStatus.Paused 123 | if spotstatus == 'error': 124 | status = PlayerStatus.Error 125 | return status.value 126 | 127 | def get_track_info(self) -> tuple[int, str, str, str, int, str, int, int]: 128 | return self.track_info 129 | 130 | # Return any message you want to show to the user. tuple with format: (show_message: bool, message: str, message_timeout_ms: int) 131 | def get_message(self) -> tuple[bool, str, int]: 132 | return (self.show_message, self.message, self.message_timeout) 133 | 134 | def clear_message(self) -> None: 135 | self.show_message = False 136 | self.message = '' 137 | self.message_timeout = 0 138 | 139 | # -------- Events to be called by a timer -------- 140 | 141 | def poll_events(self) -> bool: 142 | self.load() 143 | 144 | should_request_focus = self.player.connected and not self.was_connected 145 | self.was_connected = self.player.connected 146 | 147 | # Should tell UI to refresh if we are connected and were not connected before 148 | return should_request_focus 149 | 150 | # Runs the asyncio event loop, should be called from a new thread 151 | def run_loop(self): 152 | self.player.run_loop() -------------------------------------------------------------------------------- /python/requirements.txt: -------------------------------------------------------------------------------- 1 | python-libdiscid==2.0.3 2 | musicbrainzngs==0.7.1 3 | pycdio==2.1.1 4 | python-vlc==3.0.20123 5 | dbus-next==0.2.3 6 | -------------------------------------------------------------------------------- /scale-skin.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | IFS=$'\n\t' 5 | 6 | for filePath in skin/*.png; do 7 | if [ -f "$filePath" ]; then 8 | fileName="${filePath##*/}" 9 | echo "$fileName" 10 | convert -scale 400% "$filePath" "assets/$fileName" 11 | fi 12 | done 13 | 14 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | IFS=$'\n\t' 5 | 6 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 7 | cd "$SCRIPT_DIR" 8 | 9 | # This setups python environment 10 | sudo apt-get install build-essential vlc libiso9660-dev libcdio-dev libcdio-utils swig python3-pip python3-full python3-dev libdiscid0 libdiscid-dev -y 11 | 12 | python3 -m venv venv 13 | source venv/bin/activate 14 | pip install -r python/requirements.txt 15 | -------------------------------------------------------------------------------- /shutdown.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | sudo shutdown now 4 | -------------------------------------------------------------------------------- /skin/balanceHandle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/balanceHandle.png -------------------------------------------------------------------------------- /skin/balanceHandle_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/balanceHandle_p.png -------------------------------------------------------------------------------- /skin/eq_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/eq_off.png -------------------------------------------------------------------------------- /skin/eq_off_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/eq_off_p.png -------------------------------------------------------------------------------- /skin/eq_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/eq_on.png -------------------------------------------------------------------------------- /skin/eq_on_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/eq_on_p.png -------------------------------------------------------------------------------- /skin/logoButton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/logoButton.png -------------------------------------------------------------------------------- /skin/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/next.png -------------------------------------------------------------------------------- /skin/next_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/next_p.png -------------------------------------------------------------------------------- /skin/open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/open.png -------------------------------------------------------------------------------- /skin/open_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/open_p.png -------------------------------------------------------------------------------- /skin/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/pause.png -------------------------------------------------------------------------------- /skin/pause_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/pause_p.png -------------------------------------------------------------------------------- /skin/pl_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/pl_add.png -------------------------------------------------------------------------------- /skin/pl_add_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/pl_add_p.png -------------------------------------------------------------------------------- /skin/pl_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/pl_close.png -------------------------------------------------------------------------------- /skin/pl_close_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/pl_close_p.png -------------------------------------------------------------------------------- /skin/pl_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/pl_off.png -------------------------------------------------------------------------------- /skin/pl_off_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/pl_off_p.png -------------------------------------------------------------------------------- /skin/pl_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/pl_on.png -------------------------------------------------------------------------------- /skin/pl_on_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/pl_on_p.png -------------------------------------------------------------------------------- /skin/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/play.png -------------------------------------------------------------------------------- /skin/play_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/play_p.png -------------------------------------------------------------------------------- /skin/posHandle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/posHandle.png -------------------------------------------------------------------------------- /skin/posHandle_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/posHandle_p.png -------------------------------------------------------------------------------- /skin/prev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/prev.png -------------------------------------------------------------------------------- /skin/prev_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/prev_p.png -------------------------------------------------------------------------------- /skin/repeat_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/repeat_off.png -------------------------------------------------------------------------------- /skin/repeat_off_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/repeat_off_p.png -------------------------------------------------------------------------------- /skin/repeat_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/repeat_on.png -------------------------------------------------------------------------------- /skin/repeat_on_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/repeat_on_p.png -------------------------------------------------------------------------------- /skin/scroll_handle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/scroll_handle.png -------------------------------------------------------------------------------- /skin/scroll_handle_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/scroll_handle_p.png -------------------------------------------------------------------------------- /skin/shuffle_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/shuffle_off.png -------------------------------------------------------------------------------- /skin/shuffle_off_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/shuffle_off_p.png -------------------------------------------------------------------------------- /skin/shuffle_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/shuffle_on.png -------------------------------------------------------------------------------- /skin/shuffle_on_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/shuffle_on_p.png -------------------------------------------------------------------------------- /skin/status_paused.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/status_paused.png -------------------------------------------------------------------------------- /skin/status_playing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/status_playing.png -------------------------------------------------------------------------------- /skin/status_stopped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/status_stopped.png -------------------------------------------------------------------------------- /skin/stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/stop.png -------------------------------------------------------------------------------- /skin/stop_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/stop_p.png -------------------------------------------------------------------------------- /skin/visualizationBackground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/visualizationBackground.png -------------------------------------------------------------------------------- /skin/volumeHandle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/volumeHandle.png -------------------------------------------------------------------------------- /skin/volumeHandle_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rodmg/linamp/865479908ebfa1e75966507ce8359b51ab33f186/skin/volumeHandle_p.png -------------------------------------------------------------------------------- /src/audiosource-base/audiosource.cpp: -------------------------------------------------------------------------------- 1 | #include "audiosource.h" 2 | 3 | AudioSource::AudioSource(QObject *parent) 4 | : QObject{parent} 5 | { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/audiosource-base/audiosource.h: -------------------------------------------------------------------------------- 1 | #ifndef AUDIOSOURCE_H 2 | #define AUDIOSOURCE_H 3 | 4 | #include 5 | #include "mediaplayer.h" 6 | 7 | 8 | class AudioSource : public QObject 9 | { 10 | Q_OBJECT 11 | public: 12 | explicit AudioSource(QObject *parent = nullptr); 13 | 14 | signals: 15 | void playbackStateChanged(MediaPlayer::PlaybackState state); 16 | void positionChanged(qint64 progress); 17 | void dataEmitted(const QByteArray& data, QAudioFormat format); 18 | void metadataChanged(QMediaMetaData metadata); 19 | void durationChanged(qint64 duration); 20 | void eqEnabledChanged(bool enabled); 21 | void plEnabledChanged(bool enabled); 22 | void shuffleEnabledChanged(bool enabled); 23 | void repeatEnabledChanged(bool enabled); 24 | void messageSet(QString message, qint64 timeout); 25 | void messageClear(); 26 | void requestActivation(); // Asks the coordinator to be selected, coordinator can ignore this 27 | 28 | public slots: 29 | virtual void activate() = 0; 30 | virtual void deactivate() = 0; 31 | virtual void handlePl() = 0; 32 | virtual void handlePrevious() = 0; 33 | virtual void handlePlay() = 0; 34 | virtual void handlePause() = 0; 35 | virtual void handleStop() = 0; 36 | virtual void handleNext() = 0; 37 | virtual void handleOpen() = 0; 38 | virtual void handleShuffle() = 0; 39 | virtual void handleRepeat() = 0; 40 | virtual void handleSeek(int mseconds) = 0; 41 | 42 | }; 43 | 44 | #endif // AUDIOSOURCE_H 45 | -------------------------------------------------------------------------------- /src/audiosource-base/audiosourcewspectrumcapture.h: -------------------------------------------------------------------------------- 1 | #ifndef AUDIOSOURCEWSPECTRUMCAPTURE_H 2 | #define AUDIOSOURCEWSPECTRUMCAPTURE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "audiosource.h" 12 | 13 | struct PwData { 14 | struct pw_main_loop *loop; 15 | struct pw_stream *stream; 16 | 17 | struct spa_audio_info format; 18 | unsigned move:1; 19 | 20 | QMutex *sampleMutex; 21 | QByteArray *sample; 22 | QDataStream *sampleStream; 23 | }; 24 | 25 | class AudioSourceWSpectrumCapture : public AudioSource 26 | { 27 | Q_OBJECT 28 | public: 29 | explicit AudioSourceWSpectrumCapture(QObject *parent = nullptr); 30 | ~AudioSourceWSpectrumCapture(); 31 | 32 | void startSpectrum(); 33 | void stopSpectrum(); 34 | 35 | private: 36 | bool spectrumRunning = false; 37 | QTimer *dataEmitTimer = nullptr; 38 | void emitData(); 39 | 40 | QAudioFormat spectrumDataFormat; 41 | 42 | struct PwData pwData; 43 | void pwLoop(); 44 | 45 | QFuture pwLoopThread; 46 | 47 | signals: 48 | 49 | }; 50 | 51 | #endif // AUDIOSOURCEWSPECTRUMCAPTURE_H 52 | -------------------------------------------------------------------------------- /src/audiosource-coordinator/audiosourcecoordinator.h: -------------------------------------------------------------------------------- 1 | #ifndef AUDIOSOURCECOORDINATOR_H 2 | #define AUDIOSOURCECOORDINATOR_H 3 | 4 | #include 5 | #include "audiosource.h" 6 | #include "systemaudiocontrol.h" 7 | #include "playerview.h" 8 | 9 | class AudioSourceCoordinator : public QObject 10 | { 11 | Q_OBJECT 12 | public: 13 | explicit AudioSourceCoordinator(QObject *parent = nullptr, PlayerView *playerView = nullptr); 14 | 15 | void addSource(AudioSource *source, QString label, bool activate = false); 16 | 17 | signals: 18 | void sourceChanged(int source); 19 | 20 | public slots: 21 | void setSource(int source); 22 | void setVolume(int volume); 23 | void setBalance(int balance); 24 | 25 | private: 26 | QList sources; 27 | QList sourceLabels; 28 | int currentSource = -1; 29 | 30 | SystemAudioControl *system_audio = nullptr; 31 | PlayerView *view = nullptr; 32 | }; 33 | 34 | #endif // AUDIOSOURCECOORDINATOR_H 35 | -------------------------------------------------------------------------------- /src/audiosourcecd/audiosourcecd.h: -------------------------------------------------------------------------------- 1 | #ifndef AUDIOSOURCECD_H 2 | #define AUDIOSOURCECD_H 3 | 4 | #define PY_SSIZE_T_CLEAN 5 | #undef slots 6 | #include 7 | #define slots Q_SLOTS 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include "audiosourcewspectrumcapture.h" 14 | 15 | class AudioSourceCD : public AudioSourceWSpectrumCapture 16 | { 17 | Q_OBJECT 18 | public: 19 | explicit AudioSourceCD(QObject *parent = nullptr); 20 | ~AudioSourceCD(); 21 | 22 | public slots: 23 | void activate(); 24 | void deactivate(); 25 | void handlePl(); 26 | void handlePrevious(); 27 | void handlePlay(); 28 | void handlePause(); 29 | void handleStop(); 30 | void handleNext(); 31 | void handleOpen(); 32 | void handleShuffle(); 33 | void handleRepeat(); 34 | void handleSeek(int mseconds); 35 | 36 | private: 37 | bool isActive = false; 38 | 39 | PyObject *cdplayerModule; 40 | PyObject *cdplayer; 41 | 42 | QTimer *progressRefreshTimer = nullptr; 43 | QTimer *progressInterpolateTimer = nullptr; 44 | QElapsedTimer progressInterpolateElapsedTimer; 45 | quint32 currentProgress = 0; 46 | void refreshProgress(); 47 | void interpolateProgress(); 48 | 49 | // Detect disc insertion thread 50 | QTimer *detectDiscInsertionTimer = nullptr; 51 | bool pollInProgress = false; 52 | void pollDetectDiscInsertion(); 53 | bool doPollDetectDiscInsertion(); 54 | void handlePollResult(); 55 | QFutureWatcher pollResultWatcher; 56 | 57 | // Load disc thread 58 | void doLoad(); 59 | void handleLoadEnd(); 60 | QFutureWatcher loadWatcher; 61 | 62 | // Eject thread 63 | void doEject(); 64 | void handleEjectEnd(); 65 | QFutureWatcher ejectWatcher; 66 | 67 | QString currentStatus; // Status as it comes from python 68 | bool isShuffleEnabled = false; 69 | bool isRepeatEnabled = false; 70 | 71 | void refreshStatus(bool shouldRefreshTrackInfo = true); 72 | void refreshTrackInfo(bool force = false); 73 | 74 | quint32 currentTrackNumber = std::numeric_limits::max(); 75 | }; 76 | 77 | #endif // AUDIOSOURCECD_H 78 | -------------------------------------------------------------------------------- /src/audiosourcefile/audiosourcefile.h: -------------------------------------------------------------------------------- 1 | #ifndef AUDIOSOURCEFILE_H 2 | #define AUDIOSOURCEFILE_H 3 | 4 | #include 5 | #include 6 | 7 | #include "audiosource.h" 8 | #include "qmediaplaylist.h" 9 | #include "playlistmodel.h" 10 | #include "mediaplayer.h" 11 | 12 | class AudioSourceFile : public AudioSource 13 | { 14 | Q_OBJECT 15 | public: 16 | explicit AudioSourceFile(QObject *parent = nullptr, PlaylistModel *playlistModel = nullptr); 17 | 18 | signals: 19 | void showPlaylistRequested(); 20 | 21 | public slots: 22 | void activate(); 23 | void deactivate(); 24 | void handlePl(); 25 | void handlePrevious(); 26 | void handlePlay(); 27 | void handlePause(); 28 | void handleStop(); 29 | void handleNext(); 30 | void handleOpen(); 31 | void handleShuffle(); 32 | void handleRepeat(); 33 | void handleSeek(int mseconds); 34 | 35 | void jump(const QModelIndex &index); 36 | 37 | void addToPlaylist(const QList &urls); 38 | 39 | private slots: 40 | void handleMetaDataChanged(); 41 | void handleMediaStatusChanged(MediaPlayer::MediaStatus status); 42 | void handleBufferingProgress(float progress); 43 | void handleMediaError(); 44 | void handlePlaylistPositionChanged(int); 45 | void handlePlaylistMediaRemoved(int, int); 46 | void handleSpectrumData(const QByteArray& data); 47 | 48 | 49 | private: 50 | MediaPlayer *m_player = nullptr; 51 | QMediaPlaylist *m_playlist = nullptr; 52 | PlaylistModel *m_playlistModel = nullptr; 53 | 54 | QString m_statusInfo; 55 | 56 | bool shuffleEnabled = false; 57 | bool repeatEnabled = false; 58 | bool shouldBePlaying = false; 59 | 60 | void setStatusInfo(const QString &info); 61 | 62 | }; 63 | 64 | #endif // AUDIOSOURCEFILE_H 65 | -------------------------------------------------------------------------------- /src/audiosourcefile/mediaplayer.h: -------------------------------------------------------------------------------- 1 | // Based on https://stackoverflow.com/questions/41197576/how-to-play-mp3-file-using-qaudiooutput-and-qaudiodecoder 2 | #ifndef MEDIAPLAYER_H 3 | #define MEDIAPLAYER_H 4 | 5 | #include "qmediametadata.h" 6 | #include "qurl.h" 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | // Class for decode audio files like MP3 and push decoded audio data to QOutputDevice (like speaker) and also signal newData(). 16 | // For decoding it uses QAudioDecoder which uses QAudioFormat for decode audio file for desire format, then put decoded data to buffer. 17 | // based on: https://github.com/Znurre/QtMixer 18 | class MediaPlayer : public QIODevice 19 | { 20 | Q_OBJECT 21 | 22 | public: 23 | MediaPlayer(QObject *parent = nullptr); 24 | 25 | enum PlaybackState { 26 | StoppedState, 27 | PlayingState, 28 | PausedState 29 | }; 30 | enum MediaStatus { 31 | NoMedia, 32 | LoadingMedia, 33 | LoadedMedia, 34 | StalledMedia, 35 | BufferingMedia, 36 | BufferedMedia, 37 | EndOfMedia, 38 | InvalidMedia 39 | }; 40 | enum Error { 41 | NoError, 42 | ResourceError, 43 | FormatError, 44 | NetworkError, 45 | AccessDeniedError 46 | }; 47 | 48 | void play(); 49 | void pause(); 50 | void stop(bool stopAudioOutput = true); 51 | 52 | PlaybackState playbackState() const; 53 | 54 | qint64 duration() const; 55 | qint64 position() const; 56 | float bufferProgress() const; 57 | MediaStatus mediaStatus() const; 58 | float volume() const; 59 | QMediaMetaData metaData() const; 60 | Error error() const; 61 | QString errorString() const; 62 | QAudioFormat format(); 63 | 64 | protected: 65 | qint64 readData(char* data, qint64 maxlen) override; 66 | qint64 writeData(const char* data, qint64 len) override; 67 | 68 | private: 69 | QBuffer m_input; 70 | QBuffer m_output; 71 | QByteArray m_data; 72 | QAudioFormat m_format; 73 | QAudioDecoder *m_decoder = nullptr; 74 | QAudioSink *m_audioOutput = nullptr; 75 | QUrl m_source; 76 | QMediaMetaData m_metaData = QMediaMetaData{}; 77 | QMutex initMutex; 78 | QMutex readMutex; 79 | 80 | Error m_error = NoError; 81 | MediaStatus m_status = MediaStatus::NoMedia; 82 | PlaybackState m_state = PlaybackState::StoppedState; 83 | 84 | bool isInited; 85 | bool isDecodingFinished; 86 | qint8 bufferUnderrunRetries = 0; 87 | 88 | bool m_seekable = false; 89 | qint64 m_position = 0; 90 | float m_volume = 1.0; // range: 0.0 - 1.0 91 | 92 | bool init(const QAudioFormat& format); 93 | void setupDecoder(); 94 | void setupAudioOutput(); 95 | void clearDecoder(); 96 | void clearAudioOutput(); 97 | void clear(); 98 | bool atEnd() const override; 99 | void loadMetaData(); 100 | void setError(Error error); 101 | void setMediaStatus(MediaStatus status); 102 | 103 | public slots: 104 | void setSource(const QUrl &source); 105 | void clearSource(); 106 | void setPosition(qint64 position); 107 | void setVolume(float volume); 108 | 109 | private slots: 110 | void bufferReady(); 111 | void finished(); 112 | void onPositionChanged(); 113 | void onDurationChanged(qint64 duration); 114 | void onDecoderError(QAudioDecoder::Error error); 115 | void onAtEnd(); 116 | void onOutputStateChanged(QAudio::State newState); 117 | 118 | signals: 119 | void playbackStateChanged(MediaPlayer::PlaybackState state); 120 | void mediaStatusChanged(MediaPlayer::MediaStatus status); 121 | void newData(const QByteArray& data); 122 | void durationChanged(qint64 duration); 123 | void positionChanged(qint64 position); 124 | void bufferProgressChanged(float progress); 125 | void volumeChanged(float volume); 126 | void metaDataChanged(); 127 | void errorChanged(); 128 | }; 129 | 130 | #endif // MEDIAPLAYER_H 131 | -------------------------------------------------------------------------------- /src/audiosourcepython/audiosourcepython.h: -------------------------------------------------------------------------------- 1 | #ifndef AUDIOSOURCEPYTHON_H 2 | #define AUDIOSOURCEPYTHON_H 3 | 4 | #define PY_SSIZE_T_CLEAN 5 | #undef slots 6 | #include 7 | #define slots Q_SLOTS 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include "audiosourcewspectrumcapture.h" 14 | 15 | class AudioSourcePython : public AudioSourceWSpectrumCapture 16 | { 17 | Q_OBJECT 18 | public: 19 | explicit AudioSourcePython(QString module, QString className, QObject *parent = nullptr); 20 | ~AudioSourcePython(); 21 | 22 | public slots: 23 | void activate(); 24 | void deactivate(); 25 | void handlePl(); 26 | void handlePrevious(); 27 | void handlePlay(); 28 | void handlePause(); 29 | void handleStop(); 30 | void handleNext(); 31 | void handleOpen(); 32 | void handleShuffle(); 33 | void handleRepeat(); 34 | void handleSeek(int mseconds); 35 | 36 | private: 37 | bool isActive = false; 38 | 39 | PyObject *playerModule; 40 | PyObject *player; 41 | 42 | QTimer *progressRefreshTimer = nullptr; 43 | QTimer *progressInterpolateTimer = nullptr; 44 | QElapsedTimer progressInterpolateElapsedTimer; 45 | quint32 currentProgress = 0; 46 | void refreshProgress(); 47 | void interpolateProgress(); 48 | 49 | // Python event loop thread 50 | void runPythonLoop(); 51 | QFutureWatcher pyLoopWatcher; 52 | 53 | // Poll events thread 54 | QTimer *pollEventsTimer = nullptr; 55 | bool pollInProgress = false; 56 | void pollEvents(); 57 | bool doPollEvents(); 58 | void handlePollResult(); 59 | QFutureWatcher pollResultWatcher; 60 | 61 | // Load details thread 62 | void doLoad(); 63 | void handleLoadEnd(); 64 | QFutureWatcher loadWatcher; 65 | 66 | // Eject thread 67 | void doEject(); 68 | void handleEjectEnd(); 69 | QFutureWatcher ejectWatcher; 70 | 71 | QString currentStatus; // Status as it comes from python 72 | bool isShuffleEnabled = false; 73 | bool isRepeatEnabled = false; 74 | 75 | void refreshStatus(bool shouldRefreshTrackInfo = true); 76 | void refreshTrackInfo(bool force = false); 77 | void refreshMessage(); 78 | 79 | QMediaMetaData currentMetadata; 80 | }; 81 | 82 | #endif // AUDIOSOURCEPYTHON_H 83 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2017 The Qt Company Ltd. 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause 3 | // Copyright (C) 2023 Rodrigo Mendez. 4 | 5 | #include "mainwindow.h" 6 | #include "scale.h" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #define APP_VERSION_STR "1.0.1" 15 | 16 | int main(int argc, char *argv[]) 17 | { 18 | QApplication app(argc, argv); 19 | 20 | QCoreApplication::setApplicationName("Linamp"); 21 | QCoreApplication::setOrganizationName("Rod"); 22 | QCoreApplication::setApplicationVersion(APP_VERSION_STR); 23 | QCommandLineParser parser; 24 | parser.setApplicationDescription("Linamp"); 25 | parser.addHelpOption(); 26 | parser.addVersionOption(); 27 | parser.addPositionalArgument("url", "The URL(s) to open."); 28 | parser.process(app); 29 | 30 | MainWindow window; 31 | if (!parser.positionalArguments().isEmpty()) { 32 | QList urls; 33 | for (auto &a : parser.positionalArguments()) 34 | urls.append(QUrl::fromUserInput(a, QDir::currentPath())); 35 | //window.player->addToPlaylist(urls); 36 | } 37 | 38 | #ifdef IS_EMBEDDED 39 | window.setWindowState(Qt::WindowFullScreen); 40 | #endif 41 | window.show(); 42 | 43 | return app.exec(); 44 | } 45 | -------------------------------------------------------------------------------- /src/shared/fft.cpp: -------------------------------------------------------------------------------- 1 | // Based on fft.cc from audacious project 2 | /* 3 | * fft.c 4 | * Copyright 2011 John Lindgren 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions are met: 8 | * 9 | * 1. Redistributions of source code must retain the above copyright notice, 10 | * this list of conditions, and the following disclaimer. 11 | * 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions, and the following disclaimer in the documentation 14 | * provided with the distribution. 15 | * 16 | * This software is provided "as is" and without any warranty, express or 17 | * implied. In no event shall the authors be liable for any damages arising from 18 | * the use of this software. 19 | */ 20 | 21 | #include "fft.h" 22 | #include 23 | #include 24 | 25 | #define TWO_PI 6.2831853f 26 | 27 | #define LOGN 9 /* log N (base 2) */ 28 | 29 | typedef std::complex Complex; 30 | 31 | static float hamming[N]; /* hamming window, scaled to sum to 1 */ 32 | static int reversed[N]; /* bit-reversal table */ 33 | static Complex roots[N / 2]; /* N-th roots of unity */ 34 | static char generated = 0; /* set if tables have been generated */ 35 | 36 | /* Reverse the order of the lowest LOGN bits in an integer. */ 37 | 38 | static int bit_reverse(int x) 39 | { 40 | int y = 0; 41 | 42 | for (int n = LOGN; n--;) 43 | { 44 | y = (y << 1) | (x & 1); 45 | x >>= 1; 46 | } 47 | 48 | return y; 49 | } 50 | 51 | /* Generate lookup tables. */ 52 | 53 | static void generate_tables() 54 | { 55 | if (generated) 56 | return; 57 | 58 | for (int n = 0; n < N; n++) 59 | hamming[n] = 1 - 0.85f * cosf(n * (TWO_PI / N)); 60 | for (int n = 0; n < N; n++) 61 | reversed[n] = bit_reverse(n); 62 | for (int n = 0; n < N / 2; n++) 63 | roots[n] = exp(Complex(0, n * (TWO_PI / N))); 64 | 65 | generated = 1; 66 | } 67 | 68 | /* Perform the DFT using the Cooley-Tukey algorithm. At each step s, where 69 | * s=1..log N (base 2), there are N/(2^s) groups of intertwined butterfly 70 | * operations. Each group contains (2^s)/2 butterflies, and each butterfly has 71 | * a span of (2^s)/2. The twiddle factors are nth roots of unity where n = 2^s. 72 | */ 73 | 74 | static void do_fft(Complex a[N]) 75 | { 76 | int half = 1; /* (2^s)/2 */ 77 | int inv = N / 2; /* N/(2^s) */ 78 | 79 | /* loop through steps */ 80 | while (inv) 81 | { 82 | /* loop through groups */ 83 | for (int g = 0; g < N; g += half << 1) 84 | { 85 | /* loop through butterflies */ 86 | for (int b = 0, r = 0; b < half; b++, r += inv) 87 | { 88 | Complex even = a[g + b]; 89 | Complex odd = roots[r] * a[g + half + b]; 90 | a[g + b] = even + odd; 91 | a[g + half + b] = even - odd; 92 | } 93 | } 94 | 95 | half <<= 1; 96 | inv >>= 1; 97 | } 98 | } 99 | 100 | /* Input is N=512 PCM samples. 101 | * Output is intensity of frequencies from 1 to N/2=256. */ 102 | 103 | void calc_freq(const float data[N], float freq[N / 2]) 104 | { 105 | generate_tables(); 106 | 107 | /* input is filtered by a Hamming window */ 108 | /* input values are in bit-reversed order */ 109 | Complex a[N]; 110 | for (int n = 0; n < N; n++) 111 | a[reversed[n]] = data[n] * hamming[n]; 112 | 113 | do_fft(a); 114 | 115 | /* output values are divided by N */ 116 | /* frequencies from 1 to N/2-1 are doubled */ 117 | for (int n = 0; n < N / 2 - 1; n++) 118 | freq[n] = 2 * abs(a[1 + n]) / N; 119 | 120 | /* frequency N/2 is not doubled */ 121 | freq[N / 2 - 1] = abs(a[N / 2]) / N; 122 | } 123 | -------------------------------------------------------------------------------- /src/shared/fft.h: -------------------------------------------------------------------------------- 1 | #ifndef FFT_H 2 | #define FFT_H 3 | 4 | // Based on fft.cc from audacious project 5 | /* 6 | * fft.c 7 | * Copyright 2011 John Lindgren 8 | * 9 | * Redistribution and use in source and binary forms, with or without 10 | * modification, are permitted provided that the following conditions are met: 11 | * 12 | * 1. Redistributions of source code must retain the above copyright notice, 13 | * this list of conditions, and the following disclaimer. 14 | * 15 | * 2. Redistributions in binary form must reproduce the above copyright notice, 16 | * this list of conditions, and the following disclaimer in the documentation 17 | * provided with the distribution. 18 | * 19 | * This software is provided "as is" and without any warranty, express or 20 | * implied. In no event shall the authors be liable for any damages arising from 21 | * the use of this software. 22 | */ 23 | 24 | #define N 512 /* size of the DFT */ 25 | 26 | /* Input is N=512 PCM samples. 27 | * Output is intensity of frequencies from 1 to N/2=256. */ 28 | void calc_freq(const float data[N], float freq[N / 2]); 29 | 30 | #endif // FFT_H 31 | -------------------------------------------------------------------------------- /src/shared/linampslider.cpp: -------------------------------------------------------------------------------- 1 | #include "linampslider.h" 2 | #include 3 | #include 4 | 5 | LinampSlider::LinampSlider(QWidget* parent) : 6 | QSlider(parent), 7 | gradient(QSize(100,100), QImage::Format_RGB32) 8 | { 9 | } 10 | 11 | void LinampSlider::setGradient(QColor from, QColor to, Qt::Orientation orientation) 12 | { 13 | setOrientation(orientation); 14 | // create linear gradient 15 | QLinearGradient linearGrad(QPointF(0, 0), (orientation==Qt::Horizontal) ? QPointF(100, 0) : QPointF(0, 100)); 16 | linearGrad.setColorAt(0, from); 17 | linearGrad.setColorAt(1, to); 18 | 19 | // paint gradient in a QImage: 20 | QPainter p(&gradient); 21 | p.fillRect(gradient.rect(), linearGrad); 22 | 23 | connect(this, SIGNAL(valueChanged(int)), this, SLOT(changeColor(int))); 24 | 25 | // initialize 26 | changeColor(value()); 27 | } 28 | 29 | void LinampSlider::setGradient(QList steps, Qt::Orientation orientation) 30 | { 31 | setOrientation(orientation); 32 | // create linear gradient 33 | QLinearGradient linearGrad(QPointF(0, 0), (orientation==Qt::Horizontal) ? QPointF(100, 0) : QPointF(0, 100)); 34 | for(int i = 0; i < steps.count(); i++) { 35 | linearGrad.setColorAt(float(i)/float(steps.count() - 1), steps[i]); 36 | } 37 | 38 | // paint gradient in a QImage: 39 | QPainter p(&gradient); 40 | p.fillRect(gradient.rect(), linearGrad); 41 | 42 | connect(this, SIGNAL(valueChanged(int)), this, SLOT(changeColor(int))); 43 | 44 | // initialize 45 | changeColor(value()); 46 | } 47 | 48 | void LinampSlider::changeColor(int pos) 49 | { 50 | QColor color; 51 | 52 | if (orientation() == Qt::Horizontal) 53 | { 54 | // retrieve color index based on cursor position 55 | int posIndex = gradient.size().width() * (pos - minimum()) / (maximum() - minimum()); 56 | posIndex = std::min(posIndex, gradient.width() - 1); 57 | 58 | // pickup appropriate color 59 | color = gradient.pixel(posIndex, gradient.size().height()/2); 60 | } 61 | else 62 | { 63 | // retrieve color index based on cursor position 64 | int posIndex = gradient.size().height() * (pos - minimum()) / (maximum() - minimum()); 65 | posIndex = std::min(posIndex, gradient.height() - 1); 66 | 67 | // pickup appropriate color 68 | color = gradient.pixel(gradient.size().width()/2, posIndex); 69 | } 70 | 71 | // create and apply stylesheet! 72 | // can be customized to change background and handle border! 73 | 74 | if(baseStylesheet.length() == 0) { 75 | // Capture original unmodified stylesheet the first time 76 | baseStylesheet = styleSheet(); 77 | } 78 | 79 | // Append background color 80 | QString stylesheet = baseStylesheet + 81 | "QSlider::sub-page:" + ((orientation() == Qt::Horizontal) ? QString("horizontal"):QString("vertical")) + "{ \ 82 | background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1, stop: 0 " + color.darker(130).name() + ", stop: 1 " + color.name() + "); \ 83 | }" + 84 | "QSlider::add-page:" + ((orientation() == Qt::Horizontal) ? QString("horizontal"):QString("vertical")) + "{ \ 85 | background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1, stop: 0 " + color.darker(130).name() + ", stop: 1 " + color.name() + "); \ 86 | }"; 87 | 88 | setStyleSheet(stylesheet); 89 | } 90 | -------------------------------------------------------------------------------- /src/shared/linampslider.h: -------------------------------------------------------------------------------- 1 | #ifndef LINAMPSLIDER_H 2 | #define LINAMPSLIDER_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class LinampSlider : public QSlider 9 | { 10 | Q_OBJECT 11 | public: 12 | LinampSlider(QWidget* parent); 13 | 14 | void setGradient(QColor from, QColor to, Qt::Orientation orientation); 15 | 16 | void setGradient(QList steps, Qt::Orientation orientation); 17 | 18 | private slots: 19 | void changeColor(int); 20 | 21 | private: 22 | QImage gradient; 23 | QString baseStylesheet; 24 | }; 25 | 26 | #endif // LINAMPSLIDER_H 27 | -------------------------------------------------------------------------------- /src/shared/scale.cpp: -------------------------------------------------------------------------------- 1 | #include "scale.h" 2 | 3 | #include 4 | 5 | QString getStylesheet(QString name) 6 | { 7 | QFile file(":/styles/" + name + "." + QString::number(UI_SCALE) + "x.qss"); 8 | file.open(QFile::ReadOnly); 9 | QString styleSheet = QLatin1String(file.readAll()); 10 | return styleSheet; 11 | } 12 | -------------------------------------------------------------------------------- /src/shared/scale.h: -------------------------------------------------------------------------------- 1 | #ifndef SCALE_H 2 | #define SCALE_H 3 | 4 | #define UI_SCALE 4 5 | #define IS_EMBEDDED 6 | 7 | #include 8 | 9 | QString getStylesheet(QString name); 10 | 11 | #endif // SCALE_H 12 | -------------------------------------------------------------------------------- /src/shared/systemaudiocontrol.cpp: -------------------------------------------------------------------------------- 1 | #include "systemaudiocontrol.h" 2 | #include 3 | 4 | SystemAudioControl::SystemAudioControl(QObject *parent) 5 | : QObject{parent} { 6 | init(); 7 | loadAlsaSettings(); 8 | 9 | // Start periodically polling for updates on volume settings 10 | pollTimer = new QTimer(this); 11 | pollTimer->setInterval(10000); 12 | connect(pollTimer, &QTimer::timeout, this, QOverload<>::of(&SystemAudioControl::loadAlsaSettings)); 13 | pollTimer->start(); 14 | } 15 | 16 | SystemAudioControl::~SystemAudioControl() { 17 | snd_mixer_close(handle); 18 | } 19 | 20 | void SystemAudioControl::init() { 21 | // Open the mixer 22 | if (snd_mixer_open(&handle, 0) < 0) { 23 | qDebug() << "Cannot open mixer"; 24 | return; 25 | } 26 | 27 | // Attach the mixer 28 | if (snd_mixer_attach(handle, "default") < 0) { 29 | qDebug() << "Cannot attach mixer"; 30 | snd_mixer_close(handle); 31 | return; 32 | } 33 | 34 | // Register the mixer 35 | if (snd_mixer_selem_register(handle, NULL, NULL) < 0) { 36 | qDebug() << "Cannot register mixer"; 37 | snd_mixer_close(handle); 38 | return; 39 | } 40 | 41 | // Load the mixer 42 | if (snd_mixer_load(handle) < 0) { 43 | qDebug() << "Cannot load mixer"; 44 | snd_mixer_close(handle); 45 | return; 46 | } 47 | 48 | // Set the element ID 49 | snd_mixer_selem_id_alloca(&sid); 50 | snd_mixer_selem_id_set_index(sid, 0); 51 | snd_mixer_selem_id_set_name(sid, "Master"); 52 | 53 | // Get the mixer element 54 | elem = snd_mixer_find_selem(handle, sid); 55 | if (!elem) { 56 | qDebug() << "Cannot find mixer element"; 57 | snd_mixer_close(handle); 58 | return; 59 | } 60 | 61 | initSuccess = true; 62 | } 63 | 64 | void SystemAudioControl::setVolume(int volume) { 65 | this->volume = volume; 66 | applyAlsaSettings(); 67 | } 68 | 69 | void SystemAudioControl::setBalance(int balance) { 70 | this->balance = balance; 71 | applyAlsaSettings(); 72 | } 73 | 74 | int SystemAudioControl::getVolume() { 75 | return this->volume; 76 | } 77 | 78 | int SystemAudioControl::getBalance() { 79 | return this->balance; 80 | } 81 | 82 | void SystemAudioControl::applyAlsaSettings() { 83 | if(!initSuccess) return; 84 | 85 | long min, max; 86 | 87 | // Get the volume range 88 | snd_mixer_selem_get_playback_volume_range(elem, &min, &max); 89 | 90 | long targetVolume = volume * (max - min) / 100 + min; 91 | 92 | // Calculate the left and right volumes based on the balance 93 | long leftVolume = (balance <= 0) ? targetVolume : targetVolume - (balance * targetVolume) / 100; 94 | long rightVolume = (balance >= 0) ? targetVolume : targetVolume + (balance * targetVolume) / 100; 95 | 96 | // Set the left and right volumes 97 | snd_mixer_selem_set_playback_volume(elem, SND_MIXER_SCHN_FRONT_LEFT, leftVolume); 98 | snd_mixer_selem_set_playback_volume(elem, SND_MIXER_SCHN_FRONT_RIGHT, rightVolume); 99 | } 100 | 101 | void SystemAudioControl::loadAlsaSettings() { 102 | 103 | // Need to re-init in order to get fresh values 104 | // Otherwise we get the same values always 105 | if(initSuccess) { 106 | snd_mixer_close(handle); 107 | } 108 | init(); 109 | 110 | if(!initSuccess) return; 111 | 112 | long min, max, leftVolume, rightVolume; 113 | 114 | 115 | // Get the volume range 116 | snd_mixer_selem_get_playback_volume_range(elem, &min, &max); 117 | 118 | // Get the left and right volumes 119 | snd_mixer_selem_get_playback_volume(elem, SND_MIXER_SCHN_FRONT_LEFT, &leftVolume); 120 | snd_mixer_selem_get_playback_volume(elem, SND_MIXER_SCHN_FRONT_RIGHT, &rightVolume); 121 | 122 | //qDebug() << "min: " << min << "max: " << max; 123 | //qDebug() << "leftVolume: " << leftVolume << "rightVolume: " << rightVolume; 124 | 125 | // Scale to 0-100 scale 126 | leftVolume = leftVolume / ((max - min) / 100 + min); 127 | rightVolume = rightVolume / ((max - min) / 100 + min); 128 | 129 | //qDebug() << "scaled leftVolume: " << leftVolume << "scaled rightVolume: " << rightVolume; 130 | 131 | // Derive target volume 132 | int oldVolume = volume; 133 | volume = leftVolume >= rightVolume ? leftVolume : rightVolume; 134 | 135 | // Derive balance 136 | int oldBalance = balance; 137 | balance = (rightVolume - leftVolume) * (100.0 / volume); 138 | 139 | if(oldVolume != volume) { 140 | emit volumeChanged(volume); 141 | } 142 | 143 | if(oldBalance != balance) { 144 | emit balanceChanged(balance); 145 | } 146 | 147 | //qDebug() << "Volume: " << volume << "Balance: " << balance; 148 | } 149 | -------------------------------------------------------------------------------- /src/shared/systemaudiocontrol.h: -------------------------------------------------------------------------------- 1 | #ifndef SYSTEMAUDIOCONTROL_H 2 | #define SYSTEMAUDIOCONTROL_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class SystemAudioControl: public QObject { 9 | Q_OBJECT 10 | public: 11 | explicit SystemAudioControl(QObject *parent = nullptr); 12 | ~SystemAudioControl(); 13 | 14 | void setVolume(int volume); 15 | void setBalance(int balance); 16 | 17 | int getVolume(); 18 | int getBalance(); 19 | 20 | signals: 21 | void volumeChanged(int volume); 22 | void balanceChanged(int balance); 23 | 24 | private: 25 | snd_mixer_t *handle; 26 | snd_mixer_selem_id_t *sid; 27 | snd_mixer_elem_t *elem; 28 | 29 | QTimer *pollTimer = nullptr; 30 | 31 | bool initSuccess = false; 32 | 33 | int volume = 100; // 0 to 100 34 | int balance = 0; // -100 to 100 35 | 36 | void init(); 37 | void applyAlsaSettings(); 38 | void loadAlsaSettings(); 39 | 40 | }; 41 | 42 | #endif // SYSTEMAUDIOCONTROL_H 43 | -------------------------------------------------------------------------------- /src/shared/util.cpp: -------------------------------------------------------------------------------- 1 | #include "util.h" 2 | #include "qdatetime.h" 3 | #include "qfileinfo.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | QMediaMetaData parseMetaData(const QUrl &url) 11 | { 12 | QMediaMetaData metadata; 13 | TagLib::FileRef f(url.toLocalFile().toLocal8Bit().data()); 14 | 15 | if(!f.isNull() && f.tag()) { 16 | TagLib::Tag *tag = f.tag(); 17 | 18 | QString title = QString::fromStdString(tag->title().toCString(true)); 19 | 20 | // If title is not set, use the file name 21 | if(!title.length()) { 22 | title = url.fileName(); 23 | } 24 | 25 | QString albumTitle = QString::fromStdString(tag->album().toCString(true)); 26 | QString artist = QString::fromStdString(tag->artist().toCString(true)); 27 | QString comment = QString::fromStdString(tag->comment().toCString(true)); 28 | QString genre = QString::fromStdString(tag->genre().toCString(true)); 29 | qint64 track = tag->track(); 30 | qint64 year = tag->year(); 31 | 32 | metadata = QMediaMetaData{}; 33 | metadata.insert(QMediaMetaData::Title, title); 34 | metadata.insert(QMediaMetaData::AlbumTitle, albumTitle); 35 | metadata.insert(QMediaMetaData::AlbumArtist, artist); 36 | metadata.insert(QMediaMetaData::Comment, comment); 37 | metadata.insert(QMediaMetaData::Genre, genre); 38 | metadata.insert(QMediaMetaData::TrackNumber, track); 39 | metadata.insert(QMediaMetaData::Url, url); 40 | metadata.insert(QMediaMetaData::Date, year); 41 | } 42 | 43 | if(!f.isNull() && f.audioProperties()) { 44 | TagLib::AudioProperties *properties = f.audioProperties(); 45 | 46 | qint64 duration = properties->lengthInMilliseconds(); 47 | qint64 bitrate = properties->bitrate() * 1000; 48 | qint64 sampleRate = properties->sampleRate(); 49 | 50 | metadata.insert(QMediaMetaData::AudioBitRate, bitrate); 51 | metadata.insert(QMediaMetaData::Comment, QString::number(sampleRate)); // Using Comment as sample rate 52 | metadata.insert(QMediaMetaData::Duration, duration); 53 | } 54 | 55 | return metadata; 56 | } 57 | 58 | QString formatDuration(qint64 ms) 59 | { 60 | // Calculate duration 61 | qint64 duration = ms/1000; 62 | QTime totalTime((duration / 3600) % 60, (duration / 60) % 60, duration % 60, 63 | (duration * 1000) % 1000); 64 | QString format = "mm:ss"; 65 | if (duration > 3600) 66 | format = "hh:mm:ss"; 67 | QString durationStr = totalTime.toString(format); 68 | return durationStr; 69 | } 70 | 71 | QStringList audioFileFilters() 72 | { 73 | QStringList filters; 74 | filters << "*.mp3" << "*.flac" << "*.m4a" << "*.ogg" << "*.wma" << "*.wav" << "*.m3u"; 75 | return filters; 76 | } 77 | 78 | bool isAudioFile(QString path) 79 | { 80 | QStringList filters = audioFileFilters(); 81 | 82 | for(const QString &filter : filters) { 83 | QRegularExpression rx = QRegularExpression::fromWildcard(filter, 84 | Qt::CaseInsensitive, 85 | QRegularExpression::UnanchoredWildcardConversion); 86 | if(rx.match(path).hasMatch()) { 87 | return true; 88 | } 89 | } 90 | 91 | return false; 92 | } 93 | 94 | bool isPlaylist(const QUrl &url) 95 | { 96 | if (!url.isLocalFile()) 97 | return false; 98 | const QFileInfo fileInfo(url.toLocalFile()); 99 | return fileInfo.exists() 100 | && !fileInfo.suffix().compare(QLatin1String("m3u"), Qt::CaseInsensitive); 101 | } 102 | -------------------------------------------------------------------------------- /src/shared/util.h: -------------------------------------------------------------------------------- 1 | #ifndef UTIL_H 2 | #define UTIL_H 3 | 4 | #include 5 | #include 6 | 7 | #define DEFAULT_SAMPLE_RATE 44100 8 | #define MAX_AUDIO_STREAM_SAMPLE_SIZE 4096 9 | 10 | QMediaMetaData parseMetaData(const QUrl &url); 11 | 12 | QString formatDuration(qint64 ms); 13 | 14 | QStringList audioFileFilters(); 15 | 16 | bool isAudioFile(QString path); 17 | 18 | bool isPlaylist(const QUrl &url); // Check for ".m3u" playlists. 19 | 20 | #endif // UTIL_H 21 | -------------------------------------------------------------------------------- /src/view-basewindow/desktopbasewindow.cpp: -------------------------------------------------------------------------------- 1 | #include "desktopbasewindow.h" 2 | #include "ui_desktopbasewindow.h" 3 | #include "scale.h" 4 | 5 | DesktopBaseWindow::DesktopBaseWindow(QWidget *parent) : 6 | QWidget(parent), 7 | ui(new Ui::DesktopBaseWindow) 8 | { 9 | ui->setupUi(this); 10 | scale(); 11 | } 12 | 13 | DesktopBaseWindow::~DesktopBaseWindow() 14 | { 15 | delete ui; 16 | } 17 | 18 | void DesktopBaseWindow::scale() 19 | { 20 | this->setBaseSize(this->baseSize() * UI_SCALE); 21 | this->layout()->setContentsMargins(this->layout()->contentsMargins() * UI_SCALE); 22 | 23 | ui->bodyContainer->layout()->setContentsMargins(ui->bodyContainer->layout()->contentsMargins() * UI_SCALE); 24 | ui->bodyOuterFrame->layout()->setContentsMargins(ui->bodyOuterFrame->layout()->contentsMargins() * UI_SCALE); 25 | 26 | ui->titlebarContainer->setMaximumHeight(ui->titlebarContainer->maximumHeight() * UI_SCALE); 27 | ui->titlebarContainer->setMinimumHeight(ui->titlebarContainer->minimumHeight() * UI_SCALE); 28 | 29 | this->setStyleSheet(getStylesheet("desktopbasewindow")); 30 | } 31 | -------------------------------------------------------------------------------- /src/view-basewindow/desktopbasewindow.h: -------------------------------------------------------------------------------- 1 | #ifndef DESKTOPBASEWINDOW_H 2 | #define DESKTOPBASEWINDOW_H 3 | 4 | #include 5 | 6 | namespace Ui { 7 | class DesktopBaseWindow; 8 | } 9 | 10 | class DesktopBaseWindow : public QWidget 11 | { 12 | Q_OBJECT 13 | 14 | public: 15 | explicit DesktopBaseWindow(QWidget *parent = nullptr); 16 | ~DesktopBaseWindow(); 17 | Ui::DesktopBaseWindow *ui; 18 | 19 | private: 20 | void scale(); 21 | }; 22 | 23 | #endif // DESKTOPBASEWINDOW_H 24 | -------------------------------------------------------------------------------- /src/view-basewindow/desktopplayerwindow.cpp: -------------------------------------------------------------------------------- 1 | #include "desktopplayerwindow.h" 2 | #include "ui_desktopplayerwindow.h" 3 | #include "scale.h" 4 | 5 | DesktopPlayerWindow::DesktopPlayerWindow(QWidget *parent) : 6 | QWidget(parent), 7 | ui(new Ui::DesktopPlayerWindow) 8 | { 9 | ui->setupUi(this); 10 | scale(); 11 | } 12 | 13 | DesktopPlayerWindow::~DesktopPlayerWindow() 14 | { 15 | delete ui; 16 | } 17 | 18 | void DesktopPlayerWindow::scale() 19 | { 20 | this->setBaseSize(this->baseSize() * UI_SCALE); 21 | this->setMinimumSize(this->minimumSize() * UI_SCALE); 22 | this->setMaximumSize(this->maximumSize() * UI_SCALE); 23 | this->layout()->setContentsMargins(this->layout()->contentsMargins() * UI_SCALE); 24 | 25 | ui->dpwBodyInnerFrame->layout()->setContentsMargins(ui->dpwBodyInnerFrame->layout()->contentsMargins() * UI_SCALE); 26 | 27 | QSize sh = ui->horizontalSpacer->sizeHint(); 28 | QSizePolicy sp = ui->horizontalSpacer->sizePolicy(); 29 | ui->horizontalSpacer->changeSize(sh.width()*UI_SCALE, sh.height(), sp.horizontalPolicy(), sp.verticalPolicy()); 30 | 31 | QSize vsh = ui->verticalSpacer_2->sizeHint(); 32 | QSizePolicy vsp = ui->verticalSpacer_2->sizePolicy(); 33 | ui->verticalSpacer_2->changeSize(vsh.width(), vsh.height()*UI_SCALE, vsp.horizontalPolicy(), vsp.verticalPolicy()); 34 | } 35 | -------------------------------------------------------------------------------- /src/view-basewindow/desktopplayerwindow.h: -------------------------------------------------------------------------------- 1 | #ifndef DESKTOPPLAYERWINDOW_H 2 | #define DESKTOPPLAYERWINDOW_H 3 | 4 | #include 5 | 6 | namespace Ui { 7 | class DesktopPlayerWindow; 8 | } 9 | 10 | class DesktopPlayerWindow : public QWidget 11 | { 12 | Q_OBJECT 13 | 14 | public: 15 | explicit DesktopPlayerWindow(QWidget *parent = nullptr); 16 | ~DesktopPlayerWindow(); 17 | Ui::DesktopPlayerWindow *ui; 18 | 19 | private: 20 | void scale(); 21 | }; 22 | 23 | #endif // DESKTOPPLAYERWINDOW_H 24 | -------------------------------------------------------------------------------- /src/view-basewindow/desktopplayerwindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | DesktopPlayerWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 277 10 | 128 11 | 12 | 13 | 14 | 15 | 243 16 | 86 17 | 18 | 19 | 20 | 21 | 320 22 | 128 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 51 32 | 51 33 | 80 34 | 35 | 36 | 37 | 38 | 39 | 40 | 51 41 | 51 42 | 80 43 | 44 | 45 | 46 | 47 | 48 | 49 | 51 50 | 51 51 | 80 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 51 61 | 51 62 | 80 63 | 64 | 65 | 66 | 67 | 68 | 69 | 51 70 | 51 71 | 80 72 | 73 | 74 | 75 | 76 | 77 | 78 | 51 79 | 51 80 | 80 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 51 90 | 51 91 | 80 92 | 93 | 94 | 95 | 96 | 97 | 98 | 51 99 | 51 100 | 80 101 | 102 | 103 | 104 | 105 | 106 | 107 | 51 108 | 51 109 | 80 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | Form 118 | 119 | 120 | #DesktopPlayerWindow { 121 | background-color: #333350; 122 | } 123 | 124 | 125 | 126 | 0 127 | 128 | 129 | 0 130 | 131 | 132 | 0 133 | 134 | 135 | 0 136 | 137 | 138 | 0 139 | 140 | 141 | 142 | 143 | #bodyInnerFrame { 144 | background-color: #333350; 145 | } 146 | 147 | 148 | QFrame::NoFrame 149 | 150 | 151 | QFrame::Plain 152 | 153 | 154 | 155 | 0 156 | 157 | 158 | 4 159 | 160 | 161 | 7 162 | 163 | 164 | 2 165 | 166 | 167 | 5 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | Qt::Vertical 176 | 177 | 178 | QSizePolicy::Fixed 179 | 180 | 181 | 182 | 20 183 | 5 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 0 192 | 193 | 194 | 195 | 196 | Qt::Horizontal 197 | 198 | 199 | QSizePolicy::Fixed 200 | 201 | 202 | 203 | 6 204 | 1 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | -------------------------------------------------------------------------------- /src/view-basewindow/embeddedbasewindow.cpp: -------------------------------------------------------------------------------- 1 | #include "embeddedbasewindow.h" 2 | #include "ui_embeddedbasewindow.h" 3 | 4 | EmbeddedBaseWindow::EmbeddedBaseWindow(QWidget *parent) : 5 | QWidget(parent), 6 | ui(new Ui::EmbeddedBaseWindow) 7 | { 8 | ui->setupUi(this); 9 | } 10 | 11 | EmbeddedBaseWindow::~EmbeddedBaseWindow() 12 | { 13 | delete ui; 14 | } 15 | -------------------------------------------------------------------------------- /src/view-basewindow/embeddedbasewindow.h: -------------------------------------------------------------------------------- 1 | #ifndef EMBEDDEDBASEWINDOW_H 2 | #define EMBEDDEDBASEWINDOW_H 3 | 4 | #include 5 | 6 | namespace Ui { 7 | class EmbeddedBaseWindow; 8 | } 9 | 10 | class EmbeddedBaseWindow : public QWidget 11 | { 12 | Q_OBJECT 13 | 14 | public: 15 | explicit EmbeddedBaseWindow(QWidget *parent = nullptr); 16 | ~EmbeddedBaseWindow(); 17 | Ui::EmbeddedBaseWindow *ui; 18 | }; 19 | 20 | #endif // EMBEDDEDBASEWINDOW_H 21 | -------------------------------------------------------------------------------- /src/view-basewindow/embeddedbasewindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | EmbeddedBaseWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1280 10 | 400 11 | 12 | 13 | 14 | 15 | 1280 16 | 400 17 | 18 | 19 | 20 | 21 | 16777215 22 | 16777215 23 | 24 | 25 | 26 | Form 27 | 28 | 29 | #EmbeddedBaseWindow { 30 | background-color: #333350; 31 | } 32 | 33 | 34 | 35 | 0 36 | 37 | 38 | 0 39 | 40 | 41 | 0 42 | 43 | 44 | 0 45 | 46 | 47 | 0 48 | 49 | 50 | 51 | 52 | 53 | 0 54 | 0 55 | 56 | 57 | 58 | 59 | 1116 60 | 0 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/view-basewindow/mainwindow.h: -------------------------------------------------------------------------------- 1 | #ifndef MAINWINDOW_H 2 | #define MAINWINDOW_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include "audiosourcecd.h" 8 | #include "audiosourcecoordinator.h" 9 | #include "audiosourcefile.h" 10 | #include "audiosourcepython.h" 11 | #include "controlbuttonswidget.h" 12 | #include "mainmenuview.h" 13 | #include "playerview.h" 14 | #include "playlistview.h" 15 | #include "qmediaplaylist.h" 16 | #include "playlistmodel.h" 17 | 18 | class MainWindow : public QMainWindow 19 | { 20 | Q_OBJECT 21 | public: 22 | explicit MainWindow(QWidget *parent = nullptr); 23 | ~MainWindow(); 24 | 25 | QStackedLayout *viewStack; 26 | 27 | PlayerView *player; 28 | ControlButtonsWidget *controlButtons; 29 | PlaylistView *playlist; 30 | MainMenuView *menu; 31 | AudioSourceCoordinator *coordinator; 32 | AudioSourceFile *fileSource; 33 | AudioSourcePython *btSource; 34 | AudioSourceCD *cdSource; 35 | AudioSourcePython *spotSource; 36 | 37 | public slots: 38 | void showPlayer(); 39 | void showPlaylist(); 40 | void showMenu(); 41 | void showShutdownModal(); 42 | void open(); 43 | 44 | private: 45 | QMediaPlaylist *m_playlist = nullptr; 46 | PlaylistModel *m_playlistModel = nullptr; 47 | QProcess *shutdownProcess = nullptr; 48 | void shutdown(); 49 | 50 | }; 51 | 52 | #endif // MAINWINDOW_H 53 | -------------------------------------------------------------------------------- /src/view-basewindow/titlebar.cpp: -------------------------------------------------------------------------------- 1 | #include "titlebar.h" 2 | #include "ui_titlebar.h" 3 | #include "scale.h" 4 | #include 5 | 6 | TitleBar::TitleBar(QWidget *parent) : 7 | QWidget(parent), 8 | ui(new Ui::TitleBar) 9 | { 10 | ui->setupUi(this); 11 | scale(); 12 | connect(ui->closeButton, &QPushButton::clicked, window(), &QWidget::close); 13 | connect(ui->minimizeButton, &QPushButton::clicked, window(), &QWidget::showMinimized); 14 | } 15 | 16 | TitleBar::~TitleBar() 17 | { 18 | delete ui; 19 | } 20 | 21 | 22 | void TitleBar::scale() 23 | { 24 | QRect geo = this->geometry(); 25 | geo.setHeight(geo.height() * UI_SCALE); 26 | this->setGeometry(geo); 27 | 28 | ui->closeButton->setMaximumSize(ui->closeButton->maximumSize() * UI_SCALE); 29 | ui->closeButton->setMinimumSize(ui->closeButton->minimumSize() * UI_SCALE); 30 | 31 | ui->minimizeButton->setMaximumSize(ui->minimizeButton->maximumSize() * UI_SCALE); 32 | ui->minimizeButton->setMinimumSize(ui->minimizeButton->minimumSize() * UI_SCALE); 33 | 34 | QFont titleFont = ui->windowTitle->font(); 35 | titleFont.setPointSize(titleFont.pointSize() * UI_SCALE); 36 | ui->windowTitle->setFont(titleFont); 37 | 38 | ui->decorationL->layout()->setContentsMargins(ui->decorationL->layout()->contentsMargins() * UI_SCALE); 39 | ui->decorationL->layout()->setSpacing(ui->decorationL->layout()->spacing() * UI_SCALE); 40 | 41 | ui->decorationR->layout()->setContentsMargins(ui->decorationR->layout()->contentsMargins() * UI_SCALE); 42 | ui->decorationR->layout()->setSpacing(ui->decorationR->layout()->spacing() * UI_SCALE); 43 | 44 | ui->lineLT->setMaximumHeight(ui->lineLT->maximumHeight() * UI_SCALE); 45 | ui->lineLT->setMinimumHeight(ui->lineLT->minimumHeight() * UI_SCALE); 46 | 47 | ui->lineLB->setMaximumHeight(ui->lineLB->maximumHeight() * UI_SCALE); 48 | ui->lineLB->setMinimumHeight(ui->lineLB->minimumHeight() * UI_SCALE); 49 | 50 | ui->lineRT->setMaximumHeight(ui->lineRT->maximumHeight() * UI_SCALE); 51 | ui->lineRT->setMinimumHeight(ui->lineRT->minimumHeight() * UI_SCALE); 52 | 53 | ui->lineRB->setMaximumHeight(ui->lineRB->maximumHeight() * UI_SCALE); 54 | ui->lineRB->setMinimumHeight(ui->lineRB->minimumHeight() * UI_SCALE); 55 | 56 | QSize sh = ui->horizontalSpacer->sizeHint(); 57 | QSizePolicy sp = ui->horizontalSpacer->sizePolicy(); 58 | ui->horizontalSpacer->changeSize(sh.width()*UI_SCALE, sh.height(), sp.horizontalPolicy(), sp.verticalPolicy()); 59 | 60 | QSize sh2 = ui->horizontalSpacer_2->sizeHint(); 61 | QSizePolicy sp2 = ui->horizontalSpacer_2->sizePolicy(); 62 | ui->horizontalSpacer_2->changeSize(sh2.width()*UI_SCALE, sh2.height(), sp2.horizontalPolicy(), sp2.verticalPolicy()); 63 | 64 | QSize sh4 = ui->horizontalSpacer_4->sizeHint(); 65 | QSizePolicy sp4 = ui->horizontalSpacer_4->sizePolicy(); 66 | ui->horizontalSpacer_4->changeSize(sh4.width()*UI_SCALE, sh4.height(), sp4.horizontalPolicy(), sp4.verticalPolicy()); 67 | } 68 | 69 | void TitleBar::mousePressEvent(QMouseEvent *event) 70 | { 71 | if(event->button() == Qt::LeftButton) 72 | { 73 | m_pCursor = event->globalPosition() - window()->geometry().topLeft(); 74 | event->accept(); 75 | } 76 | } 77 | 78 | void TitleBar::mouseMoveEvent(QMouseEvent *event) 79 | { 80 | if(event->buttons() & Qt::LeftButton) 81 | { 82 | window()->move((event->globalPosition() - m_pCursor).toPoint()); 83 | event->accept(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/view-basewindow/titlebar.h: -------------------------------------------------------------------------------- 1 | #ifndef TITLEBAR_H 2 | #define TITLEBAR_H 3 | 4 | #include 5 | 6 | namespace Ui { 7 | class TitleBar; 8 | } 9 | 10 | class TitleBar : public QWidget 11 | { 12 | Q_OBJECT 13 | 14 | public: 15 | explicit TitleBar(QWidget *parent = nullptr); 16 | ~TitleBar(); 17 | 18 | protected: 19 | void mousePressEvent(QMouseEvent *event); 20 | void mouseMoveEvent(QMouseEvent *event); 21 | 22 | private: 23 | Ui::TitleBar *ui; 24 | void scale(); 25 | QPointF m_pCursor; 26 | }; 27 | 28 | #endif // TITLEBAR_H 29 | -------------------------------------------------------------------------------- /src/view-menu/mainmenuview.cpp: -------------------------------------------------------------------------------- 1 | #include "mainmenuview.h" 2 | #include "ui_mainmenuview.h" 3 | 4 | MainMenuView::MainMenuView(QWidget *parent) : 5 | QWidget(parent), 6 | ui(new Ui::MainMenuView) 7 | { 8 | ui->setupUi(this); 9 | 10 | connect(ui->backButton, &QPushButton::clicked, this, &MainMenuView::backClicked); 11 | connect(ui->fileSourceButton, &QPushButton::clicked, this, &MainMenuView::fileSourceClicked); 12 | connect(ui->btSourceButton, &QPushButton::clicked, this, &MainMenuView::btSourceClicked); 13 | connect(ui->spotifySourceButton, &QPushButton::clicked, this, &MainMenuView::spotifySourceClicked); 14 | connect(ui->cdSourceButton, &QPushButton::clicked, this, &MainMenuView::cdSourceClicked); 15 | } 16 | 17 | MainMenuView::~MainMenuView() 18 | { 19 | delete ui; 20 | } 21 | 22 | void MainMenuView::fileSourceClicked() 23 | { 24 | emit sourceSelected(0); 25 | emit backClicked(); 26 | } 27 | 28 | void MainMenuView::btSourceClicked() 29 | { 30 | emit sourceSelected(1); 31 | emit backClicked(); 32 | } 33 | 34 | void MainMenuView::spotifySourceClicked() 35 | { 36 | emit sourceSelected(3); 37 | emit backClicked(); 38 | } 39 | 40 | void MainMenuView::cdSourceClicked() 41 | { 42 | emit sourceSelected(2); 43 | emit backClicked(); 44 | } 45 | 46 | void MainMenuView::shutdown() 47 | { 48 | QString appPath = QCoreApplication::applicationDirPath(); 49 | QString cmd = appPath + "/shutdown.sh"; 50 | 51 | shutdownProcess = new QProcess(this); 52 | shutdownProcess->start(cmd); 53 | } 54 | -------------------------------------------------------------------------------- /src/view-menu/mainmenuview.h: -------------------------------------------------------------------------------- 1 | #ifndef MAINMENUVIEW_H 2 | #define MAINMENUVIEW_H 3 | 4 | #include 5 | #include 6 | 7 | namespace Ui { 8 | class MainMenuView; 9 | } 10 | 11 | class MainMenuView : public QWidget 12 | { 13 | Q_OBJECT 14 | 15 | public: 16 | explicit MainMenuView(QWidget *parent = nullptr); 17 | ~MainMenuView(); 18 | 19 | signals: 20 | void sourceSelected(int source); 21 | void backClicked(); 22 | 23 | private: 24 | Ui::MainMenuView *ui; 25 | 26 | void fileSourceClicked(); 27 | void btSourceClicked(); 28 | void spotifySourceClicked(); 29 | void cdSourceClicked(); 30 | 31 | QProcess *shutdownProcess = nullptr; 32 | void shutdown(); 33 | }; 34 | 35 | #endif // MAINMENUVIEW_H 36 | -------------------------------------------------------------------------------- /src/view-player/controlbuttonswidget.cpp: -------------------------------------------------------------------------------- 1 | #include "controlbuttonswidget.h" 2 | #include "ui_controlbuttonswidget.h" 3 | #include "scale.h" 4 | 5 | #include 6 | 7 | ControlButtonsWidget::ControlButtonsWidget(QWidget *parent) : 8 | QWidget(parent), 9 | ui(new Ui::ControlButtonsWidget) 10 | { 11 | ui->setupUi(this); 12 | scale(); 13 | 14 | connect(ui->playButton, &QPushButton::clicked, this, &ControlButtonsWidget::playClicked); 15 | connect(ui->pauseButton, &QPushButton::clicked, this, &ControlButtonsWidget::pauseClicked); 16 | connect(ui->stopButton, &QPushButton::clicked, this, &ControlButtonsWidget::stopClicked); 17 | connect(ui->nextButton, &QPushButton::clicked, this, &ControlButtonsWidget::nextClicked); 18 | connect(ui->backButton, &QPushButton::clicked, this, &ControlButtonsWidget::previousClicked); 19 | connect(ui->openButton, &QPushButton::clicked, this, &ControlButtonsWidget::openClicked); 20 | connect(ui->repeatButton, &QCheckBox::clicked, this, &ControlButtonsWidget::repeatClicked); 21 | connect(ui->shuffleButton, &QCheckBox::clicked, this, &ControlButtonsWidget::shuffleClicked); 22 | connect(ui->logoButton, &QCheckBox::clicked, this, &ControlButtonsWidget::logoClicked); 23 | } 24 | 25 | ControlButtonsWidget::~ControlButtonsWidget() 26 | { 27 | delete ui; 28 | } 29 | 30 | void ControlButtonsWidget::scale() 31 | { 32 | ui->backButton->setMaximumWidth(ui->backButton->maximumWidth() * UI_SCALE); 33 | ui->backButton->setMinimumWidth(ui->backButton->minimumWidth() * UI_SCALE); 34 | ui->backButton->setMaximumHeight(ui->backButton->maximumHeight() * UI_SCALE); 35 | ui->backButton->setMinimumHeight(ui->backButton->minimumHeight() * UI_SCALE); 36 | 37 | ui->playButton->setMaximumWidth(ui->playButton->maximumWidth() * UI_SCALE); 38 | ui->playButton->setMinimumWidth(ui->playButton->minimumWidth() * UI_SCALE); 39 | ui->playButton->setMaximumHeight(ui->playButton->maximumHeight() * UI_SCALE); 40 | ui->playButton->setMinimumHeight(ui->playButton->minimumHeight() * UI_SCALE); 41 | 42 | ui->pauseButton->setMaximumWidth(ui->pauseButton->maximumWidth() * UI_SCALE); 43 | ui->pauseButton->setMinimumWidth(ui->pauseButton->minimumWidth() * UI_SCALE); 44 | ui->pauseButton->setMaximumHeight(ui->pauseButton->maximumHeight() * UI_SCALE); 45 | ui->pauseButton->setMinimumHeight(ui->pauseButton->minimumHeight() * UI_SCALE); 46 | 47 | ui->stopButton->setMaximumWidth(ui->stopButton->maximumWidth() * UI_SCALE); 48 | ui->stopButton->setMinimumWidth(ui->stopButton->minimumWidth() * UI_SCALE); 49 | ui->stopButton->setMaximumHeight(ui->stopButton->maximumHeight() * UI_SCALE); 50 | ui->stopButton->setMinimumHeight(ui->stopButton->minimumHeight() * UI_SCALE); 51 | 52 | ui->nextButton->setMaximumWidth(ui->nextButton->maximumWidth() * UI_SCALE); 53 | ui->nextButton->setMinimumWidth(ui->nextButton->minimumWidth() * UI_SCALE); 54 | ui->nextButton->setMaximumHeight(ui->nextButton->maximumHeight() * UI_SCALE); 55 | ui->nextButton->setMinimumHeight(ui->nextButton->minimumHeight() * UI_SCALE); 56 | 57 | ui->openButton->setMaximumWidth(ui->openButton->maximumWidth() * UI_SCALE); 58 | ui->openButton->setMinimumWidth(ui->openButton->minimumWidth() * UI_SCALE); 59 | ui->openButton->setMaximumHeight(ui->openButton->maximumHeight() * UI_SCALE); 60 | ui->openButton->setMinimumHeight(ui->openButton->minimumHeight() * UI_SCALE); 61 | 62 | ui->shuffleButton->setMaximumWidth(ui->shuffleButton->maximumWidth() * UI_SCALE); 63 | ui->shuffleButton->setMinimumWidth(ui->shuffleButton->minimumWidth() * UI_SCALE); 64 | ui->shuffleButton->setMaximumHeight(ui->shuffleButton->maximumHeight() * UI_SCALE); 65 | ui->shuffleButton->setMinimumHeight(ui->shuffleButton->minimumHeight() * UI_SCALE); 66 | ui->shuffleButton->setStyleSheet(getStylesheet("controlbuttonswidget.shuffleButton")); 67 | 68 | ui->repeatButton->setMaximumWidth(ui->repeatButton->maximumWidth() * UI_SCALE); 69 | ui->repeatButton->setMinimumWidth(ui->repeatButton->minimumWidth() * UI_SCALE); 70 | ui->repeatButton->setMaximumHeight(ui->repeatButton->maximumHeight() * UI_SCALE); 71 | ui->repeatButton->setMinimumHeight(ui->repeatButton->minimumHeight() * UI_SCALE); 72 | ui->repeatButton->setStyleSheet(getStylesheet("controlbuttonswidget.repeatButton")); 73 | 74 | ui->logoButton->setMaximumWidth(ui->logoButton->maximumWidth() * UI_SCALE); 75 | ui->logoButton->setMinimumWidth(ui->logoButton->minimumWidth() * UI_SCALE); 76 | ui->logoButton->setMaximumHeight(ui->logoButton->maximumHeight() * UI_SCALE); 77 | ui->logoButton->setMinimumHeight(ui->logoButton->minimumHeight() * UI_SCALE); 78 | 79 | ui->playControlsContainer->setMaximumWidth(ui->playControlsContainer->maximumWidth() * UI_SCALE); 80 | ui->playControlsContainer->setMinimumWidth(ui->playControlsContainer->minimumWidth() * UI_SCALE); 81 | ui->playControlsContainer->setMaximumHeight(ui->playControlsContainer->maximumHeight() * UI_SCALE); 82 | ui->playControlsContainer->setMinimumHeight(ui->playControlsContainer->minimumHeight() * UI_SCALE); 83 | 84 | this->setMaximumWidth(this->maximumWidth() * UI_SCALE); 85 | this->setMinimumWidth(this->minimumWidth() * UI_SCALE); 86 | this->setMaximumHeight(this->maximumHeight() * UI_SCALE); 87 | this->setMinimumHeight(this->minimumHeight() * UI_SCALE); 88 | } 89 | 90 | void ControlButtonsWidget::setShuffleEnabled(bool enabled) 91 | { 92 | ui->shuffleButton->setChecked(enabled); 93 | } 94 | 95 | void ControlButtonsWidget::setRepeatEnabled(bool enabled) 96 | { 97 | ui->repeatButton->setChecked(enabled); 98 | } 99 | -------------------------------------------------------------------------------- /src/view-player/controlbuttonswidget.h: -------------------------------------------------------------------------------- 1 | #ifndef CONTROLBUTTONSWIDGET_H 2 | #define CONTROLBUTTONSWIDGET_H 3 | 4 | #include 5 | 6 | namespace Ui { 7 | class ControlButtonsWidget; 8 | } 9 | 10 | class ControlButtonsWidget : public QWidget 11 | { 12 | Q_OBJECT 13 | 14 | public: 15 | explicit ControlButtonsWidget(QWidget *parent = nullptr); 16 | ~ControlButtonsWidget(); 17 | 18 | void setShuffleEnabled(bool enabled); 19 | void setRepeatEnabled(bool enabled); 20 | 21 | private: 22 | Ui::ControlButtonsWidget *ui; 23 | void scale(); 24 | 25 | signals: 26 | void playClicked(); 27 | void pauseClicked(); 28 | void stopClicked(); 29 | void nextClicked(); 30 | void previousClicked(); 31 | void openClicked(); 32 | void repeatClicked(bool checked); 33 | void shuffleClicked(bool checked); 34 | void logoClicked(); 35 | }; 36 | 37 | #endif // CONTROLBUTTONSWIDGET_H 38 | -------------------------------------------------------------------------------- /src/view-player/playerview.h: -------------------------------------------------------------------------------- 1 | #ifndef PLAYERVIEW_H 2 | #define PLAYERVIEW_H 3 | 4 | #include "controlbuttonswidget.h" 5 | #include "mediaplayer.h" 6 | #include "spectrumwidget.h" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | QT_BEGIN_NAMESPACE 14 | class QAbstractItemView; 15 | class QLabel; 16 | class QModelIndex; 17 | class QPushButton; 18 | class QComboBox; 19 | class QSlider; 20 | class QStatusBar; 21 | QT_END_NAMESPACE 22 | 23 | namespace Ui { 24 | class PlayerView; 25 | } 26 | 27 | class PlayerView : public QWidget 28 | { 29 | Q_OBJECT 30 | 31 | public: 32 | explicit PlayerView(QWidget *parent = nullptr, ControlButtonsWidget *ctlBtns = nullptr); 33 | ~PlayerView(); 34 | 35 | void setSourceLabel(QString label); 36 | 37 | public slots: 38 | void setPlaybackState(MediaPlayer::PlaybackState state); 39 | void setPosition(qint64 progress); 40 | void setSpectrumData(const QByteArray& data, QAudioFormat format); 41 | void setMetadata(QMediaMetaData metadata); 42 | void setDuration(qint64 duration); 43 | void setVolume(int volume); 44 | void setBalance(int balance); 45 | void setEqEnabled(bool enabled); 46 | void setPlEnabled(bool enabled); 47 | void setShuffleEnabled(bool enabled); 48 | void setRepeatEnabled(bool enabled); 49 | void setMessage(QString message, qint64 timeout); 50 | void clearMessage(); 51 | 52 | signals: 53 | void volumeChanged(int volume); 54 | void balanceChanged(int balance); 55 | void positionChanged(qint64 progress); 56 | void eqClicked(); 57 | void plClicked(); 58 | void previousClicked(); 59 | void playClicked(); 60 | void pauseClicked(); 61 | void stopClicked(); 62 | void nextClicked(); 63 | void openClicked(); 64 | void shuffleClicked(); 65 | void repeatClicked(); 66 | void menuClicked(); 67 | 68 | private: 69 | Ui::PlayerView *ui; 70 | void scale(); 71 | SpectrumWidget *spectrum = nullptr; 72 | QTimer *messageTimer = nullptr; 73 | ControlButtonsWidget *controlButtons = nullptr; 74 | 75 | QString m_trackInfo; 76 | QString m_statusInfo; 77 | qint64 m_duration; 78 | 79 | bool plEnabled = false; 80 | bool eqEnabled = false; 81 | bool shuffleEnabled = false; 82 | bool repeatEnabled = false; 83 | 84 | void updateDurationInfo(qint64 currentInfo); 85 | void setTrackInfo(const QString &info); 86 | 87 | private slots: 88 | void handleBalanceChanged(); 89 | 90 | }; 91 | 92 | #endif // PLAYERVIEW_H 93 | -------------------------------------------------------------------------------- /src/view-player/scrolltext.cpp: -------------------------------------------------------------------------------- 1 | #include "scrolltext.h" 2 | #include 3 | 4 | 5 | ScrollText::ScrollText(QWidget *parent) : 6 | QWidget(parent), scrollPos(0) 7 | { 8 | staticText.setTextFormat(Qt::PlainText); 9 | 10 | leftMargin = height() / 3; 11 | 12 | setSeparator(" --- "); 13 | 14 | connect(&timer, SIGNAL(timeout()), this, SLOT(timer_timeout())); 15 | timer.setInterval(50); 16 | } 17 | 18 | QString ScrollText::text() const 19 | { 20 | return _text; 21 | } 22 | 23 | void ScrollText::setText(QString text) 24 | { 25 | _text = text; 26 | updateText(); 27 | update(); 28 | } 29 | 30 | QString ScrollText::separator() const 31 | { 32 | return _separator; 33 | } 34 | 35 | void ScrollText::setSeparator(QString separator) 36 | { 37 | _separator = separator; 38 | updateText(); 39 | update(); 40 | } 41 | 42 | void ScrollText::updateText() 43 | { 44 | timer.stop(); 45 | 46 | singleTextWidth = fontMetrics().horizontalAdvance(_text); 47 | scrollEnabled = (singleTextWidth > width() - leftMargin); 48 | 49 | if(scrollEnabled) 50 | { 51 | scrollPos = -64; 52 | staticText.setText(_text + _separator); 53 | timer.start(); 54 | } 55 | else 56 | staticText.setText(_text); 57 | 58 | staticText.prepare(QTransform(), font()); 59 | wholeTextSize = QSize(fontMetrics().horizontalAdvance(staticText.text()), fontMetrics().height()); 60 | } 61 | 62 | void ScrollText::paintEvent(QPaintEvent*) 63 | { 64 | QPainter p(this); 65 | 66 | if(scrollEnabled) 67 | { 68 | buffer.fill(qRgba(0, 0, 0, 0)); 69 | QPainter pb(&buffer); 70 | pb.setPen(p.pen()); 71 | pb.setFont(p.font()); 72 | 73 | int x = qMin(-scrollPos, 0) + leftMargin; 74 | while(x < width()) 75 | { 76 | pb.drawStaticText(QPointF(x, (height() - wholeTextSize.height()) / 2) + QPoint(2, 2), staticText); 77 | x += wholeTextSize.width(); 78 | } 79 | 80 | //Apply Alpha Channel 81 | /*pb.setCompositionMode(QPainter::CompositionMode_DestinationIn); 82 | pb.setClipRect(width() - 15, 0, 15, height()); 83 | pb.drawImage(0, 0, alphaChannel); 84 | pb.setClipRect(0, 0, 15, height()); 85 | //initial situation: don't apply alpha channel in the left half of the image at all; apply it more and more until scrollPos gets positive 86 | if(scrollPos < 0) 87 | pb.setOpacity((qreal)(qMax(-8, scrollPos) + 8) / 8.0); 88 | pb.drawImage(0, 0, alphaChannel);*/ 89 | 90 | //pb.end(); 91 | p.drawImage(0, 0, buffer); 92 | } 93 | else 94 | { 95 | p.drawStaticText(QPointF(leftMargin, (height() - wholeTextSize.height()) / 2), staticText); 96 | } 97 | } 98 | 99 | void ScrollText::resizeEvent(QResizeEvent*) 100 | { 101 | //When the widget is resized, we need to update the alpha channel. 102 | 103 | alphaChannel = QImage(size(), QImage::Format_ARGB32_Premultiplied); 104 | buffer = QImage(size(), QImage::Format_ARGB32_Premultiplied); 105 | 106 | //Create Alpha Channel: 107 | if(width() > 64) 108 | { 109 | //create first scanline 110 | QRgb* scanline1 = (QRgb*)alphaChannel.scanLine(0); 111 | for(int x = 1; x < 16; ++x) 112 | scanline1[x - 1] = scanline1[width() - x] = qRgba(0, 0, 0, x << 4); 113 | for(int x = 15; x < width() - 15; ++x) 114 | scanline1[x] = qRgb(0, 0, 0); 115 | //copy scanline to the other ones 116 | for(int y = 1; y < height(); ++y) 117 | memcpy(alphaChannel.scanLine(y), (uchar*)scanline1, width() * 4); 118 | } 119 | else 120 | alphaChannel.fill(qRgb(0, 0, 0)); 121 | 122 | 123 | //Update scrolling state 124 | bool newScrollEnabled = (singleTextWidth > width() - leftMargin); 125 | if(newScrollEnabled != scrollEnabled) 126 | updateText(); 127 | } 128 | 129 | void ScrollText::timer_timeout() 130 | { 131 | scrollPos = (scrollPos + 2) 132 | % wholeTextSize.width(); 133 | update(); 134 | } 135 | -------------------------------------------------------------------------------- /src/view-player/scrolltext.h: -------------------------------------------------------------------------------- 1 | #ifndef SCROLLTEXT_H 2 | #define SCROLLTEXT_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | 9 | class ScrollText : public QWidget 10 | { 11 | Q_OBJECT 12 | Q_PROPERTY(QString text READ text WRITE setText) 13 | Q_PROPERTY(QString separator READ separator WRITE setSeparator) 14 | 15 | public: 16 | explicit ScrollText(QWidget *parent = 0); 17 | QString text() const; 18 | QString separator() const; 19 | 20 | public slots: 21 | void setText(QString text); 22 | void setSeparator(QString separator); 23 | 24 | 25 | protected: 26 | virtual void paintEvent(QPaintEvent *); 27 | virtual void resizeEvent(QResizeEvent *); 28 | 29 | private: 30 | void updateText(); 31 | QString _text; 32 | QString _separator; 33 | QStaticText staticText; 34 | int singleTextWidth; 35 | QSize wholeTextSize; 36 | int leftMargin; 37 | bool scrollEnabled; 38 | int scrollPos; 39 | QImage alphaChannel; 40 | QImage buffer; 41 | QTimer timer; 42 | 43 | private slots: 44 | virtual void timer_timeout(); 45 | }; 46 | 47 | #endif // SCROLLTEXT_H 48 | -------------------------------------------------------------------------------- /src/view-player/spectrumwidget.h: -------------------------------------------------------------------------------- 1 | #ifndef SPECTRUMWIDGET_H 2 | #define SPECTRUMWIDGET_H 3 | 4 | #include "qaudioformat.h" 5 | #define DFT_SIZE 512 /* size of the DFT */ 6 | #define N_BANDS 19 7 | 8 | #include 9 | #include 10 | 11 | class SpectrumWidget : public QWidget 12 | { 13 | Q_OBJECT 14 | public: 15 | explicit SpectrumWidget(QWidget *parent = nullptr); 16 | void play(); 17 | void pause(); 18 | void stop(); 19 | 20 | protected: 21 | void paintEvent (QPaintEvent *); 22 | 23 | private: 24 | float m_data[DFT_SIZE * 4]; 25 | float m_xscale[N_BANDS + 1]; 26 | int m_bandValues[N_BANDS + 1]; 27 | int m_bandDelays[N_BANDS + 1]; 28 | int m_peakValues[N_BANDS + 1]; 29 | int m_peakDelays[N_BANDS + 1]; 30 | bool m_playing = false; 31 | QTimer *m_renderTimer = nullptr; 32 | QAudioFormat m_format; 33 | 34 | void paintBackground(QPainter &); 35 | void paintSpectrum(QPainter &); 36 | void paintPeaks(QPainter &); 37 | 38 | void clear(); 39 | 40 | public slots: 41 | void setData(const QByteArray &data, QAudioFormat format); 42 | 43 | signals: 44 | 45 | }; 46 | 47 | #endif // SPECTRUMWIDGET_H 48 | -------------------------------------------------------------------------------- /src/view-playlist/filebrowsericonprovider.cpp: -------------------------------------------------------------------------------- 1 | #include "filebrowsericonprovider.h" 2 | #include "util.h" 3 | #include 4 | 5 | FileBrowserIconProvider::FileBrowserIconProvider() 6 | { 7 | folderIcon = new QIcon(":/assets/fb_folderIcon.png"); 8 | folderIcon->addFile(":/assets/fb_folderIcon_selected.png", QSize(), QIcon::Selected); 9 | 10 | musicIcon = new QIcon(":/assets/fb_musicIcon.png"); 11 | musicIcon->addFile(":/assets/fb_musicIcon_selected.png", QSize(), QIcon::Selected); 12 | } 13 | 14 | QIcon FileBrowserIconProvider::icon(QAbstractFileIconProvider::IconType type) const 15 | { 16 | if(type == QAbstractFileIconProvider::IconType::Folder) { 17 | return *folderIcon; 18 | } 19 | 20 | return QFileIconProvider::icon(type); 21 | } 22 | 23 | QIcon FileBrowserIconProvider::icon(const QFileInfo &info) const 24 | { 25 | 26 | if(info.isDir()) { 27 | return *folderIcon; 28 | } 29 | 30 | const QString &path = info.absoluteFilePath(); 31 | 32 | if(isAudioFile(path)) { 33 | return *musicIcon; 34 | } 35 | 36 | return QFileIconProvider::icon(info); 37 | } 38 | -------------------------------------------------------------------------------- /src/view-playlist/filebrowsericonprovider.h: -------------------------------------------------------------------------------- 1 | #ifndef FILEBROWSERICONPROVIDER_H 2 | #define FILEBROWSERICONPROVIDER_H 3 | 4 | #include 5 | #include 6 | 7 | class FileBrowserIconProvider : public QFileIconProvider 8 | { 9 | public: 10 | FileBrowserIconProvider(); 11 | 12 | QIcon icon(QAbstractFileIconProvider::IconType type) const override; 13 | QIcon icon(const QFileInfo &info) const override; 14 | 15 | private: 16 | QIcon *folderIcon = nullptr; 17 | QIcon *musicIcon = nullptr; 18 | }; 19 | 20 | #endif // FILEBROWSERICONPROVIDER_H 21 | -------------------------------------------------------------------------------- /src/view-playlist/playlistmodel.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2017 The Qt Company Ltd. 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause 3 | 4 | #ifndef PLAYLISTMODEL_H 5 | #define PLAYLISTMODEL_H 6 | 7 | #include 8 | #include 9 | 10 | QT_BEGIN_NAMESPACE 11 | class QMediaPlaylist; 12 | QT_END_NAMESPACE 13 | 14 | class PlaylistModel : public QAbstractItemModel 15 | { 16 | Q_OBJECT 17 | 18 | public: 19 | enum Column { Track = 0, Title, Artist, Album, Duration, ColumnCount }; 20 | QString MimeType = "application/playlist.model"; 21 | 22 | explicit PlaylistModel(QObject *parent = nullptr); 23 | ~PlaylistModel(); 24 | 25 | int rowCount(const QModelIndex &parent = QModelIndex()) const override; 26 | int columnCount(const QModelIndex &parent = QModelIndex()) const override; 27 | bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; 28 | 29 | QModelIndex index(int row, int column, 30 | const QModelIndex &parent = QModelIndex()) const override; 31 | QModelIndex parent(const QModelIndex &child) const override; 32 | 33 | QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; 34 | 35 | QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; 36 | 37 | QMediaPlaylist *playlist() const; 38 | 39 | bool setData(const QModelIndex &index, const QVariant &value, 40 | int role = Qt::DisplayRole) override; 41 | 42 | Qt::ItemFlags flags(const QModelIndex &index) const override; 43 | 44 | Qt::DropActions supportedDragActions() const override; 45 | Qt::DropActions supportedDropActions() const override; 46 | 47 | QStringList mimeTypes() const override; 48 | bool canDropMimeData(const QMimeData *data, Qt::DropAction action, 49 | int row, int column, const QModelIndex &parent) const override; 50 | QMimeData *mimeData(const QModelIndexList &indexes) const override; 51 | bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, 52 | int column, const QModelIndex &parent) override; 53 | 54 | private slots: 55 | void beginInsertItems(int start, int end); 56 | void endInsertItems(); 57 | void beginRemoveItems(int start, int end); 58 | void endRemoveItems(); 59 | void changeItems(int start, int end); 60 | 61 | private: 62 | QScopedPointer m_playlist; 63 | }; 64 | 65 | #endif // PLAYLISTMODEL_H 66 | -------------------------------------------------------------------------------- /src/view-playlist/playlistview.h: -------------------------------------------------------------------------------- 1 | #ifndef PLAYLISTVIEW_H 2 | #define PLAYLISTVIEW_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "qmediaplaylist.h" 9 | #include "playlistmodel.h" 10 | 11 | namespace Ui { 12 | class PlaylistView; 13 | } 14 | 15 | class PlaylistView : public QWidget 16 | { 17 | Q_OBJECT 18 | 19 | public: 20 | explicit PlaylistView(QWidget *parent = nullptr, PlaylistModel *playlistModel = nullptr); 21 | ~PlaylistView(); 22 | 23 | private: 24 | Ui::PlaylistView *ui; 25 | QMediaPlaylist *m_playlist = nullptr; 26 | PlaylistModel *m_playlistModel = nullptr; 27 | QFileSystemModel *m_fileSystemModel = nullptr; 28 | QScroller *m_playlistViewScroller = nullptr; 29 | 30 | void setupPlayListUi(); 31 | void setupFileBrowserUi(); 32 | 33 | // File browser functions 34 | void fbCd(QString path); 35 | 36 | private slots: 37 | void playlistPositionChanged(int); 38 | void clearPlaylist(); 39 | void removeItem(); 40 | void handleSongSelected(const QModelIndex &index); 41 | void handleSelectionChanged(int index); 42 | 43 | // File browser functions 44 | void fbGoHome(); 45 | void fbGoUp(); 46 | void fbAdd(); 47 | void fbToggleSelect(); 48 | 49 | void fbItemClicked(const QModelIndex &index); 50 | 51 | void updateTotalDuration(); 52 | 53 | void toggleEditMode(); 54 | 55 | signals: 56 | void showPlayerClicked(); 57 | void songSelected(const QModelIndex &index); 58 | void addSelectedFilesClicked(const QList &urls); 59 | }; 60 | 61 | #endif // PLAYLISTVIEW_H 62 | -------------------------------------------------------------------------------- /src/view-playlist/qmediaplaylist.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 The Qt Company Ltd. 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only 3 | 4 | #ifndef QMEDIAPLAYLIST_H 5 | #define QMEDIAPLAYLIST_H 6 | 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | QT_BEGIN_NAMESPACE 13 | 14 | class QMediaPlaylistPrivate; 15 | class QMediaPlaylist : public QObject 16 | { 17 | Q_OBJECT 18 | Q_PROPERTY(QMediaPlaylist::PlaybackMode playbackMode READ playbackMode WRITE setPlaybackMode 19 | NOTIFY playbackModeChanged) 20 | Q_PROPERTY(QUrl currentMedia READ currentMedia NOTIFY currentMediaChanged) 21 | Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) 22 | 23 | public: 24 | enum PlaybackMode { CurrentItemOnce, CurrentItemInLoop, Sequential, Loop }; 25 | Q_ENUM(PlaybackMode) 26 | enum Error { NoError, FormatError, FormatNotSupportedError, NetworkError, AccessDeniedError }; 27 | Q_ENUM(Error) 28 | 29 | explicit QMediaPlaylist(QObject *parent = nullptr); 30 | virtual ~QMediaPlaylist(); 31 | 32 | PlaybackMode playbackMode() const; 33 | void setPlaybackMode(PlaybackMode mode); 34 | void setShuffle(bool shuffle); 35 | 36 | // For playlist 37 | int currentIndex() const; 38 | QUrl currentMedia() const; 39 | 40 | // For playqueue 41 | int currentQueueIndex() const; 42 | QUrl currentQueueMedia() const; 43 | 44 | int nextIndex(int steps = 1) const; 45 | int previousIndex(int steps = 1) const; 46 | 47 | int nextQueueIndex(int steps = 1) const; 48 | int previousQueueIndex(int steps = 1) const; 49 | 50 | QUrl media(int index) const; 51 | QUrl queueMedia(int index) const; 52 | 53 | QMediaMetaData mediaMetadata(int index) const; 54 | QMediaMetaData queueMediaMetadata(int index) const; 55 | 56 | 57 | int mediaCount() const; 58 | bool isEmpty() const; 59 | qint64 totalDuration() const; 60 | 61 | void addMedia(const QUrl &content); 62 | void addMedia(const QList &items); 63 | bool insertMedia(int index, const QUrl &content); 64 | bool insertMedia(int index, const QList &items); 65 | bool moveMedia(int from, int to); 66 | bool removeMedia(int pos); 67 | bool removeMedia(int start, int end); 68 | void clear(); 69 | 70 | void load(const QUrl &location, const char *format = nullptr); 71 | void load(QIODevice *device, const char *format = nullptr); 72 | 73 | bool save(const QUrl &location, const char *format = nullptr) const; 74 | bool save(QIODevice *device, const char *format) const; 75 | 76 | Error error() const; 77 | QString errorString() const; 78 | 79 | public slots: 80 | void shuffle(); 81 | void unshuffle(); 82 | 83 | void next(); 84 | void previous(); 85 | 86 | void setCurrentIndex(int index); 87 | void setCurrentQueueIndex(int index); 88 | 89 | signals: 90 | void currentIndexChanged(int index); 91 | void playbackModeChanged(QMediaPlaylist::PlaybackMode mode); 92 | void currentMediaChanged(const QUrl &); 93 | 94 | // Emited when we want to inform the view to select a different index without playinf 95 | void currentSelectionChanged(int index); 96 | 97 | void mediaAboutToBeInserted(int start, int end); 98 | void mediaInserted(int start, int end); 99 | void mediaAboutToBeRemoved(int start, int end); 100 | void mediaRemoved(int start, int end); 101 | void mediaChanged(int start, int end); 102 | 103 | void loaded(); 104 | void loadFailed(); 105 | 106 | private: 107 | bool shuffleEnabled = false; 108 | QMediaPlaylistPrivate *d_ptr; 109 | Q_DECLARE_PRIVATE(QMediaPlaylist) 110 | 111 | // Metadata for files in the playlist 112 | QMap m_mediaMetadata; 113 | void loadMetadata(const QUrl &url); 114 | void vacuumMetadata(); 115 | }; 116 | 117 | QT_END_NAMESPACE 118 | 119 | Q_MEDIA_ENUM_DEBUG(QMediaPlaylist, PlaybackMode) 120 | Q_MEDIA_ENUM_DEBUG(QMediaPlaylist, Error) 121 | 122 | #endif // QMEDIAPLAYLIST_H 123 | -------------------------------------------------------------------------------- /src/view-playlist/qmediaplaylist_p.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 The Qt Company Ltd. 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only 3 | 4 | #include "qmediaplaylist_p.h" 5 | 6 | QT_BEGIN_NAMESPACE 7 | 8 | QMediaPlaylistPrivate::QMediaPlaylistPrivate() : error(QMediaPlaylist::NoError) { } 9 | 10 | QMediaPlaylistPrivate::~QMediaPlaylistPrivate() 11 | { 12 | delete parser; 13 | } 14 | 15 | void QMediaPlaylistPrivate::loadFailed(QMediaPlaylist::Error error, const QString &errorString) 16 | { 17 | this->error = error; 18 | this->errorString = errorString; 19 | 20 | emit q_ptr->loadFailed(); 21 | } 22 | 23 | void QMediaPlaylistPrivate::loadFinished() 24 | { 25 | q_ptr->addMedia(parser->playlist); 26 | 27 | emit q_ptr->loaded(); 28 | } 29 | 30 | bool QMediaPlaylistPrivate::checkFormat(const char *format) const 31 | { 32 | QLatin1String f(format); 33 | QPlaylistFileParser::FileType type = 34 | format ? QPlaylistFileParser::UNKNOWN : QPlaylistFileParser::M3U8; 35 | if (format) { 36 | if (f == QLatin1String("m3u") || f == QLatin1String("text/uri-list") 37 | || f == QLatin1String("audio/x-mpegurl") || f == QLatin1String("audio/mpegurl")) 38 | type = QPlaylistFileParser::M3U; 39 | else if (f == QLatin1String("m3u8") || f == QLatin1String("application/x-mpegURL") 40 | || f == QLatin1String("application/vnd.apple.mpegurl")) 41 | type = QPlaylistFileParser::M3U8; 42 | } 43 | 44 | if (type == QPlaylistFileParser::UNKNOWN || type == QPlaylistFileParser::PLS) { 45 | error = QMediaPlaylist::FormatNotSupportedError; 46 | errorString = QMediaPlaylist::tr("This file format is not supported."); 47 | return false; 48 | } 49 | return true; 50 | } 51 | 52 | void QMediaPlaylistPrivate::ensureParser() 53 | { 54 | if (parser) 55 | return; 56 | 57 | parser = new QPlaylistFileParser(q_ptr); 58 | QObject::connect(parser, &QPlaylistFileParser::finished, [this]() { loadFinished(); }); 59 | QObject::connect(parser, &QPlaylistFileParser::error, 60 | [this](QMediaPlaylist::Error err, const QString &errorMsg) { 61 | loadFailed(err, errorMsg); 62 | }); 63 | } 64 | 65 | int QMediaPlaylistPrivate::currentPos() const 66 | { 67 | return m_currentPos; 68 | } 69 | 70 | int QMediaPlaylistPrivate::currentQueuePos() const 71 | { 72 | return m_currentQueuePos; 73 | } 74 | 75 | void QMediaPlaylistPrivate::setCurrentPos(int pos) 76 | { 77 | m_currentPos = pos; 78 | 79 | if (pos < 0 || pos >= playlist.size()) { 80 | m_currentQueuePos = pos; 81 | return; 82 | } 83 | 84 | // Sync currentPlayPos 85 | QUrl item = playlist.at(m_currentPos); 86 | m_currentQueuePos = playqueue.indexOf(item); 87 | } 88 | 89 | void QMediaPlaylistPrivate::setCurrentQueuePos(int pos) 90 | { 91 | m_currentQueuePos = pos; 92 | 93 | if (pos < 0 || pos >= playlist.size()) { 94 | m_currentPos = pos; 95 | return; 96 | } 97 | 98 | // Sync currentPos 99 | QUrl item = playqueue.at(m_currentQueuePos); 100 | m_currentPos = playlist.indexOf(item); 101 | } 102 | 103 | QT_END_NAMESPACE 104 | -------------------------------------------------------------------------------- /src/view-playlist/qmediaplaylist_p.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 The Qt Company Ltd. 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only 3 | 4 | #ifndef QMEDIAPLAYLIST_P_H 5 | #define QMEDIAPLAYLIST_P_H 6 | 7 | // 8 | // W A R N I N G 9 | // ------------- 10 | // 11 | // This file is not part of the Qt API. It exists purely as an 12 | // implementation detail. This header file may change from version to 13 | // version without notice, or even be removed. 14 | // 15 | // We mean it. 16 | // 17 | 18 | #include "qmediaplaylist.h" 19 | #include "qplaylistfileparser.h" 20 | 21 | #include 22 | 23 | #include 24 | 25 | #ifdef Q_MOC_RUN 26 | # pragma Q_MOC_EXPAND_MACROS 27 | #endif 28 | 29 | QT_BEGIN_NAMESPACE 30 | 31 | class QMediaPlaylistControl; 32 | 33 | class QMediaPlaylistPrivate 34 | { 35 | Q_DECLARE_PUBLIC(QMediaPlaylist) 36 | public: 37 | QMediaPlaylistPrivate(); 38 | 39 | virtual ~QMediaPlaylistPrivate(); 40 | 41 | void loadFailed(QMediaPlaylist::Error error, const QString &errorString); 42 | 43 | void loadFinished(); 44 | 45 | bool checkFormat(const char *format) const; 46 | 47 | void ensureParser(); 48 | 49 | int nextPosition(int steps) const; 50 | int prevPosition(int steps) const; 51 | 52 | int nextQueuePosition(int steps) const; 53 | int prevQueuePosition(int steps) const; 54 | 55 | QList playlist; // Displayed playlist 56 | QList playqueue; // Actual playing queue 57 | 58 | int currentPos() const; 59 | int currentQueuePos() const; 60 | void setCurrentPos(int pos); 61 | void setCurrentQueuePos(int pos); 62 | QMediaPlaylist::PlaybackMode playbackMode = QMediaPlaylist::Sequential; 63 | 64 | QPlaylistFileParser *parser = nullptr; 65 | mutable QMediaPlaylist::Error error; 66 | mutable QString errorString; 67 | 68 | QMediaPlaylist *q_ptr; 69 | 70 | private: 71 | int m_currentPos = -1; 72 | int m_currentQueuePos = -1; 73 | }; 74 | 75 | QT_END_NAMESPACE 76 | 77 | #endif // QMEDIAPLAYLIST_P_H 78 | -------------------------------------------------------------------------------- /src/view-playlist/qplaylistfileparser.h: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 The Qt Company Ltd. 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only 3 | 4 | #ifndef PLAYLISTFILEPARSER_P_H 5 | #define PLAYLISTFILEPARSER_P_H 6 | 7 | // 8 | // W A R N I N G 9 | // ------------- 10 | // 11 | // This file is not part of the Qt API. It exists purely as an 12 | // implementation detail. This header file may change from version to 13 | // version without notice, or even be removed. 14 | // 15 | // We mean it. 16 | // 17 | 18 | #include "qmediaplaylist.h" 19 | #include "qtmultimediaglobal.h" 20 | 21 | #include 22 | 23 | QT_BEGIN_NAMESPACE 24 | 25 | class QIODevice; 26 | class QUrl; 27 | class QNetworkRequest; 28 | 29 | class QPlaylistFileParserPrivate; 30 | 31 | class QPlaylistFileParser : public QObject 32 | { 33 | Q_OBJECT 34 | public: 35 | QPlaylistFileParser(QObject *parent = nullptr); 36 | ~QPlaylistFileParser(); 37 | 38 | enum FileType { 39 | UNKNOWN, 40 | M3U, 41 | M3U8, // UTF-8 version of M3U 42 | PLS 43 | }; 44 | 45 | void start(const QUrl &media, QIODevice *stream = nullptr, const QString &mimeType = QString()); 46 | void start(const QUrl &request, const QString &mimeType = QString()); 47 | void start(QIODevice *stream, const QString &mimeType = QString()); 48 | void abort(); 49 | 50 | QList playlist; 51 | 52 | signals: 53 | void newItem(const QVariant &content); 54 | void finished(); 55 | void error(QMediaPlaylist::Error err, const QString &errorMsg); 56 | 57 | private slots: 58 | void handleData(); 59 | void handleError(); 60 | 61 | private: 62 | static FileType findByMimeType(const QString &mime); 63 | static FileType findBySuffixType(const QString &suffix); 64 | static FileType findByDataHeader(const char *data, quint32 size); 65 | static FileType findPlaylistType(QIODevice *device, const QString &mime); 66 | static FileType findPlaylistType(const QString &suffix, const QString &mime, 67 | const char *data = nullptr, quint32 size = 0); 68 | 69 | Q_DISABLE_COPY(QPlaylistFileParser) 70 | Q_DECLARE_PRIVATE(QPlaylistFileParser) 71 | QScopedPointer d_ptr; 72 | }; 73 | 74 | QT_END_NAMESPACE 75 | 76 | #endif // PLAYLISTFILEPARSER_P_H 77 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 4 | 5 | source $SCRIPT_DIR/venv/bin/activate 6 | DISPLAY=:0 PYTHONPATH=$SCRIPT_DIR/python $SCRIPT_DIR/build/player 7 | -------------------------------------------------------------------------------- /styles/controlbuttonswidget.repeatButton.1x.qss: -------------------------------------------------------------------------------- 1 | QCheckBox { 2 | spacing: 0px; 3 | } 4 | 5 | QCheckBox::indicator { 6 | width: 28px; 7 | height: 15px; 8 | } 9 | 10 | QCheckBox::indicator:unchecked { 11 | image: url(:assets/repeat_off.png); 12 | } 13 | 14 | QCheckBox::indicator:unchecked:hover { 15 | image: url(:assets/repeat_off.png); 16 | } 17 | 18 | QCheckBox::indicator:unchecked:pressed { 19 | image: url(:assets/repeat_off_p.png); 20 | } 21 | 22 | QCheckBox::indicator:checked { 23 | image: url(:assets/repeat_on.png); 24 | } 25 | 26 | QCheckBox::indicator:checked:hover { 27 | image: url(:assets/repeat_on.png); 28 | } 29 | 30 | QCheckBox::indicator:checked:pressed { 31 | image: url(:assets/repeat_on_p.png); 32 | } 33 | 34 | QCheckBox::indicator:indeterminate:hover { 35 | image: url(:assets/repeat_off.png); 36 | } 37 | 38 | QCheckBox::indicator:indeterminate:pressed { 39 | image: url(:assets/repeat_off_p.png); 40 | } 41 | -------------------------------------------------------------------------------- /styles/controlbuttonswidget.repeatButton.2x.qss: -------------------------------------------------------------------------------- 1 | QCheckBox { 2 | spacing: 0px; 3 | } 4 | 5 | QCheckBox::indicator { 6 | width: 56px; 7 | height: 30px; 8 | } 9 | 10 | QCheckBox::indicator:unchecked { 11 | image: url(:assets/repeat_off.png); 12 | } 13 | 14 | QCheckBox::indicator:unchecked:hover { 15 | image: url(:assets/repeat_off.png); 16 | } 17 | 18 | QCheckBox::indicator:unchecked:pressed { 19 | image: url(:assets/repeat_off_p.png); 20 | } 21 | 22 | QCheckBox::indicator:checked { 23 | image: url(:assets/repeat_on.png); 24 | } 25 | 26 | QCheckBox::indicator:checked:hover { 27 | image: url(:assets/repeat_on.png); 28 | } 29 | 30 | QCheckBox::indicator:checked:pressed { 31 | image: url(:assets/repeat_on_p.png); 32 | } 33 | 34 | QCheckBox::indicator:indeterminate:hover { 35 | image: url(:assets/repeat_off.png); 36 | } 37 | 38 | QCheckBox::indicator:indeterminate:pressed { 39 | image: url(:assets/repeat_off_p.png); 40 | } 41 | -------------------------------------------------------------------------------- /styles/controlbuttonswidget.repeatButton.3x.qss: -------------------------------------------------------------------------------- 1 | QCheckBox { 2 | spacing: 0px; 3 | } 4 | 5 | QCheckBox::indicator { 6 | width: 84px; 7 | height: 45px; 8 | } 9 | 10 | QCheckBox::indicator:unchecked { 11 | image: url(:assets/repeat_off.png); 12 | } 13 | 14 | QCheckBox::indicator:unchecked:hover { 15 | image: url(:assets/repeat_off.png); 16 | } 17 | 18 | QCheckBox::indicator:unchecked:pressed { 19 | image: url(:assets/repeat_off_p.png); 20 | } 21 | 22 | QCheckBox::indicator:checked { 23 | image: url(:assets/repeat_on.png); 24 | } 25 | 26 | QCheckBox::indicator:checked:hover { 27 | image: url(:assets/repeat_on.png); 28 | } 29 | 30 | QCheckBox::indicator:checked:pressed { 31 | image: url(:assets/repeat_on_p.png); 32 | } 33 | 34 | QCheckBox::indicator:indeterminate:hover { 35 | image: url(:assets/repeat_off.png); 36 | } 37 | 38 | QCheckBox::indicator:indeterminate:pressed { 39 | image: url(:assets/repeat_off_p.png); 40 | } 41 | -------------------------------------------------------------------------------- /styles/controlbuttonswidget.repeatButton.4x.qss: -------------------------------------------------------------------------------- 1 | QCheckBox { 2 | spacing: 0px; 3 | } 4 | 5 | QCheckBox::indicator { 6 | width: 112px; 7 | height: 60px; 8 | } 9 | 10 | QCheckBox::indicator:unchecked { 11 | image: url(:assets/repeat_off.png); 12 | } 13 | 14 | QCheckBox::indicator:unchecked:hover { 15 | image: url(:assets/repeat_off.png); 16 | } 17 | 18 | QCheckBox::indicator:unchecked:pressed { 19 | image: url(:assets/repeat_off_p.png); 20 | } 21 | 22 | QCheckBox::indicator:checked { 23 | image: url(:assets/repeat_on.png); 24 | } 25 | 26 | QCheckBox::indicator:checked:hover { 27 | image: url(:assets/repeat_on.png); 28 | } 29 | 30 | QCheckBox::indicator:checked:pressed { 31 | image: url(:assets/repeat_on_p.png); 32 | } 33 | 34 | QCheckBox::indicator:indeterminate:hover { 35 | image: url(:assets/repeat_off.png); 36 | } 37 | 38 | QCheckBox::indicator:indeterminate:pressed { 39 | image: url(:assets/repeat_off_p.png); 40 | } 41 | -------------------------------------------------------------------------------- /styles/controlbuttonswidget.shuffleButton.1x.qss: -------------------------------------------------------------------------------- 1 | QCheckBox { 2 | spacing: 0px; 3 | } 4 | 5 | QCheckBox::indicator { 6 | width: 47px; 7 | height: 15px; 8 | } 9 | 10 | QCheckBox::indicator:unchecked { 11 | image: url(:assets/shuffle_off.png); 12 | } 13 | 14 | QCheckBox::indicator:unchecked:hover { 15 | image: url(:assets/shuffle_off.png); 16 | } 17 | 18 | QCheckBox::indicator:unchecked:pressed { 19 | image: url(:assets/shuffle_off_p.png); 20 | } 21 | 22 | QCheckBox::indicator:checked { 23 | image: url(:assets/shuffle_on.png); 24 | } 25 | 26 | QCheckBox::indicator:checked:hover { 27 | image: url(:assets/shuffle_on.png); 28 | } 29 | 30 | QCheckBox::indicator:checked:pressed { 31 | image: url(:assets/shuffle_on_p.png); 32 | } 33 | 34 | QCheckBox::indicator:indeterminate:hover { 35 | image: url(:assets/shuffle_off.png); 36 | } 37 | 38 | QCheckBox::indicator:indeterminate:pressed { 39 | image: url(:assets/shuffle_off_p.png); 40 | } 41 | -------------------------------------------------------------------------------- /styles/controlbuttonswidget.shuffleButton.2x.qss: -------------------------------------------------------------------------------- 1 | QCheckBox { 2 | spacing: 0px; 3 | } 4 | 5 | QCheckBox::indicator { 6 | width: 94px; 7 | height: 30px; 8 | } 9 | 10 | QCheckBox::indicator:unchecked { 11 | image: url(:assets/shuffle_off.png); 12 | } 13 | 14 | QCheckBox::indicator:unchecked:hover { 15 | image: url(:assets/shuffle_off.png); 16 | } 17 | 18 | QCheckBox::indicator:unchecked:pressed { 19 | image: url(:assets/shuffle_off_p.png); 20 | } 21 | 22 | QCheckBox::indicator:checked { 23 | image: url(:assets/shuffle_on.png); 24 | } 25 | 26 | QCheckBox::indicator:checked:hover { 27 | image: url(:assets/shuffle_on.png); 28 | } 29 | 30 | QCheckBox::indicator:checked:pressed { 31 | image: url(:assets/shuffle_on_p.png); 32 | } 33 | 34 | QCheckBox::indicator:indeterminate:hover { 35 | image: url(:assets/shuffle_off.png); 36 | } 37 | 38 | QCheckBox::indicator:indeterminate:pressed { 39 | image: url(:assets/shuffle_off_p.png); 40 | } 41 | -------------------------------------------------------------------------------- /styles/controlbuttonswidget.shuffleButton.3x.qss: -------------------------------------------------------------------------------- 1 | QCheckBox { 2 | spacing: 0px; 3 | } 4 | 5 | QCheckBox::indicator { 6 | width: 141px; 7 | height: 45px; 8 | } 9 | 10 | QCheckBox::indicator:unchecked { 11 | image: url(:assets/shuffle_off.png); 12 | } 13 | 14 | QCheckBox::indicator:unchecked:hover { 15 | image: url(:assets/shuffle_off.png); 16 | } 17 | 18 | QCheckBox::indicator:unchecked:pressed { 19 | image: url(:assets/shuffle_off_p.png); 20 | } 21 | 22 | QCheckBox::indicator:checked { 23 | image: url(:assets/shuffle_on.png); 24 | } 25 | 26 | QCheckBox::indicator:checked:hover { 27 | image: url(:assets/shuffle_on.png); 28 | } 29 | 30 | QCheckBox::indicator:checked:pressed { 31 | image: url(:assets/shuffle_on_p.png); 32 | } 33 | 34 | QCheckBox::indicator:indeterminate:hover { 35 | image: url(:assets/shuffle_off.png); 36 | } 37 | 38 | QCheckBox::indicator:indeterminate:pressed { 39 | image: url(:assets/shuffle_off_p.png); 40 | } 41 | -------------------------------------------------------------------------------- /styles/controlbuttonswidget.shuffleButton.4x.qss: -------------------------------------------------------------------------------- 1 | QCheckBox { 2 | spacing: 0px; 3 | } 4 | 5 | QCheckBox::indicator { 6 | width: 188px; 7 | height: 60px; 8 | } 9 | 10 | QCheckBox::indicator:unchecked { 11 | image: url(:assets/shuffle_off.png); 12 | } 13 | 14 | QCheckBox::indicator:unchecked:hover { 15 | image: url(:assets/shuffle_off.png); 16 | } 17 | 18 | QCheckBox::indicator:unchecked:pressed { 19 | image: url(:assets/shuffle_off_p.png); 20 | } 21 | 22 | QCheckBox::indicator:checked { 23 | image: url(:assets/shuffle_on.png); 24 | } 25 | 26 | QCheckBox::indicator:checked:hover { 27 | image: url(:assets/shuffle_on.png); 28 | } 29 | 30 | QCheckBox::indicator:checked:pressed { 31 | image: url(:assets/shuffle_on_p.png); 32 | } 33 | 34 | QCheckBox::indicator:indeterminate:hover { 35 | image: url(:assets/shuffle_off.png); 36 | } 37 | 38 | QCheckBox::indicator:indeterminate:pressed { 39 | image: url(:assets/shuffle_off_p.png); 40 | } 41 | -------------------------------------------------------------------------------- /styles/desktopbasewindow.1x.qss: -------------------------------------------------------------------------------- 1 | #DesktopBaseWindow { 2 | border: 1px solid #0d0d14; 3 | background-color: #333350; 4 | } 5 | 6 | #outerFrame { 7 | border-top: 1px solid #5c5c63; 8 | border-left: 1px solid #5c5c63; 9 | border-bottom: 1px solid #25253a; 10 | border-right: 1px solid #25253a; 11 | } 12 | 13 | #bodyOuterFrame { 14 | border-top: 1px solid #25253a; 15 | border-left: 1px solid #25253a; 16 | border-bottom: 1px solid #5c5c63; 17 | border-right: 1px solid #5c5c63; 18 | background-color: #25253a; 19 | } 20 | 21 | #bodyInnerFrame { 22 | border-top: 1px solid #5c5c63; 23 | border-left: 1px solid #5c5c63; 24 | border-bottom: 1px solid #25253a; 25 | border-right: 1px solid #25253a; 26 | background-color: #333350; 27 | } 28 | -------------------------------------------------------------------------------- /styles/desktopbasewindow.2x.qss: -------------------------------------------------------------------------------- 1 | #DesktopBaseWindow { 2 | border: 2px solid #0d0d14; 3 | background-color: #333350; 4 | } 5 | 6 | #outerFrame { 7 | border-top: 2px solid #5c5c63; 8 | border-left: 2px solid #5c5c63; 9 | border-bottom: 2px solid #25253a; 10 | border-right: 2px solid #25253a; 11 | } 12 | 13 | #bodyOuterFrame { 14 | border-top: 2px solid #25253a; 15 | border-left: 2px solid #25253a; 16 | border-bottom: 2px solid #5c5c63; 17 | border-right: 2px solid #5c5c63; 18 | background-color: #25253a; 19 | } 20 | 21 | #bodyInnerFrame { 22 | border-top: 2px solid #5c5c63; 23 | border-left: 2px solid #5c5c63; 24 | border-bottom: 2px solid #25253a; 25 | border-right: 2px solid #25253a; 26 | background-color: #333350; 27 | } 28 | -------------------------------------------------------------------------------- /styles/desktopbasewindow.3x.qss: -------------------------------------------------------------------------------- 1 | #DesktopBaseWindow { 2 | border: 3px solid #0d0d14; 3 | background-color: #333350; 4 | } 5 | 6 | #outerFrame { 7 | border-top: 3px solid #5c5c63; 8 | border-left: 3px solid #5c5c63; 9 | border-bottom: 3px solid #25253a; 10 | border-right: 3px solid #25253a; 11 | } 12 | 13 | #bodyOuterFrame { 14 | border-top: 3px solid #25253a; 15 | border-left: 3px solid #25253a; 16 | border-bottom: 3px solid #5c5c63; 17 | border-right: 3px solid #5c5c63; 18 | background-color: #25253a; 19 | } 20 | 21 | #bodyInnerFrame { 22 | border-top: 3px solid #5c5c63; 23 | border-left: 3px solid #5c5c63; 24 | border-bottom: 3px solid #25253a; 25 | border-right: 3px solid #25253a; 26 | background-color: #333350; 27 | } 28 | -------------------------------------------------------------------------------- /styles/desktopbasewindow.4x.qss: -------------------------------------------------------------------------------- 1 | #DesktopBaseWindow { 2 | border: 4px solid #0d0d14; 3 | background-color: #333350; 4 | } 5 | 6 | #outerFrame { 7 | border-top: 4px solid #5c5c63; 8 | border-left: 4px solid #5c5c63; 9 | border-bottom: 4px solid #25253a; 10 | border-right: 4px solid #25253a; 11 | } 12 | 13 | #bodyOuterFrame { 14 | border-top: 4px solid #25253a; 15 | border-left: 4px solid #25253a; 16 | border-bottom: 4px solid #5c5c63; 17 | border-right: 4px solid #5c5c63; 18 | background-color: #25253a; 19 | } 20 | 21 | #bodyInnerFrame { 22 | border-top: 4px solid #5c5c63; 23 | border-left: 4px solid #5c5c63; 24 | border-bottom: 4px solid #25253a; 25 | border-right: 4px solid #25253a; 26 | background-color: #333350; 27 | } 28 | -------------------------------------------------------------------------------- /styles/playerview.balanceSlider.1x.qss: -------------------------------------------------------------------------------- 1 | QSlider::handle:horizontal { 2 | width: 14px; 3 | height: 11px; 4 | background-color: transparent; 5 | border-image: url(:assets/balanceHandle.png); 6 | background: none; 7 | background-repeat: none; 8 | margin-top: -2.5px; 9 | margin-bottom: -2.5px; 10 | } 11 | 12 | QSlider::handle:horizontal:pressed { 13 | border-image: url(:assets/balanceHandle_p.png); 14 | } 15 | 16 | QSlider::groove:horizontal { 17 | background: transparent; 18 | height: 5px; 19 | } 20 | 21 | QSlider::sub-page:horizontal { 22 | background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1, stop: 0 #1c6c14, stop: 1 #28991c); 23 | border-top: 0.66px solid #161623; 24 | border-left: 0.66px solid #161623; 25 | border-bottom: 0.66px solid #7d7d92; 26 | border-right: 0.66px solid #7d7d92; 27 | border-radius: 0.66px; 28 | } 29 | 30 | QSlider::add-page:horizontal { 31 | height: 5px; 32 | background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1, stop: 0 #1c6c14, stop: 1 #28991c); 33 | border-top: 0.66px solid #161623; 34 | border-left: 0.66px solid #161623; 35 | border-bottom: 0.66px solid #7d7d92; 36 | border-right: 0.66px solid #7d7d92; 37 | border-radius: 0.66px; 38 | } 39 | -------------------------------------------------------------------------------- /styles/playerview.balanceSlider.2x.qss: -------------------------------------------------------------------------------- 1 | QSlider::handle:horizontal { 2 | width: 24px; 3 | height: 22px; 4 | background-color: transparent; 5 | border-image: url(:assets/balanceHandle.png); 6 | background: none; 7 | background-repeat: none; 8 | margin-top: -5px; 9 | margin-bottom: -5px; 10 | } 11 | 12 | QSlider::handle:horizontal:pressed { 13 | border-image: url(:assets/balanceHandle_p.png); 14 | } 15 | 16 | QSlider::groove:horizontal { 17 | background: transparent; 18 | height: 10px; 19 | } 20 | 21 | QSlider::sub-page:horizontal { 22 | background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1, stop: 0 #1c6c14, stop: 1 #28991c); 23 | border-top: 1.32px solid #161623; 24 | border-left: 1.32px solid #161623; 25 | border-bottom: 1.32px solid #7d7d92; 26 | border-right: 1.32px solid #7d7d92; 27 | border-radius: 1.32px; 28 | } 29 | 30 | QSlider::add-page:horizontal { 31 | height: 10px; 32 | background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1, stop: 0 #1c6c14, stop: 1 #28991c); 33 | border-top: 1.32px solid #161623; 34 | border-left: 1.32px solid #161623; 35 | border-bottom: 1.32px solid #7d7d92; 36 | border-right: 1.32px solid #7d7d92; 37 | border-radius: 1.32px; 38 | } 39 | -------------------------------------------------------------------------------- /styles/playerview.balanceSlider.3x.qss: -------------------------------------------------------------------------------- 1 | QSlider::handle:horizontal { 2 | width: 42px; 3 | height: 33px; 4 | background-color: transparent; 5 | border-image: url(:assets/balanceHandle.png); 6 | background: none; 7 | background-repeat: none; 8 | margin-top: -7.5px; 9 | margin-bottom: -7.5px; 10 | } 11 | 12 | QSlider::handle:horizontal:pressed { 13 | border-image: url(:assets/balanceHandle_p.png); 14 | } 15 | 16 | QSlider::groove:horizontal { 17 | background: transparent; 18 | height: 15px; 19 | } 20 | 21 | QSlider::sub-page:horizontal { 22 | background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1, stop: 0 #1c6c14, stop: 1 #28991c); 23 | border-top: 2px solid #161623; 24 | border-left: 2px solid #161623; 25 | border-bottom: 2px solid #7d7d92; 26 | border-right: 2px solid #7d7d92; 27 | border-radius: 2px; 28 | } 29 | 30 | QSlider::add-page:horizontal { 31 | height: 15px; 32 | background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1, stop: 0 #1c6c14, stop: 1 #28991c); 33 | border-top: 2px solid #161623; 34 | border-left: 2px solid #161623; 35 | border-bottom: 2px solid #7d7d92; 36 | border-right: 2px solid #7d7d92; 37 | border-radius: 2px; 38 | } 39 | -------------------------------------------------------------------------------- /styles/playerview.balanceSlider.4x.qss: -------------------------------------------------------------------------------- 1 | QSlider::handle:horizontal { 2 | width: 56px; 3 | height: 44px; 4 | background-color: transparent; 5 | border-image: url(:assets/balanceHandle.png); 6 | background: none; 7 | background-repeat: none; 8 | margin-top: -10px; 9 | margin-bottom: -10px; 10 | } 11 | 12 | QSlider::handle:horizontal:pressed { 13 | border-image: url(:assets/balanceHandle_p.png); 14 | } 15 | 16 | QSlider::groove:horizontal { 17 | background: transparent; 18 | height: 20px; 19 | } 20 | 21 | QSlider::sub-page:horizontal { 22 | background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1, stop: 0 #1c6c14, stop: 1 #28991c); 23 | border-top: 2.64px solid #161623; 24 | border-left: 2.64px solid #161623; 25 | border-bottom: 2.64px solid #7d7d92; 26 | border-right: 2.64px solid #7d7d92; 27 | border-radius: 2.64px; 28 | } 29 | 30 | QSlider::add-page:horizontal { 31 | height: 20px; 32 | background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1, stop: 0 #1c6c14, stop: 1 #28991c); 33 | border-top: 2.64px solid #161623; 34 | border-left: 2.64px solid #161623; 35 | border-bottom: 2.64px solid #7d7d92; 36 | border-right: 2.64px solid #7d7d92; 37 | border-radius: 2.64px; 38 | } 39 | -------------------------------------------------------------------------------- /styles/playerview.codecDetailsContainer.1x.qss: -------------------------------------------------------------------------------- 1 | #kHzLabel { 2 | font-size: 5pt; 3 | } 4 | 5 | #kbpsLabel { 6 | font-size: 5pt; 7 | } 8 | 9 | #monoLabel { 10 | font-size: 4pt; 11 | } 12 | 13 | #stereoLabel { 14 | font-size: 4pt; 15 | } 16 | -------------------------------------------------------------------------------- /styles/playerview.codecDetailsContainer.2x.qss: -------------------------------------------------------------------------------- 1 | #kHzLabel { 2 | font-size: 10pt; 3 | } 4 | 5 | #kbpsLabel { 6 | font-size: 10pt; 7 | } 8 | 9 | #monoLabel { 10 | font-size: 8pt; 11 | } 12 | 13 | #stereoLabel { 14 | font-size: 8pt; 15 | } 16 | -------------------------------------------------------------------------------- /styles/playerview.codecDetailsContainer.3x.qss: -------------------------------------------------------------------------------- 1 | #kHzLabel { 2 | font-size: 15pt; 3 | } 4 | 5 | #kbpsLabel { 6 | font-size: 15pt; 7 | } 8 | 9 | #monoLabel { 10 | font-size: 11pt; 11 | } 12 | 13 | #stereoLabel { 14 | font-size: 11pt; 15 | } 16 | -------------------------------------------------------------------------------- /styles/playerview.codecDetailsContainer.4x.qss: -------------------------------------------------------------------------------- 1 | #kHzLabel { 2 | font-size: 20pt; 3 | } 4 | 5 | #kbpsLabel { 6 | font-size: 20pt; 7 | } 8 | 9 | #monoLabel { 10 | font-size: 16pt; 11 | } 12 | 13 | #stereoLabel { 14 | font-size: 16pt; 15 | } 16 | -------------------------------------------------------------------------------- /styles/playerview.eqButton.1x.qss: -------------------------------------------------------------------------------- 1 | QCheckBox { 2 | spacing: 0px; 3 | } 4 | 5 | QCheckBox::indicator { 6 | width: 23px; 7 | height: 12px; 8 | } 9 | 10 | QCheckBox::indicator:unchecked { 11 | image: url(:assets/eq_off.png); 12 | } 13 | 14 | QCheckBox::indicator:unchecked:hover { 15 | image: url(:assets/eq_off.png); 16 | } 17 | 18 | QCheckBox::indicator:unchecked:pressed { 19 | image: url(:assets/eq_off_p.png); 20 | } 21 | 22 | QCheckBox::indicator:checked { 23 | image: url(:assets/eq_on.png); 24 | } 25 | 26 | QCheckBox::indicator:checked:hover { 27 | image: url(:assets/eq_on.png); 28 | } 29 | 30 | QCheckBox::indicator:checked:pressed { 31 | image: url(:assets/eq_on_p.png); 32 | } 33 | 34 | QCheckBox::indicator:indeterminate:hover { 35 | image: url(:assets/eq_off.png); 36 | } 37 | 38 | QCheckBox::indicator:indeterminate:pressed { 39 | image: url(:assets/eq_off_p.png); 40 | } 41 | -------------------------------------------------------------------------------- /styles/playerview.eqButton.2x.qss: -------------------------------------------------------------------------------- 1 | QCheckBox { 2 | spacing: 0px; 3 | } 4 | 5 | QCheckBox::indicator { 6 | width: 46px; 7 | height: 24px; 8 | } 9 | 10 | QCheckBox::indicator:unchecked { 11 | image: url(:assets/eq_off.png); 12 | } 13 | 14 | QCheckBox::indicator:unchecked:hover { 15 | image: url(:assets/eq_off.png); 16 | } 17 | 18 | QCheckBox::indicator:unchecked:pressed { 19 | image: url(:assets/eq_off_p.png); 20 | } 21 | 22 | QCheckBox::indicator:checked { 23 | image: url(:assets/eq_on.png); 24 | } 25 | 26 | QCheckBox::indicator:checked:hover { 27 | image: url(:assets/eq_on.png); 28 | } 29 | 30 | QCheckBox::indicator:checked:pressed { 31 | image: url(:assets/eq_on_p.png); 32 | } 33 | 34 | QCheckBox::indicator:indeterminate:hover { 35 | image: url(:assets/eq_off.png); 36 | } 37 | 38 | QCheckBox::indicator:indeterminate:pressed { 39 | image: url(:assets/eq_off_p.png); 40 | } 41 | -------------------------------------------------------------------------------- /styles/playerview.eqButton.3x.qss: -------------------------------------------------------------------------------- 1 | QCheckBox { 2 | spacing: 0px; 3 | } 4 | 5 | QCheckBox::indicator { 6 | width: 69px; 7 | height: 36px; 8 | } 9 | 10 | QCheckBox::indicator:unchecked { 11 | image: url(:assets/eq_off.png); 12 | } 13 | 14 | QCheckBox::indicator:unchecked:hover { 15 | image: url(:assets/eq_off.png); 16 | } 17 | 18 | QCheckBox::indicator:unchecked:pressed { 19 | image: url(:assets/eq_off_p.png); 20 | } 21 | 22 | QCheckBox::indicator:checked { 23 | image: url(:assets/eq_on.png); 24 | } 25 | 26 | QCheckBox::indicator:checked:hover { 27 | image: url(:assets/eq_on.png); 28 | } 29 | 30 | QCheckBox::indicator:checked:pressed { 31 | image: url(:assets/eq_on_p.png); 32 | } 33 | 34 | QCheckBox::indicator:indeterminate:hover { 35 | image: url(:assets/eq_off.png); 36 | } 37 | 38 | QCheckBox::indicator:indeterminate:pressed { 39 | image: url(:assets/eq_off_p.png); 40 | } 41 | -------------------------------------------------------------------------------- /styles/playerview.eqButton.4x.qss: -------------------------------------------------------------------------------- 1 | QCheckBox { 2 | spacing: 0px; 3 | } 4 | 5 | QCheckBox::indicator { 6 | width: 92px; 7 | height: 48px; 8 | } 9 | 10 | QCheckBox::indicator:unchecked { 11 | image: url(:assets/eq_off.png); 12 | } 13 | 14 | QCheckBox::indicator:unchecked:hover { 15 | image: url(:assets/eq_off.png); 16 | } 17 | 18 | QCheckBox::indicator:unchecked:pressed { 19 | image: url(:assets/eq_off_p.png); 20 | } 21 | 22 | QCheckBox::indicator:checked { 23 | image: url(:assets/eq_on.png); 24 | } 25 | 26 | QCheckBox::indicator:checked:hover { 27 | image: url(:assets/eq_on.png); 28 | } 29 | 30 | QCheckBox::indicator:checked:pressed { 31 | image: url(:assets/eq_on_p.png); 32 | } 33 | 34 | QCheckBox::indicator:indeterminate:hover { 35 | image: url(:assets/eq_off.png); 36 | } 37 | 38 | QCheckBox::indicator:indeterminate:pressed { 39 | image: url(:assets/eq_off_p.png); 40 | } 41 | -------------------------------------------------------------------------------- /styles/playerview.kbpsFrame.1x.qss: -------------------------------------------------------------------------------- 1 | #kbpsFrame { 2 | background-color: #000000; 3 | border-top: 1px solid #26253c; 4 | border-right: 1px solid #6d6d7f; 5 | border-bottom: 1px solid #6d6d7f; 6 | border-left: 1px solid #26253c; 7 | } 8 | 9 | #kbpsValueLabel { 10 | font-size: 6pt; 11 | } 12 | -------------------------------------------------------------------------------- /styles/playerview.kbpsFrame.2x.qss: -------------------------------------------------------------------------------- 1 | #kbpsFrame { 2 | background-color: #000000; 3 | border-top: 2px solid #26253c; 4 | border-right: 2px solid #6d6d7f; 5 | border-bottom: 2px solid #6d6d7f; 6 | border-left: 2px solid #26253c; 7 | } 8 | 9 | #kbpsValueLabel { 10 | font-size: 12pt; 11 | } 12 | -------------------------------------------------------------------------------- /styles/playerview.kbpsFrame.3x.qss: -------------------------------------------------------------------------------- 1 | #kbpsFrame { 2 | background-color: #000000; 3 | border-top: 3px solid #26253c; 4 | border-right: 3px solid #6d6d7f; 5 | border-bottom: 3px solid #6d6d7f; 6 | border-left: 3px solid #26253c; 7 | } 8 | 9 | #kbpsValueLabel { 10 | font-size: 18pt; 11 | } 12 | -------------------------------------------------------------------------------- /styles/playerview.kbpsFrame.4x.qss: -------------------------------------------------------------------------------- 1 | #kbpsFrame { 2 | background-color: #000000; 3 | border-top: 4px solid #26253c; 4 | border-right: 4px solid #6d6d7f; 5 | border-bottom: 4px solid #6d6d7f; 6 | border-left: 4px solid #26253c; 7 | } 8 | 9 | #kbpsValueLabel { 10 | font-size: 24pt; 11 | } 12 | -------------------------------------------------------------------------------- /styles/playerview.khzFrame.1x.qss: -------------------------------------------------------------------------------- 1 | #khzFrame { 2 | background-color: #000000; 3 | border-top: 1px solid #26253c; 4 | border-right: 1px solid #6d6d7f; 5 | border-bottom: 1px solid #6d6d7f; 6 | border-left: 1px solid #26253c; 7 | } 8 | 9 | #khzValueLabel { 10 | font-size: 6pt; 11 | } 12 | -------------------------------------------------------------------------------- /styles/playerview.khzFrame.2x.qss: -------------------------------------------------------------------------------- 1 | #khzFrame { 2 | background-color: #000000; 3 | border-top: 2px solid #26253c; 4 | border-right: 2px solid #6d6d7f; 5 | border-bottom: 2px solid #6d6d7f; 6 | border-left: 2px solid #26253c; 7 | } 8 | 9 | #khzValueLabel { 10 | font-size: 12pt; 11 | } 12 | -------------------------------------------------------------------------------- /styles/playerview.khzFrame.3x.qss: -------------------------------------------------------------------------------- 1 | #khzFrame { 2 | background-color: #000000; 3 | border-top: 3px solid #26253c; 4 | border-right: 3px solid #6d6d7f; 5 | border-bottom: 3px solid #6d6d7f; 6 | border-left: 3px solid #26253c; 7 | } 8 | 9 | #khzValueLabel { 10 | font-size: 18pt; 11 | } 12 | -------------------------------------------------------------------------------- /styles/playerview.khzFrame.4x.qss: -------------------------------------------------------------------------------- 1 | #khzFrame { 2 | background-color: #000000; 3 | border-top: 4px solid #26253c; 4 | border-right: 4px solid #6d6d7f; 5 | border-bottom: 4px solid #6d6d7f; 6 | border-left: 4px solid #26253c; 7 | } 8 | 9 | #khzValueLabel { 10 | font-size: 24pt; 11 | } 12 | -------------------------------------------------------------------------------- /styles/playerview.playlistButton.1x.qss: -------------------------------------------------------------------------------- 1 | QCheckBox { 2 | spacing: 0px; 3 | } 4 | 5 | QCheckBox::indicator { 6 | width: 23px; 7 | height: 12px; 8 | } 9 | 10 | QCheckBox::indicator:unchecked { 11 | image: url(:assets/pl_off.png); 12 | } 13 | 14 | QCheckBox::indicator:unchecked:hover { 15 | image: url(:assets/pl_off.png); 16 | } 17 | 18 | QCheckBox::indicator:unchecked:pressed { 19 | image: url(:assets/pl_off_p.png); 20 | } 21 | 22 | QCheckBox::indicator:checked { 23 | image: url(:assets/pl_on.png); 24 | } 25 | 26 | QCheckBox::indicator:checked:hover { 27 | image: url(:assets/pl_on.png); 28 | } 29 | 30 | QCheckBox::indicator:checked:pressed { 31 | image: url(:assets/pl_on_p.png); 32 | } 33 | 34 | QCheckBox::indicator:indeterminate:hover { 35 | image: url(:assets/pl_on.png); 36 | } 37 | 38 | QCheckBox::indicator:indeterminate:pressed { 39 | image: url(:assets/pl_on_p.png); 40 | } 41 | -------------------------------------------------------------------------------- /styles/playerview.playlistButton.2x.qss: -------------------------------------------------------------------------------- 1 | QCheckBox { 2 | spacing: 0px; 3 | } 4 | 5 | QCheckBox::indicator { 6 | width: 46px; 7 | height: 24px; 8 | } 9 | 10 | QCheckBox::indicator:unchecked { 11 | image: url(:assets/pl_off.png); 12 | } 13 | 14 | QCheckBox::indicator:unchecked:hover { 15 | image: url(:assets/pl_off.png); 16 | } 17 | 18 | QCheckBox::indicator:unchecked:pressed { 19 | image: url(:assets/pl_off_p.png); 20 | } 21 | 22 | QCheckBox::indicator:checked { 23 | image: url(:assets/pl_on.png); 24 | } 25 | 26 | QCheckBox::indicator:checked:hover { 27 | image: url(:assets/pl_on.png); 28 | } 29 | 30 | QCheckBox::indicator:checked:pressed { 31 | image: url(:assets/pl_on_p.png); 32 | } 33 | 34 | QCheckBox::indicator:indeterminate:hover { 35 | image: url(:assets/pl_on.png); 36 | } 37 | 38 | QCheckBox::indicator:indeterminate:pressed { 39 | image: url(:assets/pl_on_p.png); 40 | } 41 | -------------------------------------------------------------------------------- /styles/playerview.playlistButton.3x.qss: -------------------------------------------------------------------------------- 1 | QCheckBox { 2 | spacing: 0px; 3 | } 4 | 5 | QCheckBox::indicator { 6 | width: 69px; 7 | height: 36px; 8 | } 9 | 10 | QCheckBox::indicator:unchecked { 11 | image: url(:assets/pl_off.png); 12 | } 13 | 14 | QCheckBox::indicator:unchecked:hover { 15 | image: url(:assets/pl_off.png); 16 | } 17 | 18 | QCheckBox::indicator:unchecked:pressed { 19 | image: url(:assets/pl_off_p.png); 20 | } 21 | 22 | QCheckBox::indicator:checked { 23 | image: url(:assets/pl_on.png); 24 | } 25 | 26 | QCheckBox::indicator:checked:hover { 27 | image: url(:assets/pl_on.png); 28 | } 29 | 30 | QCheckBox::indicator:checked:pressed { 31 | image: url(:assets/pl_on_p.png); 32 | } 33 | 34 | QCheckBox::indicator:indeterminate:hover { 35 | image: url(:assets/pl_on.png); 36 | } 37 | 38 | QCheckBox::indicator:indeterminate:pressed { 39 | image: url(:assets/pl_on_p.png); 40 | } 41 | -------------------------------------------------------------------------------- /styles/playerview.playlistButton.4x.qss: -------------------------------------------------------------------------------- 1 | QCheckBox { 2 | spacing: 0px; 3 | } 4 | 5 | QCheckBox::indicator { 6 | width: 92px; 7 | height: 48px; 8 | } 9 | 10 | QCheckBox::indicator:unchecked { 11 | image: url(:assets/pl_off.png); 12 | } 13 | 14 | QCheckBox::indicator:unchecked:hover { 15 | image: url(:assets/pl_off.png); 16 | } 17 | 18 | QCheckBox::indicator:unchecked:pressed { 19 | image: url(:assets/pl_off_p.png); 20 | } 21 | 22 | QCheckBox::indicator:checked { 23 | image: url(:assets/pl_on.png); 24 | } 25 | 26 | QCheckBox::indicator:checked:hover { 27 | image: url(:assets/pl_on.png); 28 | } 29 | 30 | QCheckBox::indicator:checked:pressed { 31 | image: url(:assets/pl_on_p.png); 32 | } 33 | 34 | QCheckBox::indicator:indeterminate:hover { 35 | image: url(:assets/pl_on.png); 36 | } 37 | 38 | QCheckBox::indicator:indeterminate:pressed { 39 | image: url(:assets/pl_on_p.png); 40 | } 41 | -------------------------------------------------------------------------------- /styles/playerview.posBar.1x.qss: -------------------------------------------------------------------------------- 1 | QSlider::handle:horizontal { 2 | width: 28px; 3 | height: 10px; 4 | background-color: transparent; 5 | border-image: url(:assets/posHandle.png); 6 | background: none; 7 | background-repeat: none; 8 | } 9 | 10 | QSlider::handle:horizontal:pressed { 11 | border-image: url(:assets/posHandle_p.png); 12 | } 13 | 14 | QSlider::groove:horizontal { 15 | background: transparent; 16 | } 17 | 18 | #posBar { 19 | background-color: #2c2b43; 20 | border-top: 2px solid #26253c; 21 | border-right: 1px solid #6d6d7f; 22 | border-bottom: 1px solid #6d6d7f; 23 | border-left: 2px solid #26253c; 24 | padding-top: -2px; 25 | padding-right: -1px; 26 | padding-bottom: -1px; 27 | padding-left: -2px; 28 | } 29 | -------------------------------------------------------------------------------- /styles/playerview.posBar.2x.qss: -------------------------------------------------------------------------------- 1 | QSlider::handle:horizontal { 2 | width: 56px; 3 | height: 20px; 4 | background-color: transparent; 5 | border-image: url(:assets/posHandle.png); 6 | background: none; 7 | background-repeat: none; 8 | } 9 | 10 | QSlider::handle:horizontal:pressed { 11 | border-image: url(:assets/posHandle_p.png); 12 | } 13 | 14 | QSlider::groove:horizontal { 15 | background: transparent; 16 | } 17 | 18 | #posBar { 19 | background-color: #2c2b43; 20 | border-top: 4px solid #26253c; 21 | border-right: 2px solid #6d6d7f; 22 | border-bottom: 2px solid #6d6d7f; 23 | border-left: 4px solid #26253c; 24 | padding-top: -4px; 25 | padding-right: -2px; 26 | padding-bottom: -2px; 27 | padding-left: -4px; 28 | } 29 | -------------------------------------------------------------------------------- /styles/playerview.posBar.3x.qss: -------------------------------------------------------------------------------- 1 | QSlider::handle:horizontal { 2 | width: 84px; 3 | height: 30px; 4 | background-color: transparent; 5 | border-image: url(:assets/posHandle.png); 6 | background: none; 7 | background-repeat: none; 8 | } 9 | 10 | QSlider::handle:horizontal:pressed { 11 | border-image: url(:assets/posHandle_p.png); 12 | } 13 | 14 | QSlider::groove:horizontal { 15 | background: transparent; 16 | } 17 | 18 | #posBar { 19 | background-color: #2c2b43; 20 | border-top: 6px solid #26253c; 21 | border-right: 3px solid #6d6d7f; 22 | border-bottom: 3px solid #6d6d7f; 23 | border-left: 6px solid #26253c; 24 | padding-top: -6px; 25 | padding-right: -3px; 26 | padding-bottom: -3px; 27 | padding-left: -6px; 28 | } 29 | -------------------------------------------------------------------------------- /styles/playerview.posBar.4x.qss: -------------------------------------------------------------------------------- 1 | QSlider::handle:horizontal { 2 | width: 112px; 3 | height: 40px; 4 | background-color: transparent; 5 | border-image: url(:assets/posHandle.png); 6 | background: none; 7 | background-repeat: none; 8 | } 9 | 10 | QSlider::handle:horizontal:pressed { 11 | border-image: url(:assets/posHandle_p.png); 12 | } 13 | 14 | QSlider::groove:horizontal { 15 | background: transparent; 16 | } 17 | 18 | #posBar { 19 | background-color: #2c2b43; 20 | border-top: 8px solid #26253c; 21 | border-right: 4px solid #6d6d7f; 22 | border-bottom: 4px solid #6d6d7f; 23 | border-left: 8px solid #26253c; 24 | padding-top: -8px; 25 | padding-right: -4px; 26 | padding-bottom: -4px; 27 | padding-left: -8px; 28 | } 29 | -------------------------------------------------------------------------------- /styles/playerview.songInfoContainer.1x.qss: -------------------------------------------------------------------------------- 1 | #songInfoContainer { 2 | background-color: #000000; 3 | border-top: 1px solid #26253c; 4 | border-right: 1px solid #6d6d7f; 5 | border-bottom: 1px solid #6d6d7f; 6 | border-left: 1px solid #26253c; 7 | } 8 | 9 | #songInfoLabel { 10 | font-size: 6pt; 11 | } 12 | -------------------------------------------------------------------------------- /styles/playerview.songInfoContainer.2x.qss: -------------------------------------------------------------------------------- 1 | #songInfoContainer { 2 | background-color: #000000; 3 | border-top: 2px solid #26253c; 4 | border-right: 2px solid #6d6d7f; 5 | border-bottom: 2px solid #6d6d7f; 6 | border-left: 2px solid #26253c; 7 | } 8 | 9 | #songInfoLabel { 10 | font-size: 12pt; 11 | } 12 | -------------------------------------------------------------------------------- /styles/playerview.songInfoContainer.3x.qss: -------------------------------------------------------------------------------- 1 | #songInfoContainer { 2 | background-color: #000000; 3 | border-top: 3px solid #26253c; 4 | border-right: 3px solid #6d6d7f; 5 | border-bottom: 3px solid #6d6d7f; 6 | border-left: 3px solid #26253c; 7 | } 8 | 9 | #songInfoLabel { 10 | font-size: 18pt; 11 | } 12 | -------------------------------------------------------------------------------- /styles/playerview.songInfoContainer.4x.qss: -------------------------------------------------------------------------------- 1 | #songInfoContainer { 2 | background-color: #000000; 3 | border-top: 4px solid #26253c; 4 | border-right: 4px solid #6d6d7f; 5 | border-bottom: 4px solid #6d6d7f; 6 | border-left: 4px solid #26253c; 7 | } 8 | 9 | #songInfoLabel { 10 | font-size: 24pt; 11 | } 12 | -------------------------------------------------------------------------------- /styles/playerview.visualizationFrame.1x.qss: -------------------------------------------------------------------------------- 1 | #visualizationFrame { 2 | background-color: #000; 3 | border-top: 1px solid #26253c; 4 | border-right: 1px solid #6d6d7f; 5 | border-bottom: 1px solid #6d6d7f; 6 | border-left: 1px solid #26253c; 7 | } 8 | 9 | #progressTimeLabel { 10 | font-size: 10pt; 11 | font-family: "bignumbers"; 12 | letter-spacing: 8px; 13 | color: #00f800; 14 | } 15 | -------------------------------------------------------------------------------- /styles/playerview.visualizationFrame.2x.qss: -------------------------------------------------------------------------------- 1 | #visualizationFrame { 2 | background-color: #000; 3 | border-top: 2px solid #26253c; 4 | border-right: 2px solid #6d6d7f; 5 | border-bottom: 2px solid #6d6d7f; 6 | border-left: 2px solid #26253c; 7 | } 8 | 9 | #progressTimeLabel { 10 | font-size: 20pt; 11 | font-family: "bignumbers"; 12 | letter-spacing: 8px; 13 | color: #00f800; 14 | } 15 | -------------------------------------------------------------------------------- /styles/playerview.visualizationFrame.3x.qss: -------------------------------------------------------------------------------- 1 | #visualizationFrame { 2 | background-color: #000; 3 | border-top: 3px solid #26253c; 4 | border-right: 3px solid #6d6d7f; 5 | border-bottom: 3px solid #6d6d7f; 6 | border-left: 3px solid #26253c; 7 | } 8 | 9 | #progressTimeLabel { 10 | font-size: 29pt; 11 | font-family: "bignumbers"; 12 | letter-spacing: 8px; 13 | color: #00f800; 14 | } 15 | -------------------------------------------------------------------------------- /styles/playerview.visualizationFrame.4x.qss: -------------------------------------------------------------------------------- 1 | #visualizationFrame { 2 | background-color: #000; 3 | border-top: 4px solid #26253c; 4 | border-right: 4px solid #6d6d7f; 5 | border-bottom: 4px solid #6d6d7f; 6 | border-left: 4px solid #26253c; 7 | } 8 | 9 | #progressTimeLabel { 10 | font-size: 40pt; 11 | font-family: "bignumbers"; 12 | letter-spacing: 8px; 13 | color: #00f800; 14 | } 15 | -------------------------------------------------------------------------------- /styles/playerview.volumeSlider.1x.qss: -------------------------------------------------------------------------------- 1 | QSlider::handle:horizontal { 2 | width: 14px; 3 | height: 11px; 4 | background-color: transparent; 5 | border-image: url(:assets/volumeHandle.png); 6 | background: none; 7 | background-repeat: none; 8 | margin-top: -2.5px; 9 | margin-bottom: -2.5px; 10 | } 11 | 12 | QSlider::handle:horizontal:pressed { 13 | border-image: url(:assets/volumeHandle_p.png); 14 | } 15 | 16 | QSlider::groove:horizontal { 17 | background: transparent; 18 | height: 6px; 19 | } 20 | 21 | QSlider::sub-page:horizontal { 22 | background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1, stop: 0 #9f161b, stop: 1 #e11f26); 23 | border-top: 1px solid #161623; 24 | border-left: 1px solid #161623; 25 | border-bottom: 1px solid #7d7d92; 26 | border-right: 1px solid #7d7d92; 27 | border-radius: 2.66px; 28 | } 29 | 30 | QSlider::add-page:horizontal { 31 | height: 6px; 32 | background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1, stop: 0 #1c6c14, stop: 1 #28991c); 33 | border-top: 1px solid #161623; 34 | border-left: 1px solid #161623; 35 | border-bottom: 1px solid #7d7d92; 36 | border-right: 1px solid #7d7d92; 37 | border-radius: 2.66px; 38 | } 39 | -------------------------------------------------------------------------------- /styles/playerview.volumeSlider.2x.qss: -------------------------------------------------------------------------------- 1 | QSlider::handle:horizontal { 2 | width: 24px; 3 | height: 22px; 4 | background-color: transparent; 5 | border-image: url(:assets/volumeHandle.png); 6 | background: none; 7 | background-repeat: none; 8 | margin-top: -5px; 9 | margin-bottom: -5px; 10 | } 11 | 12 | QSlider::handle:horizontal:pressed { 13 | border-image: url(:assets/volumeHandle_p.png); 14 | } 15 | 16 | QSlider::groove:horizontal { 17 | background: transparent; 18 | height: 12px; 19 | } 20 | 21 | QSlider::sub-page:horizontal { 22 | background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1, stop: 0 #9f161b, stop: 1 #e11f26); 23 | border-top: 2px solid #161623; 24 | border-left: 2px solid #161623; 25 | border-bottom: 2px solid #7d7d92; 26 | border-right: 2px solid #7d7d92; 27 | border-radius: 5.32px; 28 | } 29 | 30 | QSlider::add-page:horizontal { 31 | height: 12px; 32 | background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1, stop: 0 #1c6c14, stop: 1 #28991c); 33 | border-top: 2px solid #161623; 34 | border-left: 2px solid #161623; 35 | border-bottom: 2px solid #7d7d92; 36 | border-right: 2px solid #7d7d92; 37 | border-radius: 5.32px; 38 | } 39 | -------------------------------------------------------------------------------- /styles/playerview.volumeSlider.3x.qss: -------------------------------------------------------------------------------- 1 | QSlider::handle:horizontal { 2 | width: 42px; 3 | height: 33px; 4 | background-color: transparent; 5 | border-image: url(:assets/volumeHandle.png); 6 | background: none; 7 | background-repeat: none; 8 | margin-top: -7.5px; 9 | margin-bottom: -7.5px; 10 | } 11 | 12 | QSlider::handle:horizontal:pressed { 13 | border-image: url(:assets/volumeHandle_p.png); 14 | } 15 | 16 | QSlider::groove:horizontal { 17 | background: transparent; 18 | height: 18px; 19 | } 20 | 21 | QSlider::sub-page:horizontal { 22 | background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1, stop: 0 #9f161b, stop: 1 #e11f26); 23 | border-top: 3px solid #161623; 24 | border-left: 3px solid #161623; 25 | border-bottom: 3px solid #7d7d92; 26 | border-right: 3px solid #7d7d92; 27 | border-radius: 8px; 28 | } 29 | 30 | QSlider::add-page:horizontal { 31 | height: 18px; 32 | background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1, stop: 0 #1c6c14, stop: 1 #28991c); 33 | border-top: 3px solid #161623; 34 | border-left: 3px solid #161623; 35 | border-bottom: 3px solid #7d7d92; 36 | border-right: 3px solid #7d7d92; 37 | border-radius: 8px; 38 | } 39 | -------------------------------------------------------------------------------- /styles/playerview.volumeSlider.4x.qss: -------------------------------------------------------------------------------- 1 | QSlider::handle:horizontal { 2 | width: 56px; 3 | height: 44px; 4 | background-color: transparent; 5 | border-image: url(:assets/volumeHandle.png); 6 | background: none; 7 | background-repeat: none; 8 | margin-top: -10px; 9 | margin-bottom: -10px; 10 | } 11 | 12 | QSlider::handle:horizontal:pressed { 13 | border-image: url(:assets/volumeHandle_p.png); 14 | } 15 | 16 | QSlider::groove:horizontal { 17 | background: transparent; 18 | height: 24px; 19 | } 20 | 21 | QSlider::sub-page:horizontal { 22 | background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1, stop: 0 #9f161b, stop: 1 #e11f26); 23 | border-top: 4px solid #161623; 24 | border-left: 4px solid #161623; 25 | border-bottom: 4px solid #7d7d92; 26 | border-right: 4px solid #7d7d92; 27 | border-radius: 10.64px; 28 | } 29 | 30 | QSlider::add-page:horizontal { 31 | height: 24px; 32 | background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 1, stop: 0 #1c6c14, stop: 1 #28991c); 33 | border-top: 4px solid #161623; 34 | border-left: 4px solid #161623; 35 | border-bottom: 4px solid #7d7d92; 36 | border-right: 4px solid #7d7d92; 37 | border-radius: 10.64px; 38 | } 39 | -------------------------------------------------------------------------------- /uiassets.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | assets/next_p.png 4 | assets/next.png 5 | assets/open_p.png 6 | assets/open.png 7 | assets/pause_p.png 8 | assets/pause.png 9 | assets/play_p.png 10 | assets/play.png 11 | assets/prev_p.png 12 | assets/prev.png 13 | assets/stop_p.png 14 | assets/stop.png 15 | assets/posHandle_p.png 16 | assets/posHandle.png 17 | assets/eq_off_p.png 18 | assets/eq_off.png 19 | assets/eq_on_p.png 20 | assets/eq_on.png 21 | assets/pl_off_p.png 22 | assets/pl_off.png 23 | assets/pl_on_p.png 24 | assets/pl_on.png 25 | assets/repeat_off_p.png 26 | assets/repeat_off.png 27 | assets/repeat_on_p.png 28 | assets/repeat_on.png 29 | assets/shuffle_off_p.png 30 | assets/shuffle_off.png 31 | assets/shuffle_on_p.png 32 | assets/shuffle_on.png 33 | assets/balanceHandle_p.png 34 | assets/balanceHandle.png 35 | assets/volumeHandle_p.png 36 | assets/volumeHandle.png 37 | assets/visualizationBackground.png 38 | assets/status_paused.png 39 | assets/status_stopped.png 40 | assets/status_playing.png 41 | assets/bignumbers.ttf 42 | assets/pl_add_p.png 43 | assets/pl_add.png 44 | assets/pl_close_p.png 45 | assets/pl_close.png 46 | assets/scroll_handle_p.png 47 | assets/scroll_handle.png 48 | assets/windowClose.png 49 | assets/windowMaximize.png 50 | assets/windowMinimize.png 51 | styles/controlbuttonswidget.shuffleButton.4x.qss 52 | styles/controlbuttonswidget.shuffleButton.1x.qss 53 | styles/controlbuttonswidget.shuffleButton.2x.qss 54 | styles/controlbuttonswidget.shuffleButton.3x.qss 55 | styles/controlbuttonswidget.repeatButton.1x.qss 56 | styles/controlbuttonswidget.repeatButton.2x.qss 57 | styles/controlbuttonswidget.repeatButton.3x.qss 58 | styles/controlbuttonswidget.repeatButton.4x.qss 59 | styles/playerview.posBar.1x.qss 60 | styles/playerview.posBar.2x.qss 61 | styles/playerview.posBar.3x.qss 62 | styles/playerview.posBar.4x.qss 63 | styles/playerview.kbpsFrame.1x.qss 64 | styles/playerview.kbpsFrame.2x.qss 65 | styles/playerview.kbpsFrame.3x.qss 66 | styles/playerview.kbpsFrame.4x.qss 67 | styles/playerview.khzFrame.1x.qss 68 | styles/playerview.khzFrame.2x.qss 69 | styles/playerview.khzFrame.3x.qss 70 | styles/playerview.khzFrame.4x.qss 71 | styles/playerview.eqButton.1x.qss 72 | styles/playerview.eqButton.2x.qss 73 | styles/playerview.eqButton.3x.qss 74 | styles/playerview.eqButton.4x.qss 75 | styles/playerview.playlistButton.1x.qss 76 | styles/playerview.playlistButton.2x.qss 77 | styles/playerview.playlistButton.3x.qss 78 | styles/playerview.playlistButton.4x.qss 79 | styles/playerview.balanceSlider.1x.qss 80 | styles/playerview.balanceSlider.2x.qss 81 | styles/playerview.balanceSlider.3x.qss 82 | styles/playerview.balanceSlider.4x.qss 83 | styles/playerview.volumeSlider.1x.qss 84 | styles/playerview.volumeSlider.4x.qss 85 | styles/playerview.volumeSlider.2x.qss 86 | styles/playerview.volumeSlider.3x.qss 87 | styles/playerview.songInfoContainer.1x.qss 88 | styles/playerview.songInfoContainer.2x.qss 89 | styles/playerview.songInfoContainer.3x.qss 90 | styles/playerview.songInfoContainer.4x.qss 91 | styles/playerview.visualizationFrame.1x.qss 92 | styles/playerview.visualizationFrame.2x.qss 93 | styles/playerview.visualizationFrame.3x.qss 94 | styles/playerview.visualizationFrame.4x.qss 95 | styles/playerview.codecDetailsContainer.1x.qss 96 | styles/playerview.codecDetailsContainer.2x.qss 97 | styles/playerview.codecDetailsContainer.3x.qss 98 | styles/playerview.codecDetailsContainer.4x.qss 99 | styles/desktopbasewindow.1x.qss 100 | styles/desktopbasewindow.2x.qss 101 | styles/desktopbasewindow.3x.qss 102 | styles/desktopbasewindow.4x.qss 103 | assets/filesIcon.png 104 | assets/playlistsIcon.png 105 | assets/spotifyIcon.png 106 | assets/pl_addIcon.png 107 | assets/pl_homeIcon.png 108 | assets/pl_playerIcon.png 109 | assets/pl_upIcon.png 110 | assets/fb_folderIcon.png 111 | assets/fb_musicIcon.png 112 | assets/fb_folderIcon_selected.png 113 | assets/fb_musicIcon_selected.png 114 | assets/logoButton.png 115 | assets/source-icon-bluetooth.png 116 | assets/source-icon-cd.png 117 | assets/source-icon-file.png 118 | assets/source-icon-spotify.png 119 | assets/menu-icon-x.png 120 | 121 | 122 | --------------------------------------------------------------------------------