commit 42179bae0711c03f923c9e1be3a340aea58d38d4 Author: chaos Date: Tue Dec 5 15:13:13 2023 +0000 moved from github diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..1c17650 --- /dev/null +++ b/.clang-format @@ -0,0 +1,5 @@ +--- +Language: Cpp +BasedOnStyle: WebKit +... + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..62fb854 --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +build +mpv-build +*.o +KittehPlayer +moc_* +*AppImage* +.qmake.stash +qrc_qml.cpp +appdir +AppDir +Makefile +linuxdeploy* +other_libs +sffmpeg +*.qmake.stash +*.qmlc +*.jsc +src_qml* +qmlcache* +qrc_src* +discord-rpc +*.kate-swp + +CMakeLists.txt.user +CMakeCache.txt +CMakeFiles +CMakeScripts +Testing +Makefile +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake +_deps +*_autogen +*.core +.core +*qmlcache* +cmake_install.cmake +spdlog \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4b9f680 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,29 @@ +language: cpp +compiler: gcc +sudo: require +dist: bionic + +before_install: + - mkdir -p $HOME/.cache/apt/partial + - sudo rm -rf /var/cache/apt/archives + - sudo ln -s $HOME/.cache/apt /var/cache/apt/archives + - sudo add-apt-repository ppa:beineri/opt-qt-5.12.6-bionic -y + - sudo apt-get update + +install: + - sudo apt-get -y install build-essential git qt512-meta-minimal qt512quickcontrols qt512quickcontrols2 qt512svg qt512x11extras qt512graphicaleffects qt512svg libgl1-mesa-dev libmpv-dev libgl1-mesa-dev x11proto-xext-dev libx11-dev python3-setuptools + - sudo apt-get build-dep mpv + - source /opt/qt*/bin/qt*-env.sh + +script: + - time bash scripts/makeappimage.sh + +after_success: + - time bash scripts/upload.sh + +branches: + except: + - # Do not build tags that we create when we upload to GitHub Releases + - /^(?i:continuous)/ + + diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..cd2c9c9 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,12 @@ +{ + "configurations": [ + { + "name": "Linux", + "compileCommands": "${workspaceFolder}/build/compile_commands.json", + "intelliSenseMode": "gcc-x64", + "cStandard": "c11", + "cppStandard": "c++17" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..1ba9803 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,110 @@ +cmake_minimum_required(VERSION 3.5.1) + +project(VideoPlayer) +include_directories(${CMAKE_SOURCE_DIR} ${CMAKE_BINARY_DIR}) +set(CMAKE_AUTOMOC ON) + +include(ExternalProject) + +find_package(Qt5Core REQUIRED) +find_package(Qt5Gui REQUIRED) +find_package(Qt5Concurrent REQUIRED) + +find_package(Qt5 CONFIG REQUIRED COMPONENTS Qml Quick Gui Widgets Core X11Extras) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +option(QML_DEBUG "enable qml debug" OFF) +if(QML_DEBUG) +add_definitions(-DQT_QML_DEBUG) +endif() + +option(OLD_UBUNTU "old ubuntu" OFF) + +find_package(Qt5QuickCompiler) +if(Qt5QuickCompiler_FOUND) +qtquick_compiler_add_resources(qml_QRC src/qml/qml.qrc) +else() +qt5_add_resources(qml_QRC src/qml/qml.qrc) +endif() + +find_package(PkgConfig) +pkg_check_modules(MPV REQUIRED mpv) +pkg_check_modules(X11 x11) +pkg_check_modules(Xext xext) + + +option(PINEPHONE "for pinephone/lima gpu" OFF) +if(PINEPHONE) +add_definitions(-DPINEPHONE) +endif() + +option(USE_EXTERNAL_SPDLOG "use external spdlog" OFF) + +if(USE_EXTERNAL_SPDLOG) + include_directories(${EXTERNAL_SPDLOG_PATH}/include) + include_directories(${EXTERNAL_SPDLOG_PATH}) + include_directories(/app) +else() +execute_process( + COMMAND git clone --depth 1 https://github.com/gabime/spdlog.git + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} +) +include_directories(${CMAKE_BINARY_DIR}/spdlog/include) +endif() + +if(MPV_VERSION VERSION_GREATER "1.27.0") +set(SOURCES ${SOURCES} src/Backends/MPV/MPVBackend.cpp) +else() +add_definitions(-DDISABLE_MPV_RENDER_API) +endif() + + +if(X11_FOUND AND Xext_FOUND) +add_definitions(-DENABLE_X11) +endif(X11_FOUND AND Xext_FOUND) + +link_directories(/usr/local/lib) + #src/Backends/DirectMPV/DirectMPVBackend.cpp + +set(SOURCES + src/main.cpp + src/utils.cpp + src/enums.cpp + src/Process.cpp + src/ThumbnailCache.cpp + src/logger.cpp + src/qmldebugger.cpp + src/registerTypes.cpp + src/Backends/MPVCommon/MPVCommon.cpp + src/Backends/MPVNoFBO/MPVNoFBOBackend.cpp + ${SOURCES} +) + +set(CMAKE_BUILD_TYPE DEBUG) + +option(DEBUG "debugging out" OFF) +if(DEBUG) +SET(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} ${CMAKE_CXX_FLAGS} -ggdb -g3 -Og") +else() +SET(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} ${CMAKE_CXX_FLAGS} -s") +endif() + +SET(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} ${CMAKE_CXX_FLAGS} -Wall -Wextra -Wno-unused-command-line-argument") + +add_executable(VideoPlayer ${SOURCES} ${qml_QRC}) +set_property(TARGET VideoPlayer PROPERTY CXX_STANDARD 14) + +# Use the Qml/Quick modules from Qt 5. +target_link_libraries(VideoPlayer + ${MPV_LIBRARIES} + ${X11_LIBRARIES} + ${Xext_LIBRARIES} + Qt5::X11Extras +) +include_directories(${Qt5Gui_PRIVATE_INCLUDE_DIRS} ${Qt5Concurrent_INCLUDE_DIRS}) +qt5_use_modules(VideoPlayer Qml Quick Core Gui Widgets X11Extras) + +install (TARGETS ${PROJECT_NAME} DESTINATION bin) +install (FILES "${PROJECT_NAME}.desktop" DESTINATION share/applications) +install (FILES "${PROJECT_NAME}.png" DESTINATION share/icons/hicolor/256x256/apps) diff --git a/DOCS.md b/DOCS.md new file mode 100644 index 0000000..5c4d9f3 --- /dev/null +++ b/DOCS.md @@ -0,0 +1,25 @@ +# VideoPlayer +A video player with many themes based on Qt, QML and libmpv. + +## Config Location +- On linux the config file will be located at `~/.config/VideoPlayer/VideoPlayer.conf` + +## Supported Languages +- `english` English +- `spanish` Español +- `german` Deutsch +- `french` Française +- `italian` Italiano +- `russian` Русский +- `norwegian` Norwegian +- `tokipona` toki pona +- `telugu` తెలుగు +- `vietnamese` Tiếng Việt +- Left = config value; Right = language name +- If your language isn't listed above then please either contact me (details at bottom of readme) or make a PR with the translations which are located in the file `src/qml/utils/translations.js`. + +## MPV Config +- Since VideoPlayer's default backend is based on libmpv, you can use the mpv.conf from `~/.config/mpv/mpv.conf`. + +## SVP Support +- VideoPlayer works with SVP, you just need to follow the same guides as for MPV. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..fa0086a --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a2867a3 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# VideoPlayer + +![made with c++](https://forthebadge.com/images/badges/made-with-c-plus-plus.svg) + +![libmpv 2.29+](https://img.shields.io/badge/libmpv-2.29+-blue.svg?logo=qt&style=for-the-badge) + +![qt 5.12](https://img.shields.io/badge/Qt-5.12-41cd52.svg?logo=qt&style=for-the-badge) + + +A video player based on Qt, QML and libmpv with themes for many online video players.. + +## Themes +Screenshots for youtube, niconico and roosterteeth/videojs themes are in screenshots folder + +## How to install +### From source +#### Dependencies +##### Arch Linux +``` +pacman -S git cmake qt5-svg qt5-declarative qt5-quickcontrols qt5-quickcontrols2 qt5-graphicaleffects mpv +``` +##### Ubuntu Bionic +``` +sudo add-apt-repository ppa:beineri/opt-qt-5.12.6-bionic -y +sudo apt update +sudo apt install build-essential git qt512-meta-minimal qt512quickcontrols qt512quickcontrols2 qt512svg qt512x11extras qt512graphicaleffects qt512svg libgl1-mesa-dev libmpv-dev +``` +##### Debian +``` +sudo apt install build-essential cmake qtquickcontrols2-5-dev qtbase5-dev qtdeclarative5-dev libqt5x11extras5-dev libmpv-dev qml-module-qtquick-controls qml-module-qtquick-controls2 qml-module-qtquick-extras qml-module-qtquick-layouts qml-module-qtquick-dialogs qml-module-qtquick-privatewidgets qml-module-qtquick-localstorage qml-module-qt-labs-settings qml-module-qt-labs-platform qtbase5-private-dev libqt5svg5 +``` +- Note that I don't know if this is the full list yet, pop a issue up if building fails. + +#### Instructions +- Clone repo +- `mkdir build && cd build` +- `cmake .. -DCMAKE_INSTALL_PREFIX=/usr` +- `make` +- `sudo make install` +- The finished player will then be installed and you can launch it using `VideoPlayer` + +## Configuration +- For docs on VideoPlayer please view DOCS.md or `man VideoPlayer` diff --git a/VideoPlayer.desktop b/VideoPlayer.desktop new file mode 100644 index 0000000..3aab552 --- /dev/null +++ b/VideoPlayer.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Type=Application +Name=VideoPlayer +Comment=Video Player +Icon=VideoPlayer +Exec=VideoPlayer %U +Terminal=false +Categories=AudioVideo;Audio;Video;Player;TV; +MimeType=application/ogg;application/x-ogg;application/mxf;application/sdp;application/smil;application/x-smil;application/streamingmedia;application/x-streamingmedia;application/vnd.rn-realmedia;application/vnd.rn-realmedia-vbr;audio/aac;audio/x-aac;audio/vnd.dolby.heaac.1;audio/vnd.dolby.heaac.2;audio/aiff;audio/x-aiff;audio/m4a;audio/x-m4a;application/x-extension-m4a;audio/mp1;audio/x-mp1;audio/mp2;audio/x-mp2;audio/mp3;audio/x-mp3;audio/mpeg;audio/mpeg2;audio/mpeg3;audio/mpegurl;audio/x-mpegurl;audio/mpg;audio/x-mpg;audio/rn-mpeg;audio/musepack;audio/x-musepack;audio/ogg;audio/scpls;audio/x-scpls;audio/vnd.rn-realaudio;audio/wav;audio/x-pn-wav;audio/x-pn-windows-pcm;audio/x-realaudio;audio/x-pn-realaudio;audio/x-ms-wma;audio/x-pls;audio/x-wav;video/mpeg;video/x-mpeg2;video/x-mpeg3;video/mp4v-es;video/x-m4v;video/mp4;application/x-extension-mp4;video/divx;video/vnd.divx;video/msvideo;video/x-msvideo;video/ogg;video/quicktime;video/vnd.rn-realvideo;video/x-ms-afs;video/x-ms-asf;audio/x-ms-asf;application/vnd.ms-asf;video/x-ms-wmv;video/x-ms-wmx;video/x-ms-wvxvideo;video/x-avi;video/avi;video/x-flic;video/fli;video/x-flc;video/flv;video/x-flv;video/x-theora;video/x-theora+ogg;video/x-matroska;video/mkv;audio/x-matroska;application/x-matroska;video/webm;audio/webm;audio/vorbis;audio/x-vorbis;audio/x-vorbis+ogg;video/x-ogm;video/x-ogm+ogg;application/x-ogm;application/x-ogm-audio;application/x-ogm-video;application/x-shorten;audio/x-shorten;audio/x-ape;audio/x-wavpack;audio/x-tta;audio/AMR;audio/ac3;audio/eac3;audio/amr-wb;video/mp2t;audio/flac;audio/mp4;application/x-mpegurl;video/vnd.mpegurl;application/vnd.apple.mpegurl;audio/x-pn-au;video/3gp;video/3gpp;video/3gpp2;audio/3gpp;audio/3gpp2;video/dv;audio/dv;audio/opus;audio/vnd.dts;audio/vnd.dts.hd;audio/x-adpcm;application/x-cue;audio/m3u; +X-KDE-Protocols=ftp,http,https,mms,rtmp,rtsp,sftp,smb,videoplayer diff --git a/VideoPlayer.png b/VideoPlayer.png new file mode 100644 index 0000000..7ae0d77 Binary files /dev/null and b/VideoPlayer.png differ diff --git a/VideoPlayer.svg b/VideoPlayer.svg new file mode 100644 index 0000000..b0f0fee --- /dev/null +++ b/VideoPlayer.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/format-code.sh b/format-code.sh new file mode 100755 index 0000000..46e37ad --- /dev/null +++ b/format-code.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -x +SOURCE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" +pushd $SOURCE_DIR +find . -name "*.qml" -exec qmlfmt -i 2 -w {} \; +clang-format -style=file -i $(find src -name "*.cpp" -o -name "*.hpp" -o -name "*.c" -o -name "*.h") +#find . -name "*.cpp" -o -name "*.hpp" -o -name "*.c" -o -name "*.h" -exec clang-format -style=file -i {} \; +popd diff --git a/screenshots/NicoNico.png b/screenshots/NicoNico.png new file mode 100644 index 0000000..5dc994e Binary files /dev/null and b/screenshots/NicoNico.png differ diff --git a/screenshots/RoosterTeeth.png b/screenshots/RoosterTeeth.png new file mode 100644 index 0000000..49858e7 Binary files /dev/null and b/screenshots/RoosterTeeth.png differ diff --git a/screenshots/YouTube.png b/screenshots/YouTube.png new file mode 100644 index 0000000..43eeb8e Binary files /dev/null and b/screenshots/YouTube.png differ diff --git a/scripts/rendersvgs.sh b/scripts/rendersvgs.sh new file mode 100755 index 0000000..3a0a62c --- /dev/null +++ b/scripts/rendersvgs.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -x +SOURCE_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" +cd ${SOURCE_DIR}/../src/qml/icons + +for file in `find . -name "*.svg"`; do + rendersvg "$file" "$(echo $file | sed s/.svg/.png/)" +done \ No newline at end of file diff --git a/src/Backends/MPV/MPVBackend.cpp b/src/Backends/MPV/MPVBackend.cpp new file mode 100644 index 0000000..8eeb78e --- /dev/null +++ b/src/Backends/MPV/MPVBackend.cpp @@ -0,0 +1,332 @@ +#include "src/Backends/MPV/MPVBackend.hpp" +#include "src/Backends/MPVCommon/MPVCommon.hpp" +#include "src/qthelper.hpp" +#include "src/utils.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +class QQuickItem; +class QSize; + +#if defined(__linux__) || defined(__FreeBSD__) +#ifdef ENABLE_X11 +#include // IWYU pragma: keep +#include // IWYU pragma: keep +#include // IWYU pragma: keep +#include // IWYU pragma: keep +#include // IWYU pragma: keep +#endif +#include // IWYU pragma: keep +#endif + +bool usedirect = false; + +namespace { + +void wakeup(void* ctx) +{ + QCoreApplication::postEvent((MPVBackend*)ctx, new QEvent(QEvent::User)); +} + +void on_mpv_redraw(void* ctx) +{ + QMetaObject::invokeMethod( + reinterpret_cast(ctx), "update", Qt::QueuedConnection); +} + +static void* +get_proc_address_mpv(void* ctx, const char* name) +{ + return reinterpret_cast( + reinterpret_cast(ctx)->getProcAddress(QByteArray(name))); +} + +} // namespace + +class MpvRenderer : public QQuickFramebufferObject::Renderer { + MPVBackend* obj; + +public: + MpvRenderer(MPVBackend* new_obj) + : obj{ new_obj } + { + if (usedirect) { + int r = mpv_opengl_cb_init_gl(obj->mpv_gl_cb, NULL, get_proc_address_mpv, QOpenGLContext::currentContext()); + if (r < 0) { + std::cout << "No." << std::endl; + throw std::runtime_error("failed to initialize mpv GL context"); + } + } + } + + virtual ~MpvRenderer() {} + + // This function is called when a new FBO is needed. + // This happens on the initial frame. + QOpenGLFramebufferObject* createFramebufferObject(const QSize& size) + { + // init mpv_gl: + if (!obj->mpv_gl && !usedirect) { + mpv_opengl_init_params gl_init_params{ get_proc_address_mpv, + QOpenGLContext::currentContext(), + nullptr }; + mpv_render_param params[]{ + { MPV_RENDER_PARAM_API_TYPE, + const_cast(MPV_RENDER_API_TYPE_OPENGL) }, + { MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &gl_init_params }, + { MPV_RENDER_PARAM_INVALID, nullptr }, + { MPV_RENDER_PARAM_INVALID, nullptr } + }; +#if defined(__linux__) || defined(__FreeBSD__) +#ifdef ENABLE_X11 + if (QGuiApplication::platformName().contains("xcb")) { + params[2].type = MPV_RENDER_PARAM_X11_DISPLAY; + params[2].data = QX11Info::display(); + } +#endif + if (QGuiApplication::platformName().contains("wayland")) { + params[2].type = MPV_RENDER_PARAM_WL_DISPLAY; + auto* native = QGuiApplication::platformNativeInterface(); + params[2].data = native->nativeResourceForWindow("display", NULL); + } +#endif + + if (mpv_render_context_create(&obj->mpv_gl, obj->mpv, params) < 0) { + std::cout << "Failed to use render API, try setting Backend/direct to true in settings." << std::endl; + throw std::runtime_error("failed to initialize mpv GL context"); + } + mpv_render_context_set_update_callback(obj->mpv_gl, on_mpv_redraw, obj); + } + QMetaObject::invokeMethod(obj, "startPlayer"); + + return QQuickFramebufferObject::Renderer::createFramebufferObject(size); + } + + void render() + { + obj->window()->resetOpenGLState(); + QOpenGLFramebufferObject* fbo = framebufferObject(); + if (usedirect) { + mpv_opengl_cb_draw(obj->mpv_gl_cb, fbo->handle(), fbo->width(), fbo->height()); + } else { + mpv_opengl_fbo mpfbo{ .fbo = static_cast(fbo->handle()), + .w = fbo->width(), + .h = fbo->height(), + .internal_format = 0 }; + int flip_y{ 0 }; + mpv_render_param params[] = { { MPV_RENDER_PARAM_OPENGL_FBO, &mpfbo }, + { MPV_RENDER_PARAM_FLIP_Y, &flip_y }, + { MPV_RENDER_PARAM_INVALID, nullptr } }; + mpv_render_context_render(obj->mpv_gl, params); + } + + obj->window()->resetOpenGLState(); + } +}; + +MPVBackend::MPVBackend(QQuickItem* parent) + : QQuickFramebufferObject(parent) + , mpv{ mpv_create() } + , mpv_gl(nullptr) + , mpv_gl_cb(nullptr) + +{ + if (!mpv) + throw std::runtime_error("could not create mpv context"); + + QSettings settings; + usedirect = settings.value("Backend/direct", false).toBool(); + + mpv_set_option_string(mpv, "terminal", "true"); + mpv_set_option_string(mpv, "msg-level", "all=v"); + + // Fix? + mpv_set_option_string(mpv, "ytdl", "yes"); + + mpv_set_option_string(mpv, "slang", "en"); + + mpv_set_option_string(mpv, "config", "yes"); + mpv_observe_property(mpv, 0, "tracks-menu", MPV_FORMAT_NONE); + mpv_observe_property(mpv, 0, "chapter-list", MPV_FORMAT_NODE); + mpv_observe_property(mpv, 0, "playback-abort", MPV_FORMAT_NONE); + mpv_observe_property(mpv, 0, "chapter-list", MPV_FORMAT_NODE); + mpv_observe_property(mpv, 0, "track-list", MPV_FORMAT_NODE); + mpv_observe_property(mpv, 0, "audio-device-list", MPV_FORMAT_NODE); + mpv_observe_property(mpv, 0, "playlist-pos", MPV_FORMAT_DOUBLE); + mpv_observe_property(mpv, 0, "volume", MPV_FORMAT_NONE); + mpv_observe_property(mpv, 0, "mute", MPV_FORMAT_NONE); + mpv_observe_property(mpv, 0, "duration", MPV_FORMAT_DOUBLE); + mpv_observe_property(mpv, 0, "media-title", MPV_FORMAT_STRING); + mpv_observe_property(mpv, 0, "time-pos", MPV_FORMAT_DOUBLE); + mpv_observe_property(mpv, 0, "demuxer-cache-duration", MPV_FORMAT_DOUBLE); + mpv_observe_property(mpv, 0, "pause", MPV_FORMAT_NODE); + mpv_observe_property(mpv, 0, "playlist", MPV_FORMAT_NODE); + mpv_observe_property(mpv, 0, "speed", MPV_FORMAT_DOUBLE); + + mpv_request_log_messages(mpv, "v"); + + mpv_set_wakeup_callback(mpv, wakeup, this); + + if (mpv_initialize(mpv) < 0) + throw std::runtime_error("could not initialize mpv context"); + + if (usedirect) { + mpv_set_option_string(mpv, "vo", "libmpv"); + mpv_gl_cb = (mpv_opengl_cb_context*)mpv_get_sub_api(mpv, MPV_SUB_API_OPENGL_CB); + if (!mpv_gl_cb) + throw std::runtime_error("OpenGL not compiled in"); + mpv_opengl_cb_set_update_callback(mpv_gl_cb, on_mpv_redraw, (void*)this); + } else { + mpv_set_option_string(mpv, "vo", "libmpv"); + } + + connect(this, + &MPVBackend::onUpdate, + this, + &MPVBackend::doUpdate, + Qt::QueuedConnection); + connect(this, + &MPVBackend::positionChanged, + this, + &MPVBackend::updateDurationString, + Qt::QueuedConnection); + connect(this, + &MPVBackend::durationChanged, + this, + &MPVBackend::updateDurationString, + Qt::QueuedConnection); +} + +MPVBackend::~MPVBackend() +{ + printf("Shutting down...\n"); + Utils::SetDPMS(true); + command("write-watch-later-config"); + + if (usedirect && mpv_gl_cb) { + mpv_opengl_cb_uninit_gl(mpv_gl_cb); + } else if (mpv_gl) { + mpv_render_context_free(mpv_gl); + } + + mpv_terminate_destroy(mpv); + printf("MPV terminated.\n"); +} + +void MPVBackend::on_update(void* ctx) +{ + MPVBackend* self = (MPVBackend*)ctx; + emit self->onUpdate(); +} + +void MPVBackend::doUpdate() +{ + update(); +} + +QVariant +MPVBackend::getProperty(const QString& name) const +{ + return mpv::qt::get_property_variant(mpv, name); +} + +void MPVBackend::command(const QVariant& params) +{ + mpv::qt::node_builder node(params); + mpv_command_node(mpv, node.node(), nullptr); +} + +void MPVBackend::setProperty(const QString& name, const QVariant& value) +{ + mpv::qt::node_builder node(value); + qDebug() << "Setting property" << name << "to" << value; + mpv_set_property(mpv, name.toUtf8().data(), MPV_FORMAT_NODE, node.node()); +} + +void MPVBackend::setOption(const QString& name, const QVariant& value) +{ + mpv::qt::set_option_variant(mpv, name, value); +} + +QVariant +MPVBackend::playerCommand(const Enums::Commands& cmd) +{ + return playerCommand(cmd, QVariant("NoArgProvided")); +} + +QVariant +MPVBackend::playerCommand(const Enums::Commands& cmd, const QVariant& args) +{ + return MPVCommon::playerCommand(this, cmd, args); +} + +QString +MPVBackend::getStats() +{ + return MPVCommon::getStats(this); +} + +void MPVBackend::updateDurationString(int numTime) +{ + QMetaMethod metaMethod = sender()->metaObject()->method(senderSignalIndex()); + MPVCommon::updateDurationString(this, numTime, metaMethod); +} + +void MPVBackend::toggleOnTop() +{ + onTop = !onTop; + Utils::AlwaysOnTop(window()->winId(), onTop); +} + +bool MPVBackend::event(QEvent* event) +{ + if (event->type() == QEvent::User) { + on_mpv_events(); + } + return QObject::event(event); +} + +void MPVBackend::on_mpv_events() +{ + while (mpv) { + mpv_event* event = mpv_wait_event(mpv, 0); + if (event->event_id == MPV_EVENT_NONE) { + break; + } + handle_mpv_event(event); + } +} + +QVariantMap +MPVBackend::getAudioDevices(const QVariant& drivers) const +{ + return MPVCommon::getAudioDevices(drivers); +} + +void MPVBackend::handle_mpv_event(mpv_event* event) +{ + MPVCommon::handle_mpv_event(this, event); +} + +QQuickFramebufferObject::Renderer* +MPVBackend::createRenderer() const +{ + window()->setIcon(QIcon(":/icon.png")); + window()->setPersistentOpenGLContext(true); + window()->setPersistentSceneGraph(true); + return new MpvRenderer(const_cast(this)); +} diff --git a/src/Backends/MPV/MPVBackend.hpp b/src/Backends/MPV/MPVBackend.hpp new file mode 100644 index 0000000..ed2907d --- /dev/null +++ b/src/Backends/MPV/MPVBackend.hpp @@ -0,0 +1,99 @@ +#ifndef MPVBackend_H +#define MPVBackend_H + +#include "src/backendinterface.hpp" +#include "src/enums.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class MPVBackend + : public QQuickFramebufferObject, + public BackendInterface { + Q_INTERFACES(BackendInterface) + + Q_OBJECT + Q_PROPERTY(bool logging READ logging WRITE setLogging) + + mpv_handle* mpv; + mpv_render_context* mpv_gl; + mpv_opengl_cb_context* mpv_gl_cb; + + QSettings settings; + bool onTop = false; + bool m_logging = true; + + friend class MpvRenderer; + +public: + static void on_update(void* ctx); + + MPVBackend(QQuickItem* parent = 0); + virtual ~MPVBackend(); + virtual Renderer* createRenderer() const; + + void setLogging(bool a) + { + if (a != m_logging) { + m_logging = a; + } + } + bool logging() const { return m_logging; } + + int lastTime = 0; + double lastSpeed = 0; + QString totalDurationString; + QString lastPositionString; + +public slots: + QVariant playerCommand(const Enums::Commands& command, const QVariant& args); + QVariant playerCommand(const Enums::Commands& command); + void toggleOnTop(); + QString getStats(); + // Optional but handy for MPV or custom backend settings. + void command(const QVariant& params); + void setProperty(const QString& name, const QVariant& value); + void setOption(const QString& name, const QVariant& value); + QVariant getProperty(const QString& name) const; + // Just used for adding missing audio devices to list. + QVariantMap getAudioDevices(const QVariant& drivers) const; + bool event(QEvent* event); + +signals: + void onUpdate(); + void mpv_events(); + void onMpvEvent(mpv_event* event); + // All below required for Player API + void playStatusChanged(const Enums::PlayStatus& status); + void volumeStatusChanged(const Enums::VolumeStatus& status); + void volumeChanged(const int& volume); + void durationChanged(const double& duration); + void positionChanged(const double& position); + void cachedDurationChanged(const double& duration); + void playlistPositionChanged(const double& position); + void titleChanged(const QString& title); + void durationStringChanged(const QString& string); + void tracksChanged(const QVariantList& tracks); + void audioDevicesChanged(const QVariantMap& devices); + void playlistChanged(const QVariantList& devices); + void chaptersChanged(const QVariantList& devices); + void speedChanged(const double& speed); + +private slots: + void doUpdate(); + void on_mpv_events(); + void updateDurationString(int numTime); + +private: + void handle_mpv_event(mpv_event* event); +}; + +#endif diff --git a/src/Backends/MPVCommon/MPVCommon.cpp b/src/Backends/MPVCommon/MPVCommon.cpp new file mode 100644 index 0000000..a40105f --- /dev/null +++ b/src/Backends/MPVCommon/MPVCommon.cpp @@ -0,0 +1,574 @@ +#include "src/Backends/MPVCommon/MPVCommon.hpp" +#include "spdlog/logger.h" +#include "src/backendinterface.hpp" +#include "src/logger.h" +#include "src/utils.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +auto mpvLogger = initLogger("mpv"); + +QString humanSize(uint64_t bytes) +{ + const char* suffix[5] = { "B", "KB", "MB", "GB", "TB" }; + char length = sizeof(suffix) / sizeof(suffix[0]); + + int i = 0; + double dblBytes = bytes; + + if (bytes > 1024) { + for (i = 0; (bytes / 1024) > 0 && i < length - 1; i++, bytes /= 1024) + dblBytes = bytes / 1024.0; + } + + static char output[200]; + sprintf(output, "%.02lf %s", dblBytes, suffix[i]); + return QString(output); +} + +static inline QVariant mpvnode_to_variant(const mpv_node* node) +{ + if (!node) { + return QVariant(); + } + + switch (node->format) { + case MPV_FORMAT_STRING: + return QVariant(QString::fromUtf8(node->u.string)); + case MPV_FORMAT_FLAG: + return QVariant(static_cast(node->u.flag)); + case MPV_FORMAT_INT64: + return QVariant(static_cast(node->u.int64)); + case MPV_FORMAT_DOUBLE: + return QVariant(node->u.double_); + case MPV_FORMAT_NODE_ARRAY: { + mpv_node_list* list = node->u.list; + QVariantList qlist; + for (int n = 0; n < list->num; n++) + qlist.append(mpvnode_to_variant(&list->values[n])); + return QVariant(qlist); + } + case MPV_FORMAT_NODE_MAP: { + mpv_node_list* list = node->u.list; + QVariantMap qmap; + for (int n = 0; n < list->num; n++) { + qmap.insert(QString::fromUtf8(list->keys[n]), + mpvnode_to_variant(&list->values[n])); + } + return QVariant(qmap); + } + default: // MPV_FORMAT_NONE, unknown values (e.g. future extensions) + return QVariant(); + } +} + +namespace MPVCommon { +QString getStats(BackendInterface* b) +{ + QString stats; + stats = ""; + QString filename = b->getProperty("filename").toString(); + // File Info + stats += "File: " + filename; + stats += "
"; + QString title = b->getProperty("media-title").toString(); + if (title != filename) { + stats += "Title: " + title + "
"; + } + QString fileFormat = b->getProperty("file-format").toString(); + stats += "Format/Protocol: " + fileFormat + "
"; + double cacheUsed = b->getProperty("cache-used").toDouble(); + int demuxerSecs = b->getProperty("demuxer-cache-duration").toInt(); + QVariantMap demuxerState = b->getProperty("demuxer-cache-state").toMap(); + int demuxerCache = demuxerState.value("fw-bytes", QVariant(0)).toInt(); + + if (demuxerSecs + demuxerCache + cacheUsed > 0) { + QString cacheStats; + cacheStats += "Total Cache: "; + cacheStats += humanSize(demuxerCache + cacheUsed); + cacheStats += " (Demuxer: "; + + cacheStats += humanSize(demuxerCache); + cacheStats += ", "; + cacheStats += QString::number(demuxerSecs) + "s) "; + double cacheSpeed = b->getProperty("cache-speed").toDouble(); + if (cacheSpeed > 0) { + cacheStats += "Speed: "; + cacheStats += humanSize(demuxerSecs); + cacheStats += "/s"; + } + cacheStats += "
"; + stats += cacheStats; + } + QString fileSize = humanSize(b->getProperty("file-size").toInt()).remove("-"); + stats += "Size: " + fileSize + "
"; + + stats += "
"; + // Video Info + QVariant videoParams = b->getProperty("video-params"); + if (videoParams.isNull()) { + videoParams = b->getProperty("video-out-params"); + } + if (!videoParams.isNull()) { + stats += "Video: " + b->getProperty("video-codec").toString(); + stats += "
"; + QString avsync = QString::number(b->getProperty("avsync").toDouble(), 'f', 3); + stats += "A-V: " + QString(avsync) + "
"; + + stats += "Dropped Frames: "; + int dFDC = b->getProperty("decoder-frame-drop-count").toInt(); + if (dFDC > 0) { + stats += QString::number(dFDC) + " (decoder) "; + } + int fDC = b->getProperty("frame-drop-count").toInt(); + if (fDC > 0) { + stats += QString::number(fDC) + " (output)"; + } + stats += "
"; + + int dFPS = b->getProperty("display-fps").toInt(); + int eDFPS = b->getProperty("estimated-display-fps").toInt(); + if ((dFPS + eDFPS) > 0) { + stats += "Display FPS: "; + + if (dFPS > 0) { + stats += QString::number(dFPS); + stats += " (specified) "; + } + if (eDFPS > 0) { + stats += QString::number(eDFPS); + stats += " (estimated)"; + } + stats += "
"; + } + + int cFPS = b->getProperty("container-fps").toInt(); + int eVFPS = b->getProperty("estimated-vf-fps").toInt(); + if ((cFPS + eVFPS) > 0) { + stats += "FPS: "; + + if (cFPS > 0) { + stats += QString::number(cFPS); + stats += " (specified) "; + } + if (eVFPS > 0) { + stats += QString::number(eVFPS); + stats += " (estimated)"; + } + stats += "
"; + } + QVariantMap vPM = videoParams.toMap(); + stats += "Native Resolution: "; + stats += vPM["w"].toString() + " x " + vPM["h"].toString(); + stats += "
"; + + stats += "Window Scale: "; + stats += vPM["window-scale"].toString(); + stats += "
"; + + stats += "Aspect Ratio: "; + stats += vPM["aspect"].toString(); + stats += "
"; + + stats += "Pixel Format: "; + stats += vPM["pixelformat"].toString(); + stats += "
"; + + stats += "Primaries: "; + stats += vPM["primaries"].toString(); + stats += " Colormatrix: "; + stats += vPM["colormatrix"].toString(); + stats += "
"; + + stats += "Levels: "; + stats += vPM["colorlevels"].toString(); + double sigPeak = vPM.value("sig-peak", QVariant(0.0)).toInt(); + if (sigPeak > 0) { + stats += " (HDR Peak: " + QString::number(sigPeak) + ")"; + } + stats += "
"; + + stats += "Gamma: "; + stats += vPM["gamma"].toString(); + stats += "
"; + + int pVB = b->getProperty("packet-video-bitrate").toInt(); + if (pVB > 0) { + stats += "Bitrate: "; + stats += humanSize(pVB) + "/s"; + stats += "
"; + } + + stats += "
"; + } + QVariant audioParams = b->getProperty("audio-params"); + if (audioParams.isNull()) { + audioParams = b->getProperty("audio-out-params"); + } + if (!audioParams.isNull()) { + stats += "Audio: " + b->getProperty("audio-codec").toString(); + stats += "
"; + QVariantMap aPM = audioParams.toMap(); + + stats += "Format: "; + stats += aPM["format"].toString(); + stats += "
"; + + stats += "Sample Rate: "; + stats += aPM["samplerate"].toString() + " Hz"; + stats += "
"; + + stats += "Channels: "; + stats += aPM["chanel-count"].toString(); + stats += "
"; + + int pAB = b->getProperty("packet-audio-bitrate").toInt(); + if (pAB > 0) { + stats += "Bitrate: "; + stats += humanSize(pAB) + "/s"; + stats += "
"; + } + + stats += "
"; + } + + return stats; +} + +QVariant playerCommand(BackendInterface* b, const Enums::Commands& cmd, const QVariant& args) +{ + switch (cmd) { + case Enums::Commands::TogglePlayPause: { + b->command(QVariantList() << "cycle" + << "pause"); + break; + } + case Enums::Commands::ToggleMute: { + b->command(QVariantList() << "cycle" + << "mute"); + break; + } + case Enums::Commands::SetAudioDevice: { + b->setProperty("audio-device", args.toString()); + break; + } + case Enums::Commands::SetVolume: { + b->command(QVariantList() << "set" + << "volume" << args); + break; + } + + case Enums::Commands::AddVolume: { + + b->command(QVariantList() << "add" + << "volume" << args); + break; + } + + case Enums::Commands::AddSpeed: { + + QString speedString = QString::number(b->getProperty("speed").toDouble() + args.toDouble()); + QVariant newSpeed = QVariant(speedString.left(speedString.lastIndexOf('.') + 2)); + + b->playerCommand(Enums::Commands::SetSpeed, newSpeed); + break; + } + + case Enums::Commands::SubtractSpeed: { + + QString speedString = QString::number(b->getProperty("speed").toDouble() - args.toDouble()); + QVariant newSpeed = QVariant(speedString.left(speedString.lastIndexOf('.') + 2)); + b->playerCommand(Enums::Commands::SetSpeed, newSpeed); + break; + } + + case Enums::Commands::ChangeSpeed: { + + b->playerCommand( + Enums::Commands::SetSpeed, + QVariant(b->getProperty("speed").toDouble() * args.toDouble())); + break; + } + + case Enums::Commands::SetSpeed: { + + b->command(QVariantList() << "set" + << "speed" << args.toString()); + break; + } + case Enums::Commands::ToggleStats: { + + b->command(QVariantList() << "script-binding" + << "stats/display-stats-toggle"); + break; + } + case Enums::Commands::NextAudioTrack: { + + b->command(QVariantList() << "cycle" + << "audio"); + break; + } + case Enums::Commands::NextSubtitleTrack: { + + b->command(QVariantList() << "cycle" + << "sub"); + + break; + } + case Enums::Commands::NextVideoTrack: { + b->command(QVariantList() << "cycle" + << "video"); + break; + } + case Enums::Commands::PreviousPlaylistItem: { + + b->command(QVariantList() << "playlist-prev"); + + break; + } + case Enums::Commands::NextPlaylistItem: { + + b->command(QVariantList() << "playlist-next" + << "force"); + break; + } + case Enums::Commands::LoadFile: { + b->command(QVariantList() << "loadfile" << args); + + break; + } + case Enums::Commands::AppendFile: { + + b->command(QVariantList() << "loadfile" << args << "append-play"); + break; + } + case Enums::Commands::SeekAbsolute: { + + b->command(QVariantList() << "seek" << args << "absolute"); + + break; + } + case Enums::Commands::Seek: { + + b->command(QVariantList() << "seek" << args); + + break; + } + + case Enums::Commands::ForwardFrame: { + + b->command(QVariantList() << "frame-step"); + + break; + } + case Enums::Commands::BackwardFrame: { + + b->command(QVariantList() << "frame-back-step"); + + break; + } + + case Enums::Commands::SetTrack: { + + b->command(QVariantList() << "set" << args.toList()[0] << args.toList()[1]); + + break; + } + + case Enums::Commands::SetPlaylistPos: { + + b->command(QVariantList() << "set" + << "playlist-pos" << args); + + break; + } + + case Enums::Commands::ForcePause: { + + b->command(QVariantList() << "set" + << "pause" + << "yes"); + + break; + } + + case Enums::Commands::PreviousChapter: { + + b->command(QVariantList() << "add" << "chapter" << "-1"); + + break; + } + + case Enums::Commands::NextChapter: { + + b->command(QVariantList() << "add" << "chapter" << "1"); + + break; + } + + default: { + //qDebug() << "Command not found: " << cmd; + break; + } + } + return QVariant("NoOutput"); +} + +void updateDurationString(BackendInterface* b, int numTime, QMetaMethod metaMethod) +{ + QVariant speed = b->getProperty("speed"); + QSettings settings; + if (metaMethod.name() == "positionChanged") { + if (speed != b->lastSpeed) { + b->lastSpeed = speed.toDouble(); + } else { + if (numTime == b->lastTime) { + return; + } + } + b->lastTime = numTime; + b->lastPositionString = Utils::createTimestamp(b->lastTime); + } else if (metaMethod.name() == "durationChanged") { + b->totalDurationString = Utils::createTimestamp(numTime); + } + QString durationString; + durationString += b->lastPositionString; + durationString += " / "; + durationString += b->totalDurationString; + if (b->lastSpeed != 1) { + if (settings.value("Appearance/themeName", "").toString() != "RoosterTeeth") { + durationString += " (" + speed.toString() + "x)"; + } + } + emit b->durationStringChanged(durationString); +} + +void handle_mpv_event(BackendInterface* b, mpv_event* event) +{ + switch (event->event_id) { + case MPV_EVENT_PROPERTY_CHANGE: { + mpv_event_property* prop = (mpv_event_property*)event->data; + if (strcmp(prop->name, "time-pos") == 0) { + if (prop->format == MPV_FORMAT_DOUBLE) { + double time = *(double*)prop->data; + emit b->positionChanged(time); + } + } else if (strcmp(prop->name, "duration") == 0) { + if (prop->format == MPV_FORMAT_DOUBLE) { + double time = *(double*)prop->data; + emit b->durationChanged(time); + } + } else if (strcmp(prop->name, "mute") == 0 || strcmp(prop->name, "volume") == 0) { + double volume = b->getProperty("volume").toDouble(); + bool mute = b->getProperty("mute").toBool(); + if (mute || volume == 0) { + emit b->volumeStatusChanged(Enums::VolumeStatus::Muted); + } else { + if (volume < 25) { + emit b->volumeStatusChanged(Enums::VolumeStatus::Low); + } else { + emit b->volumeStatusChanged(Enums::VolumeStatus::Normal); + } + } + // emit volumeChanged(volume); + } else if (strcmp(prop->name, "media-title") == 0) { + if (prop->format == MPV_FORMAT_STRING) { + char* title = *(char**)prop->data; + emit b->titleChanged(QString(title)); + } + } else if (strcmp(prop->name, "demuxer-cache-duration") == 0) { + if (prop->format == MPV_FORMAT_DOUBLE) { + double duration = *(double*)prop->data; + emit b->cachedDurationChanged(duration); + } + } else if (strcmp(prop->name, "playlist-pos") == 0) { + if (prop->format == MPV_FORMAT_DOUBLE) { + double pos = *(double*)prop->data; + emit b->playlistPositionChanged(pos); + } + } else if (strcmp(prop->name, "pause") == 0) { + mpv_node* nod = (mpv_node*)prop->data; + if (mpvnode_to_variant(nod).toBool()) { + emit b->playStatusChanged(Enums::PlayStatus::Paused); + // Utils::SetScreensaver(window()->winId(), true); + } else { + emit b->playStatusChanged(Enums::PlayStatus::Playing); + // Utils::SetScreensaver(window()->winId(), false); + } + } else if (strcmp(prop->name, "track-list") == 0) { + mpv_node* nod = (mpv_node*)prop->data; + emit b->tracksChanged(mpvnode_to_variant(nod).toList()); + } else if (strcmp(prop->name, "audio-device-list") == 0) { + mpv_node* nod = (mpv_node*)prop->data; + emit b->audioDevicesChanged(b->getAudioDevices(mpvnode_to_variant(nod))); + } else if (strcmp(prop->name, "playlist") == 0) { + mpv_node* nod = (mpv_node*)prop->data; + emit b->playlistChanged(mpvnode_to_variant(nod).toList()); + } else if (strcmp(prop->name, "chapter-list") == 0) { + mpv_node* nod = (mpv_node*)prop->data; + emit b->chaptersChanged(mpvnode_to_variant(nod).toList()); + } else if (strcmp(prop->name, "speed") == 0) { + double speed = *(double*)prop->data; + emit b->speedChanged(speed); + } + break; + } + + case MPV_EVENT_LOG_MESSAGE: { + struct mpv_event_log_message* msg = (struct mpv_event_log_message*)event->data; + QString logMsg = "[" + QString(msg->prefix) + "] " + QString(msg->text).trimmed(); + QString msgLevel = QString(msg->level); + if (msgLevel.startsWith("d")) { + mpvLogger->info("{}", logMsg.toStdString()); + } else if (msgLevel.startsWith("t")) { + mpvLogger->warn("{}", logMsg.toStdString()); + } else if (msgLevel.startsWith("v")) { + mpvLogger->trace("{}", logMsg.toStdString()); + } else if (msgLevel.startsWith("i")) { + mpvLogger->info("{}", logMsg.toStdString()); + } else { + mpvLogger->warn("What: {}", logMsg.toStdString()); + } + + break; + } + case MPV_EVENT_SHUTDOWN: { + qApp->exit(); + break; + } + default: { + break; + } + } +} + +QVariantMap getAudioDevices(const QVariant& drivers) +{ + QVariantMap newDrivers; + if (drivers.isNull()) { + return newDrivers; + } + + QSequentialIterable iterable = drivers.value(); + foreach (const QVariant& v, iterable) { + QVariantMap item = v.toMap(); + newDrivers[item["description"].toString()] = item; + } + return newDrivers; +} +} \ No newline at end of file diff --git a/src/Backends/MPVCommon/MPVCommon.hpp b/src/Backends/MPVCommon/MPVCommon.hpp new file mode 100644 index 0000000..e3710fd --- /dev/null +++ b/src/Backends/MPVCommon/MPVCommon.hpp @@ -0,0 +1,21 @@ +#ifndef MPVCommon_H +#define MPVCommon_H + +#include "src/enums.hpp" +#include +#include +#include +#include +class BackendInterface; +class QMetaMethod; + +namespace MPVCommon { + +QString getStats(BackendInterface* b); +QVariant playerCommand(BackendInterface* b, const Enums::Commands& cmd, const QVariant& args); +void updateDurationString(BackendInterface* b, int numTime, QMetaMethod metaMethod); +void handle_mpv_event(BackendInterface* b, mpv_event* event); +QVariantMap getAudioDevices(const QVariant& drivers); +} + +#endif \ No newline at end of file diff --git a/src/Backends/MPVNoFBO/MPVNoFBOBackend.cpp b/src/Backends/MPVNoFBO/MPVNoFBOBackend.cpp new file mode 100644 index 0000000..3763a11 --- /dev/null +++ b/src/Backends/MPVNoFBO/MPVNoFBOBackend.cpp @@ -0,0 +1,294 @@ +#include "src/Backends/MPVNoFBO/MPVNoFBOBackend.hpp" +#include "src/Backends/MPVCommon/MPVCommon.hpp" +#include "src/qthelper.hpp" +#include "src/utils.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +void nofbowakeup(void* ctx) +{ + QCoreApplication::postEvent((MPVNoFBOBackend*)ctx, new QEvent(QEvent::User)); +} + +static void* +get_proc_address(void* ctx, const char* name) +{ + (void)ctx; + QOpenGLContext* glctx = QOpenGLContext::currentContext(); + if (!glctx) + return NULL; + return (void*)glctx->getProcAddress(QByteArray(name)); +} + +MPVNoFBORenderer::MPVNoFBORenderer(mpv_handle* a_mpv, mpv_opengl_cb_context* a_mpv_gl) + : mpv(a_mpv) + , mpv_gl(a_mpv_gl) + , window(0) + , size() +{ + int r = mpv_opengl_cb_init_gl(mpv_gl, NULL, get_proc_address, NULL); + if (r < 0) + qDebug() << "could not initialize OpenGL"; +} + +MPVNoFBORenderer::~MPVNoFBORenderer() +{ + // Until this call is done, we need to make sure the player remains + // alive. This is done implicitly with the mpv::qt::Handle instance + // in this class. + exit(0); +} + +void MPVNoFBORenderer::paint() +{ + window->resetOpenGLState(); + + // This uses 0 as framebuffer, which indicates that mpv will render directly + // to the frontbuffer. Note that mpv will always switch framebuffers + // explicitly. Some QWindow setups (such as using QQuickWidget) actually + // want you to render into a FBO in the beforeRendering() signal, and this + // code won't work there. + // The negation is used for rendering with OpenGL's flipped coordinates. + mpv_opengl_cb_draw(mpv_gl, 0, size.width(), -size.height()); + + window->resetOpenGLState(); +} + +MPVNoFBOBackend::MPVNoFBOBackend(QQuickItem* parent) + : QQuickItem(parent) + , mpv_gl(0) + , renderer(0) +{ + mpv = mpv_create(); + if (!mpv) + throw std::runtime_error("could not create mpv context"); + + mpv_set_option_string(mpv, "terminal", "no"); + mpv_set_option_string(mpv, "msg-level", "all=v"); + + // Fix? + mpv_set_option_string(mpv, "ytdl", "yes"); + + mpv_set_option_string(mpv, "slang", "en"); + + mpv_set_option_string(mpv, "config", "yes"); + mpv_observe_property(mpv, 0, "tracks-menu", MPV_FORMAT_NONE); + mpv_observe_property(mpv, 0, "chapter-list", MPV_FORMAT_NODE); + mpv_observe_property(mpv, 0, "playback-abort", MPV_FORMAT_NONE); + mpv_observe_property(mpv, 0, "chapter-list", MPV_FORMAT_NODE); + mpv_observe_property(mpv, 0, "track-list", MPV_FORMAT_NODE); + mpv_observe_property(mpv, 0, "audio-device-list", MPV_FORMAT_NODE); + mpv_observe_property(mpv, 0, "playlist-pos", MPV_FORMAT_DOUBLE); + mpv_observe_property(mpv, 0, "volume", MPV_FORMAT_NONE); + mpv_observe_property(mpv, 0, "mute", MPV_FORMAT_NONE); + mpv_observe_property(mpv, 0, "duration", MPV_FORMAT_DOUBLE); + mpv_observe_property(mpv, 0, "media-title", MPV_FORMAT_STRING); + mpv_observe_property(mpv, 0, "time-pos", MPV_FORMAT_DOUBLE); + mpv_observe_property(mpv, 0, "demuxer-cache-duration", MPV_FORMAT_DOUBLE); + mpv_observe_property(mpv, 0, "pause", MPV_FORMAT_NODE); + mpv_observe_property(mpv, 0, "playlist", MPV_FORMAT_NODE); + mpv_observe_property(mpv, 0, "speed", MPV_FORMAT_DOUBLE); + mpv_set_wakeup_callback(mpv, nofbowakeup, this); + + if (mpv_initialize(mpv) < 0) + throw std::runtime_error("could not initialize mpv context"); + + // Make use of the MPV_SUB_API_OPENGL_CB API. + mpv::qt::set_option_variant(mpv, "vo", "opengl-cb"); + + // Setup the callback that will make QtQuick update and redraw if there + // is a new video frame. Use a queued connection: this makes sure the + // doUpdate() function is run on the GUI thread. + mpv_gl = (mpv_opengl_cb_context*)mpv_get_sub_api(mpv, MPV_SUB_API_OPENGL_CB); + if (!mpv_gl) + throw std::runtime_error("OpenGL not compiled in"); + mpv_opengl_cb_set_update_callback( + mpv_gl, MPVNoFBOBackend::on_update, (void*)this); + + connect(this, + &MPVNoFBOBackend::onUpdate, + this, + &MPVNoFBOBackend::doUpdate, + Qt::QueuedConnection); + connect(this, + &MPVNoFBOBackend::positionChanged, + &MPVNoFBOBackend::updateDurationString); + connect(this, + &MPVNoFBOBackend::durationChanged, + &MPVNoFBOBackend::updateDurationString); + + connect(this, + &QQuickItem::windowChanged, + this, + &MPVNoFBOBackend::handleWindowChanged); +} + +MPVNoFBOBackend::~MPVNoFBOBackend() +{ + printf("Shutting down...\n"); + qApp->quit(); + printf("MPV terminated.\n"); +} + +void MPVNoFBOBackend::sync() +{ + + if (!renderer) { + window()->setIcon(QIcon(":/icon.png")); + renderer = new MPVNoFBORenderer(mpv, mpv_gl); + connect(window(), + &QQuickWindow::beforeRendering, + renderer, + &MPVNoFBORenderer::paint, + Qt::DirectConnection); + QMetaObject::invokeMethod(this, "startPlayer"); + } + renderer->window = window(); + renderer->size = window()->size() * window()->devicePixelRatio(); +} + +void MPVNoFBOBackend::swapped() +{ + mpv_opengl_cb_report_flip(mpv_gl, 0); +} + +void MPVNoFBOBackend::cleanup() +{ + if (renderer) { + delete renderer; + renderer = 0; + } +} + +void MPVNoFBOBackend::on_update(void* ctx) +{ + MPVNoFBOBackend* self = (MPVNoFBOBackend*)ctx; + emit self->onUpdate(); +} + +void MPVNoFBOBackend::doUpdate() +{ + window()->update(); + update(); +} + +QVariant +MPVNoFBOBackend::getProperty(const QString& name) const +{ + return mpv::qt::get_property_variant(mpv, name); +} + +void MPVNoFBOBackend::command(const QVariant& params) +{ + mpv::qt::command_variant(mpv, params); +} + +void MPVNoFBOBackend::setProperty(const QString& name, const QVariant& value) +{ + mpv::qt::set_property_variant(mpv, name, value); +} + +void MPVNoFBOBackend::setOption(const QString& name, const QVariant& value) +{ + mpv::qt::set_option_variant(mpv, name, value); +} + +void MPVNoFBOBackend::launchAboutQt() +{ + QApplication* qapp = qobject_cast(QCoreApplication::instance()); + qapp->aboutQt(); +} + +QVariant +MPVNoFBOBackend::playerCommand(const Enums::Commands& cmd) +{ + return playerCommand(cmd, QVariant("NoArgProvided")); +} + +QVariant +MPVNoFBOBackend::playerCommand(const Enums::Commands& cmd, + const QVariant& args) +{ + return MPVCommon::playerCommand(this, cmd, args); +} + +void MPVNoFBOBackend::handleWindowChanged(QQuickWindow* win) +{ + if (!win) + return; + connect(win, + &QQuickWindow::beforeSynchronizing, + this, + &MPVNoFBOBackend::sync, + Qt::DirectConnection); + connect(win, + &QQuickWindow::sceneGraphInvalidated, + this, + &MPVNoFBOBackend::cleanup, + Qt::DirectConnection); + connect(win, + &QQuickWindow::frameSwapped, + this, + &MPVNoFBOBackend::swapped, + Qt::DirectConnection); + win->setClearBeforeRendering(false); +} + +void MPVNoFBOBackend::toggleOnTop() +{ + onTop = !onTop; + Utils::AlwaysOnTop(window()->winId(), onTop); +} + +bool MPVNoFBOBackend::event(QEvent* event) +{ + if (event->type() == QEvent::User) { + on_mpv_events(); + } + return QObject::event(event); +} + +void MPVNoFBOBackend::on_mpv_events() +{ + while (mpv) { + mpv_event* event = mpv_wait_event(mpv, 0); + if (event->event_id == MPV_EVENT_NONE) { + break; + } + handle_mpv_event(event); + } +} + +void MPVNoFBOBackend::updateDurationString(int numTime) +{ + QMetaMethod metaMethod = sender()->metaObject()->method(senderSignalIndex()); + MPVCommon::updateDurationString(this, numTime, metaMethod); +} + +QVariantMap +MPVNoFBOBackend::getAudioDevices(const QVariant& drivers) const +{ + return MPVCommon::getAudioDevices(drivers); +} + +void MPVNoFBOBackend::handle_mpv_event(mpv_event* event) +{ + MPVCommon::handle_mpv_event(this, event); +} + +QString +MPVNoFBOBackend::getStats() +{ + return MPVCommon::getStats(this); +} diff --git a/src/Backends/MPVNoFBO/MPVNoFBOBackend.hpp b/src/Backends/MPVNoFBO/MPVNoFBOBackend.hpp new file mode 100644 index 0000000..b83dec0 --- /dev/null +++ b/src/Backends/MPVNoFBO/MPVNoFBOBackend.hpp @@ -0,0 +1,118 @@ +#ifndef MPVNoFBOBackend_H +#define MPVNoFBOBackend_H + +#include "src/backendinterface.hpp" +#include "src/enums.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class MPVNoFBORenderer : public QObject { + Q_OBJECT + mpv_handle* mpv; + mpv_opengl_cb_context* mpv_gl; + +public: + QQuickWindow* window; + QSize size; + + friend class MpvObject; + MPVNoFBORenderer(mpv_handle* a_mpv, mpv_opengl_cb_context* a_mpv_gl); + virtual ~MPVNoFBORenderer(); +public slots: + void paint(); +}; + +class MPVNoFBOBackend + : public QQuickItem, + public BackendInterface { + Q_INTERFACES(BackendInterface) + + Q_OBJECT + Q_PROPERTY(bool logging READ logging WRITE setLogging) + + mpv_handle* mpv; + mpv_opengl_cb_context* mpv_gl; + MPVNoFBORenderer* renderer; + bool onTop = false; + bool m_logging = true; + QSettings settings; + +public: + static void on_update(void* ctx); + + void setLogging(bool a) + { + if (a != m_logging) { + m_logging = a; + } + } + bool logging() const { return m_logging; } + + MPVNoFBOBackend(QQuickItem* parent = 0); + virtual ~MPVNoFBOBackend(); + + int lastTime = 0; + double lastSpeed = 0; + QString totalDurationString; + QString lastPositionString; + +public slots: + QVariant playerCommand(const Enums::Commands& command, const QVariant& args); + QVariant playerCommand(const Enums::Commands& command); + void launchAboutQt(); + void toggleOnTop(); + QString getStats(); + // Optional but handy for MPV or custom backend settings. + void command(const QVariant& params); + void setProperty(const QString& name, const QVariant& value); + void setOption(const QString& name, const QVariant& value); + QVariant getProperty(const QString& name) const; + + void sync(); + void swapped(); + void cleanup(); + + // Just used for adding missing audio devices to list. + QVariantMap getAudioDevices(const QVariant& drivers) const; + + bool event(QEvent* event); + +signals: + void onUpdate(); + void mpv_events(); + // All below required for Player API + void playStatusChanged(const Enums::PlayStatus& status); + void volumeStatusChanged(const Enums::VolumeStatus& status); + void volumeChanged(const int& volume); + void durationChanged(const double& duration); + void positionChanged(const double& position); + void cachedDurationChanged(const double& duration); + void playlistPositionChanged(const double& position); + void titleChanged(const QString& title); + void durationStringChanged(const QString& string); + void tracksChanged(const QVariantList& tracks); + void audioDevicesChanged(const QVariantMap& devices); + void playlistChanged(const QVariantList& devices); + void chaptersChanged(const QVariantList& devices); + void speedChanged(const double& speed); + +private slots: + void doUpdate(); + void on_mpv_events(); + void updateDurationString(int numTime); + void handleWindowChanged(QQuickWindow* win); + +private: + void handle_mpv_event(mpv_event* event); +}; + +#endif \ No newline at end of file diff --git a/src/Process.cpp b/src/Process.cpp new file mode 100644 index 0000000..5a934fc --- /dev/null +++ b/src/Process.cpp @@ -0,0 +1,25 @@ +#include "Process.h" +#include +#include +class QObject; + +Process::Process(QObject* parent) + : QProcess(parent) +{ +} + +void Process::start(const QString& program, const QVariantList& arguments) +{ + QStringList args; + + for (int i = 0; i < arguments.length(); i++) + args << arguments[i].toString(); + + QProcess::start(program, args); +} + +QString +Process::getOutput() +{ + return QProcess::readAllStandardOutput(); +} diff --git a/src/Process.h b/src/Process.h new file mode 100644 index 0000000..33d8854 --- /dev/null +++ b/src/Process.h @@ -0,0 +1,19 @@ +#ifndef Process_H +#define Process_H + +#include +#include +#include +#include + +class Process : public QProcess { + Q_OBJECT + +public: + explicit Process(QObject* parent = 0); + + Q_INVOKABLE void start(const QString& program, const QVariantList& arguments); + + Q_INVOKABLE QString getOutput(); +}; +#endif \ No newline at end of file diff --git a/src/ThumbnailCache.cpp b/src/ThumbnailCache.cpp new file mode 100644 index 0000000..b500b35 --- /dev/null +++ b/src/ThumbnailCache.cpp @@ -0,0 +1,66 @@ +#include "ThumbnailCache.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +ThumbnailCache::ThumbnailCache(QObject* parent) + : QObject(parent) + , manager(new QNetworkAccessManager(this)) +{ + cacheFolder = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/thumbs"); + if (!cacheFolder.exists()) { + cacheFolder.mkpath("."); + } +} + +void ThumbnailCache::addURL(const QString& name, const QString& mediaURL) +{ + + QString hashedURL = QString( + QCryptographicHash::hash(name.toUtf8(), QCryptographicHash::Md5).toHex()); + QString cacheFilename = hashedURL + ".jpg"; + QString cachedFilePath = cacheFolder.absoluteFilePath(cacheFilename); + if (cacheFolder.exists(cacheFilename)) { + emit thumbnailReady(name, mediaURL, "file://" + cachedFilePath); + return; + } + + QString url(mediaURL); + QFileInfo isFile = QFileInfo(url); + if (isFile.exists()) { + QImageReader reader(url); + QImage image = reader.read(); + + image.save(cachedFilePath, "JPG"); + + emit thumbnailReady(name, mediaURL, "file://" + cachedFilePath); + return; + } + + QNetworkRequest request(url); + + QNetworkReply* reply = manager->get(request); + + connect(reply, &QNetworkReply::finished, [=] { + QByteArray response_data = reply->readAll(); + + QBuffer buffer(&response_data); + buffer.open(QIODevice::ReadOnly); + + QImageReader reader(&buffer); + QImage image = reader.read(); + + image.save(cachedFilePath, "JPG"); + + emit thumbnailReady(name, mediaURL, "file://" + cachedFilePath); + }); +} diff --git a/src/ThumbnailCache.h b/src/ThumbnailCache.h new file mode 100644 index 0000000..6ea404a --- /dev/null +++ b/src/ThumbnailCache.h @@ -0,0 +1,25 @@ +#ifndef ThumbnailCache_H +#define ThumbnailCache_H +#include +#include +#include +#include + +class ThumbnailCache : public QObject { + Q_OBJECT + +public: + explicit ThumbnailCache(QObject* parent = nullptr); + +public slots: + Q_INVOKABLE void addURL(const QString& name, const QString& url); + +signals: + void thumbnailReady(const QString& name, const QString& url, + const QString& filePath); + +private: + QNetworkAccessManager* manager; + QDir cacheFolder; +}; +#endif \ No newline at end of file diff --git a/src/backendinterface.hpp b/src/backendinterface.hpp new file mode 100644 index 0000000..48ee5f7 --- /dev/null +++ b/src/backendinterface.hpp @@ -0,0 +1,46 @@ +#ifndef BackendInterface_H +#define BackendInterface_H +#include "enums.hpp" +#include +class BackendInterface { +public: + virtual ~BackendInterface(){}; + int lastTime = 0; + double lastSpeed = 0; + QString totalDurationString; + QString lastPositionString; + +public slots: + // All 5 required for Player API + virtual QVariant playerCommand(const Enums::Commands& command, + const QVariant& args) + = 0; + virtual QVariant playerCommand(const Enums::Commands& command) = 0; + virtual void toggleOnTop() = 0; + // Optional but handy for MPV or custom backend settings. + virtual void command(const QVariant& params) = 0; + virtual void setProperty(const QString& name, const QVariant& value) = 0; + virtual void setOption(const QString& name, const QVariant& value) = 0; + virtual QVariant getProperty(const QString& name) const = 0; + virtual QVariantMap getAudioDevices(const QVariant& drivers) const = 0; + +signals: + // All below required for Player API + virtual void playStatusChanged(const Enums::PlayStatus& status) = 0; + virtual void volumeStatusChanged(const Enums::VolumeStatus& status) = 0; + virtual void volumeChanged(const int& volume) = 0; + virtual void durationChanged(const double& duration) = 0; + virtual void positionChanged(const double& position) = 0; + virtual void cachedDurationChanged(const double& duration) = 0; + virtual void playlistPositionChanged(const double& position) = 0; + virtual void titleChanged(const QString& title) = 0; + virtual void durationStringChanged(const QString& string) = 0; + virtual void tracksChanged(const QVariantList& tracks) = 0; + virtual void audioDevicesChanged(const QVariantMap& devices) = 0; + virtual void playlistChanged(const QVariantList& devices) = 0; + virtual void chaptersChanged(const QVariantList& devices) = 0; + virtual void speedChanged(const double& speed) = 0; +}; +Q_DECLARE_INTERFACE(BackendInterface, "NamedKitten.BackendInterface"); + +#endif diff --git a/src/enums.cpp b/src/enums.cpp new file mode 100644 index 0000000..90ec0dd --- /dev/null +++ b/src/enums.cpp @@ -0,0 +1 @@ +#include "enums.hpp" \ No newline at end of file diff --git a/src/enums.hpp b/src/enums.hpp new file mode 100644 index 0000000..9a72e30 --- /dev/null +++ b/src/enums.hpp @@ -0,0 +1,62 @@ +#ifndef ENUMS_HPP +#define ENUMS_HPP + +#include +#include + +namespace Enums { +Q_NAMESPACE +enum class PlayStatus : int { + Playing = 0, + Paused = 1 +}; +Q_ENUM_NS(PlayStatus) +enum class VolumeStatus : int { + Muted = 0, + Low = 1, + Normal = 2 +}; +Q_ENUM_NS(VolumeStatus) +enum class Commands : int { + TogglePlayPause = 0, + ToggleMute = 1, + SetAudioDevice = 2, + AddVolume = 3, + SetVolume = 4, + AddSpeed = 5, + SubtractSpeed = 6, + ChangeSpeed = 7, + SetSpeed = 8, + ToggleStats = 9, + NextAudioTrack = 10, + NextVideoTrack = 11, + NextSubtitleTrack = 12, + PreviousPlaylistItem = 13, + NextPlaylistItem = 14, + LoadFile = 15, + AppendFile = 16, + Seek = 17, + SeekAbsolute = 18, + ForwardFrame = 19, + BackwardFrame = 20, + SetTrack = 21, + SetPlaylistPos = 22, + ForcePause = 23, + PreviousChapter = 24, + NextChapter = 25, +}; +Q_ENUM_NS(Commands) + +enum class Backends : int { + MPVBackend = 0, + DirectMPVBackend = 1 +}; +Q_ENUM_NS(Backends) +} + +// Forces meta generation. +class Dummy : public QObject { + Q_OBJECT +}; + +#endif diff --git a/src/logger.cpp b/src/logger.cpp new file mode 100644 index 0000000..11e49fe --- /dev/null +++ b/src/logger.cpp @@ -0,0 +1,43 @@ +// clang-format off +#include // IWYU pragma: export +#include // IWYU pragma: export +#include // IWYU pragma: export +#include // IWYU pragma: export +#include "spdlog/common.h" +#include "spdlog/details/file_helper-inl.h" +#include "spdlog/sinks/ansicolor_sink-inl.h" +#include "spdlog/sinks/base_sink-inl.h" +#include "spdlog/sinks/basic_file_sink-inl.h" +#include "spdlog/spdlog-inl.h" +// clang-format on +#include +#include +#include +#include +#include // IWYU pragma: keep +#include +#include // IWYU pragma: keep +#include + +std::shared_ptr +initLogger(std::string name) +{ + QSettings settings("VideoPlayer", "VideoPlayer"); + + QString logFile = settings.value("Logging/logFile", "/tmp/VideoPlayer.log").toString(); + + std::vector sinks; + + + sinks.push_back(std::make_shared()); + auto fileLogger = std::make_shared(logFile.toUtf8().constData()); + fileLogger->set_level(spdlog::level::trace); + sinks.push_back(fileLogger); + auto console = std::make_shared(name, begin(sinks), end(sinks)); + console->set_pattern("[%l][%n] %v%$"); + spdlog::register_logger(console); + console->set_level(spdlog::level::info); + + + return spdlog::get(name); +} diff --git a/src/logger.h b/src/logger.h new file mode 100644 index 0000000..f849056 --- /dev/null +++ b/src/logger.h @@ -0,0 +1,12 @@ +#ifndef LOGGER_HPP +#define LOGGER_HPP +#include // IWYU pragma: keep +#include // IWYU pragma: keep +#include // IWYU pragma: keep +#include // IWYU pragma: keep +#include // IWYU pragma: keep +#include // IWYU pragma: keep + +std::shared_ptr initLogger(std::string name); + +#endif diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..65765f8 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,135 @@ +#include +#include +#include +#include +#include +#include +#ifdef QT_QML_DEBUG +#warning "QML Debugging Enabled!!!" +#include +#endif +#include "logger.h" +#include "spdlog/logger.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern void registerTypes(); + +#ifdef WIN32 +#include "setenv_mingw.hpp" +#endif + +auto qmlLogger = initLogger("qml"); +auto miscLogger = initLogger("misc"); + +void spdLogger(QtMsgType type, const QMessageLogContext& context, const QString& msg) +{ + std::string localMsg = msg.toUtf8().constData(); + std::shared_ptr logger; + if (QString(context.category).startsWith(QString("qml"))) { + logger = qmlLogger; + } else { + logger = miscLogger; + } + + switch (type) { + case QtDebugMsg: + logger->debug("{}", localMsg); + break; + case QtInfoMsg: + logger->info("{}", localMsg); + break; + case QtWarningMsg: + logger->warn("{}", localMsg); + break; + case QtCriticalMsg: + logger->critical("{}", localMsg); + break; + case QtFatalMsg: + logger->critical("{}", localMsg); + abort(); + } +} + +int main(int argc, char* argv[]) +{ + qInstallMessageHandler(spdLogger); + + auto launcherLogger = initLogger("launcher"); + launcherLogger->info("Starting up!"); + + setenv("QT_QUICK_CONTROLS_STYLE", "Desktop", 1); + QApplication app(argc, argv); + + app.setOrganizationName("VideoPlayer"); + app.setOrganizationDomain("owo.monster"); + app.setApplicationName("VideoPlayer"); + +#ifdef QT_QML_DEBUG + // Allows debug. + QQmlDebuggingEnabler enabler; +#endif + + QSettings settings; + + bool ranFirstTimeSetup = settings.value("Setup/ranSetup", false).toBool(); + +#ifdef __linux__ + bool pinephone; +#ifdef PINEPHONE + pinephone = true; +#else + pinephone = false; +#endif + + // There once was a hacky piece of a code, now its gone. + // TODO: launch a opengl window or use offscreen to see if GL_ARB_framebuffer_object + // can be found + if (!(settings.value("Backend/disableSunxiCheck", false).toBool() || ranFirstTimeSetup) || pinephone ) { + std::string buf; + std::ifstream modulesFd; + + modulesFd.open("/proc/modules"); + + if(modulesFd.is_open()) { + while(!modulesFd.eof()) { + std::getline(modulesFd, buf); + if(buf.find("sunxi") != std::string::npos || buf.find("sun8i") != std::string::npos) { + launcherLogger->info("Running on sunxi, switching to NoFBO."); + settings.setValue("Appearance/clickToPause", false); + settings.setValue("Appearance/doubleTapToSeek", true); + settings.setValue("Appearance/scaleFactor", 2.2); + settings.setValue("Appearance/subtitlesFontSize", 38); + settings.setValue("Appearance/uiFadeTimer", 0); + settings.setValue("Backend/fbo", false); + } + } + } else { + launcherLogger->info("(THIS IS NOT AN ERROR) Cant open /proc/modules."); + } + } +#endif + + settings.setValue("Setup/ranSetup", true); + + QString newpath = QProcessEnvironment::systemEnvironment().value("APPDIR", "") + "/usr/bin:" + QProcessEnvironment::systemEnvironment().value("PATH", ""); + setenv("PATH", newpath.toUtf8().constData(), 1); + + registerTypes(); + + setlocale(LC_NUMERIC, "C"); + launcherLogger->info("Loading player..."); + + QQmlApplicationEngine engine; + engine.load(QUrl(QStringLiteral("qrc:///main.qml"))); + + return app.exec(); +} diff --git a/src/qml/Dialogs/DebugDialog.qml b/src/qml/Dialogs/DebugDialog.qml new file mode 100644 index 0000000..fd63145 --- /dev/null +++ b/src/qml/Dialogs/DebugDialog.qml @@ -0,0 +1,77 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.3 +import QtQuick.Dialogs 1.3 +import QtQuick.Window 2.2 +import player 1.0 + +Dialog { + id: debugDialog + title: "Debug" + height: 480 + width: 720 + modality: Qt.NonModal + standardButtons: Dialog.NoButton + + Component { + id: delegate + Item { + width: 200; height: 30 + Label { + text: theOutput + } + } + } + + ListModel { + id: modelly + } + + ListView { + id: output + model: modelly + delegate: delegate + height: 50 + width: parent.width + anchors { + top: parent.top + bottom: input.top + left: parent.left + right: parent.right + } + } + + + TextField { + id: input + width: parent.width + height: 40 + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + } + text: "h" + function doJsEval() { + var output; + try { + let result = eval(input.text) + output = result instanceof Array ? "[" + String(result) + "]" : String(result) + modelly.append({theOutput: output}) + } catch (e) { + output = String(e) + modelly.append({theOutput: output}) + } + } + + Keys.onReturnPressed: { + doJsEval() + event.accepted = true + } + } + Action { + shortcut: "Ctrl+Shift+i" + onTriggered: { + debugDialog.open() + } + } +} diff --git a/src/qml/Dialogs/PlaylistDialog.qml b/src/qml/Dialogs/PlaylistDialog.qml new file mode 100644 index 0000000..10ef923 --- /dev/null +++ b/src/qml/Dialogs/PlaylistDialog.qml @@ -0,0 +1,220 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.3 +import QtQuick.Dialogs 1.3 +import QtQuick.Window 2.2 +import player 1.0 + +Dialog { + id: playlistDialog + title: "Playlist" + height: Math.max(480, childrenRect.height * playlistListView.count) + width: 720 + modality: Qt.NonModal + property int thumbnailJobsRunning: 0 + property variant thumbnailJobs: [] + property int titleJobsRunning: 0 + property variant titleJobs: [] + + function addThumbnailToCache(name, output) { + output = output.replace("maxresdefault", "sddefault").split('\n')[0] + thumbnailCache.addURL(name, output) + thumbnailJobs.shift() + thumbnailJobsRunning -= 1 + } + + ThumbnailCache { + id: thumbnailCache + } + + Rectangle { + visible: false + id: titleGetter + signal titleFound(string name, string title) + } + + Timer { + interval: 500 + repeat: true + triggeredOnStart: true + running: true + onTriggered: { + if (thumbnailJobsRunning < 2) { + if (thumbnailJobs.length > 0) { + if (thumbnailJobs[0].startsWith( + "https://www.youtube.com/playlist?list=")) { + thumbnailJobs.shift() + return + } + var component = Qt.createComponent("ThumbnailProcess.qml") + var thumbnailerProcess = component.createObject(playlistDialog, { + "name": thumbnailJobs[0] + }) + if (String(thumbnailJobs[0]).indexOf("://") !== -1) { + + thumbnailerProcess.start("youtube-dl", + ["--get-thumbnail", thumbnailJobs[0]]) + } else { + thumbnailerProcess.start( + "ffmpegthumbnailer", + ["-i", thumbnailJobs[0], "-o", "/tmp/" + Qt.md5( + thumbnailJobs[0]) + ".png"]) + } + + thumbnailJobsRunning += 1 + } + } + } + } + + Timer { + interval: 100 + repeat: true + triggeredOnStart: true + running: true + onTriggered: { + if (titleJobsRunning < 5) { + if (titleJobs.length > 0) { + if (titleJobs[0].startsWith("https://www.youtube.com/playlist?list=")) { + titleJobs.shift() + return + } + var component = Qt.createComponent("TitleProcess.qml") + var titleProcess = component.createObject(playlistDialog, { + "name": titleJobs[0] + }) + titleProcess.start("youtube-dl", ["--get-title", titleJobs[0]]) + titleJobs.shift() + titleJobsRunning += 1 + } + } + } + } + + Connections { + target: player + onPlaylistChanged: function (playlist) { + playlistModel.clear() + thumbnailJobs = [] + titleJobs = [] + titleJobsRunning = 0 + thumbnailJobsRunning = 0 + for (var thing in playlist) { + var item = playlist[thing] + playlistModel.append({ + "playlistItemTitle": item["title"], + "playlistItemFilename": item["filename"], + "current": item["current"], + "playlistPos": thing + }) + } + } + } + + Component { + id: playlistDelegate + Item { + id: playlistItem + property string itemURL: "" + property string itemTitle: "" + width: playlistDialog.width + height: childrenRect.height + function getText(title, filename) { + var itemText = "" + if (title.length > 0) { + itemText += 'Title: ' + title + "
" + } + if (filename.length > 0) { + itemText += 'Filename: ' + filename + } + return itemText + } + Connections { + target: thumbnailCache + onThumbnailReady: function (name, url, path) { + console.error(name,url,path,playlistItem.itemURL) + if (name == playlistItem.itemURL) { + thumbnail.source = path + } + } + } + + Connections { + target: titleGetter + onTitleFound: function (name, title) { + if (name == playlistItem.itemURL) { + titleJobsRunning -= 1 + playlistItem.itemTitle = title + } + } + } + + Image { + id: thumbnail + source: "" + height: source.toString().length > 1 ? 144 : 0 + width: source.toString().length > 1 ? 256 : 0 + } + + Button { + width: parent.width - 20 + id: playlistItemButton + font.pixelSize: 12 + padding: 0 + anchors.left: thumbnail.right + bottomPadding: 0 + contentItem: Text { + id: playlistItemText + font: parent.font + color: "white" + text: playlistItem.getText(itemTitle, itemURL) + height: parent.height + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + wrapMode: Text.Wrap + } + + onClicked: { + player.playerCommand(Enums.Commands.SetPlaylistPos, playlistPos) + } + background: Rectangle { + color: current ? "orange" : "transparent" + } + } + + Component.onCompleted: { + if (typeof playlistItemTitle !== "undefined") { + playlistItem.itemTitle = playlistItemTitle + } else { + playlistDialog.titleJobs.push(playlistItemFilename) + } + if (typeof playlistItemFilename !== "undefined") { + playlistItem.itemURL = playlistItemFilename + } else { + playlistItem.itemURL = "" + } + playlistDialog.thumbnailJobs.push(playlistItemFilename) + } + } + } + + ListView { + id: playlistListView + anchors.fill: parent + model: ListModel { + id: playlistModel + } + delegate: playlistDelegate + highlight: Item {} + snapMode: ListView.SnapToItem + flickableDirection: Flickable.VerticalFlick + boundsBehavior: Flickable.StopAtBounds + ScrollBar.vertical: ScrollBar { + active: playlistListView.count > 1 ? true : true + } + focus: true + } + Component.onCompleted: { + playlistDialog.open() + } +} diff --git a/src/qml/Dialogs/SettingsDialog.qml b/src/qml/Dialogs/SettingsDialog.qml new file mode 100644 index 0000000..e02840c --- /dev/null +++ b/src/qml/Dialogs/SettingsDialog.qml @@ -0,0 +1,140 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.3 +import QtQuick.Dialogs 1.3 +import QtQuick.Layouts 1.2 +import QtQuick.Window 2.2 +import player 1.0 + +Dialog { + id: settingsDialog + title: translate.getTranslation("SETTINGS", i18n.language) + height: 100 + width: 720 + modality: Qt.NonModal + + signal done + + ScrollView { + id: content + height: parent.height + width: parent.width + clip: true + ScrollBar.vertical.policy: ScrollBar.AlwaysOn + Item { + id: settingsContent + implicitHeight: childrenRect.height + implicitWidth: childrenRect.width + ColumnLayout { + Text { + height: 30 + text: translate.getTranslation("LANGUAGE", i18n.language) + verticalAlignment: Text.AlignVCenter + } + LanguageSettings { + Layout.leftMargin: 30 + } + Text { + height: 30 + text: translate.getTranslation("APPEARANCE", i18n.language) + verticalAlignment: Text.AlignVCenter + } + CheckBox { + checked: appearance.titleOnlyOnFullscreen + onClicked: appearance.titleOnlyOnFullscreen = !appearance.titleOnlyOnFullscreen + text: translate.getTranslation("TITLE_ONLY_ON_FULLSCREEN", + i18n.language) + Layout.leftMargin: 30 + } + CheckBox { + checked: appearance.doubleTapToSeek + onClicked: appearance.doubleTapToSeek = !appearance.doubleTapToSeek + text: translate.getTranslation("DOUBLE_TAP_TO_SEEK", i18n.language) + Layout.leftMargin: 30 + } + Item { + Layout.leftMargin: 30 + Layout.bottomMargin: 10 + height: 30 + Text { + id: seekByLabel + height: 30 + text: translate.getTranslation("DOUBLE_TAP_TO_SEEK_BY", + i18n.language) + verticalAlignment: Text.AlignVCenter + } + TextField { + id: seekBy + anchors.left: seekByLabel.right + anchors.leftMargin: 10 + validator: IntValidator {} + inputMethodHints: Qt.ImhFormattedNumbersOnly + text: appearance.doubleTapToSeekBy + function setSeekBy() { + appearance.doubleTapToSeekBy = parseInt(seekBy.text) + } + onEditingFinished: setSeekBy() + } + } + Item { + height: 30 + Layout.bottomMargin: 10 + Layout.leftMargin: 30 + Text { + id: fontLabel + height: 30 + text: translate.getTranslation("FONT", i18n.language) + verticalAlignment: Text.AlignVCenter + } + TextField { + id: fontInput + anchors.left: fontLabel.right + anchors.leftMargin: 10 + text: appearance.fontName + function setFont() { + appearance.fontName = fontInput.text + } + onEditingFinished: setFont() + } + } + Item { + Layout.leftMargin: 30 + Layout.bottomMargin: 10 + height: 30 + Text { + id: uiFadeTimeLabel + height: 30 + text: translate.getTranslation("UI_FADE_TIME", i18n.language) + verticalAlignment: Text.AlignVCenter + } + TextField { + id: uiFadeTimeInput + anchors.left: uiFadeTimeLabel.right + anchors.leftMargin: 10 + validator: IntValidator { + bottom: 0 + } + inputMethodHints: Qt.ImhFormattedNumbersOnly + text: appearance.uiFadeTimer + function setUIFadeTime() { + appearance.uiFadeTimer = parseInt(uiFadeTimeInput.text) + } + onEditingFinished: setUIFadeTime() + } + } + } + } + } + + Connections { + target: settingsDialog + onAccepted: { + seekBy.setSeekBy() + fontInput.setFont() + uiFadeTimeInput.setUIFadeTime() + settingsDialog.done() + } + } + Component.onCompleted: { + settingsDialog.open() + } +} diff --git a/src/qml/Dialogs/SettingsItems/LanguageSettings.qml b/src/qml/Dialogs/SettingsItems/LanguageSettings.qml new file mode 100644 index 0000000..cf08957 --- /dev/null +++ b/src/qml/Dialogs/SettingsItems/LanguageSettings.qml @@ -0,0 +1,34 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.3 +import "translations.js" as Translations + +ComboBox { + id: languageSelector + height: 30 + editable: false + pressed: true + model: Object.keys(Translations.languages).map(function (key) { + return Translations.languages[key] + }) + delegate: ItemDelegate { + height: 25 + width: languageSelector.width + contentItem: Text { + text: modelData + color: "#21be2b" + font: languageSelector.font + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } + highlighted: languageSelector.highlightedIndex === index + } + onActivated: { + console.warn(currentText) + i18n.language = Object.keys(Translations.languages).filter(function (key) { + return Translations.languages[key] === currentText + })[0] + } + Component.onCompleted: { + currentIndex = languageSelector.find(Translations.languages[i18n.language]) + } +} diff --git a/src/qml/Items/AudioDeviceItem.qml b/src/qml/Items/AudioDeviceItem.qml new file mode 100644 index 0000000..7721fc5 --- /dev/null +++ b/src/qml/Items/AudioDeviceItem.qml @@ -0,0 +1,12 @@ +import QtQuick.Controls 2.3 +import player 1.0 + +Action { + property string deviceID: "none" + checkable: false + checked: false + + onTriggered: { + player.playerCommand(Enums.Commands.SetAudioDevice, deviceID) + } +} diff --git a/src/qml/Items/ChapterMarkerItem.qml b/src/qml/Items/ChapterMarkerItem.qml new file mode 100644 index 0000000..eac18d9 --- /dev/null +++ b/src/qml/Items/ChapterMarkerItem.qml @@ -0,0 +1,22 @@ +import QtQuick 2.0 + +Rectangle { + id: chapterMarker + property int time: 0 + color: getAppearanceValueForTheme(appearance.themeName, "chapterMarkerColor") + width: 4 + height: parent.height + x: progressBar.background.width / progressBar.to * time + z: 90000 + anchors { + top: parent.top + bottom: parent.bottom + } + + Connections { + target: player + onChaptersChanged: { + chapterMarker.destroy() + } + } +} diff --git a/src/qml/Items/CustomMenuItem.qml b/src/qml/Items/CustomMenuItem.qml new file mode 100644 index 0000000..e0d638d --- /dev/null +++ b/src/qml/Items/CustomMenuItem.qml @@ -0,0 +1,26 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.3 + +MenuItem { + id: menuItem + implicitHeight: 20 + + contentItem: Text { + text: menuItem.text + opacity: 1 + color: menuItem.highlighted ? "#5a50da" : "white" + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + font { + family: appearance.fontName + bold: menuItem.highlighted + } + } + + background: Rectangle { + anchors.fill: parent + opacity: 1 + color: menuItem.highlighted ? "#c0c0f0" : "transparent" + } +} diff --git a/src/qml/Items/ThumbnailProcess.qml b/src/qml/Items/ThumbnailProcess.qml new file mode 100644 index 0000000..e57cd13 --- /dev/null +++ b/src/qml/Items/ThumbnailProcess.qml @@ -0,0 +1,13 @@ +import player 1.0 + +Process { + id: thumbnailerProcess + property string name: "" + onFinished: function () { + if (String(name).indexOf("://") !== -1) { + playlistDialog.addThumbnailToCache(name, getOutput()) + } else { + playlistDialog.addThumbnailToCache(name, "/tmp/" + Qt.md5(name) + ".png") + } + } +} diff --git a/src/qml/Items/TitleProcess.qml b/src/qml/Items/TitleProcess.qml new file mode 100644 index 0000000..5a014c4 --- /dev/null +++ b/src/qml/Items/TitleProcess.qml @@ -0,0 +1,9 @@ +import player 1.0 + +Process { + id: titleProcess + property string name: "" + onReadyRead: function () { + titleGetter.titleFound(name, getOutput()) + } +} diff --git a/src/qml/Items/TrackItem.qml b/src/qml/Items/TrackItem.qml new file mode 100644 index 0000000..0314ce1 --- /dev/null +++ b/src/qml/Items/TrackItem.qml @@ -0,0 +1,14 @@ +import QtQuick.Controls 2.3 +import player 1.0 + +Action { + id: trackItem + property string trackType: "none" + property string trackID: "none" + checkable: true + checked: false + + onTriggered: { + player.playerCommand(Enums.Commands.SetTrack, [trackType, trackID]) + } +} diff --git a/src/qml/UIComponents/Controls/BackwardButton.qml b/src/qml/UIComponents/Controls/BackwardButton.qml new file mode 100644 index 0000000..eded69f --- /dev/null +++ b/src/qml/UIComponents/Controls/BackwardButton.qml @@ -0,0 +1,8 @@ +import player 1.0 + +SmoothButton { + iconSource: "icons/" + appearance.themeName + "/backward.svg" + onClicked: { + player.playerCommand(Enums.Commands.Seek, "-10") + } +} diff --git a/src/qml/UIComponents/Controls/ForwardButton.qml b/src/qml/UIComponents/Controls/ForwardButton.qml new file mode 100644 index 0000000..0db9a8a --- /dev/null +++ b/src/qml/UIComponents/Controls/ForwardButton.qml @@ -0,0 +1,8 @@ +import player 1.0 + +SmoothButton { + iconSource: "icons/" + appearance.themeName + "/forward.svg" + onClicked: { + player.playerCommand(Enums.Commands.Seek, "10") + } +} diff --git a/src/qml/UIComponents/Controls/FullscreenButton.qml b/src/qml/UIComponents/Controls/FullscreenButton.qml new file mode 100644 index 0000000..e358405 --- /dev/null +++ b/src/qml/UIComponents/Controls/FullscreenButton.qml @@ -0,0 +1,6 @@ +SmoothButton { + iconSource: "icons/" + appearance.themeName + "/fullscreen.svg" + onClicked: { + toggleFullscreen() + } +} diff --git a/src/qml/UIComponents/Controls/PlayPauseButton.qml b/src/qml/UIComponents/Controls/PlayPauseButton.qml new file mode 100644 index 0000000..357b9bb --- /dev/null +++ b/src/qml/UIComponents/Controls/PlayPauseButton.qml @@ -0,0 +1,17 @@ +import QtQuick 2.0 +import player 1.0 + +SmoothButton { + property var playing: Enums.PlayStatus.Playing + iconSource: "icons/" + appearance.themeName + + (playing == Enums.PlayStatus.Playing ? "/pause.svg" : "/play.svg") + onClicked: { + player.playerCommand(Enums.Commands.TogglePlayPause) + } + Connections { + target: player + onPlayStatusChanged: function (status) { + playing = status + } + } +} diff --git a/src/qml/UIComponents/Controls/PlaylistNextButton.qml b/src/qml/UIComponents/Controls/PlaylistNextButton.qml new file mode 100644 index 0000000..903461f --- /dev/null +++ b/src/qml/UIComponents/Controls/PlaylistNextButton.qml @@ -0,0 +1,8 @@ +import player 1.0 + +SmoothButton { + iconSource: "icons/" + appearance.themeName + "/next.svg" + onClicked: { + player.playerCommand(Enums.Commands.NextPlaylistItem) + } +} diff --git a/src/qml/UIComponents/Controls/PlaylistPrevButton.qml b/src/qml/UIComponents/Controls/PlaylistPrevButton.qml new file mode 100644 index 0000000..6e71c42 --- /dev/null +++ b/src/qml/UIComponents/Controls/PlaylistPrevButton.qml @@ -0,0 +1,23 @@ +import QtQuick 2.0 +import player 1.0 + +SmoothButton { + id: playlistPrevButton + iconSource: "icons/" + appearance.themeName + "/prev.svg" + visible: appearance.themeName == "Youtube" ? false : true + onClicked: { + player.playerCommand(Enums.Commands.PreviousPlaylistItem) + } + Connections { + target: player + onPlaylistPositionChanged: function (position) { + if (appearance.themeName == "YouTube") { + if (position != 0) { + visible = true + } else { + visible = false + } + } + } + } +} diff --git a/src/qml/UIComponents/Controls/SettingsButton.qml b/src/qml/UIComponents/Controls/SettingsButton.qml new file mode 100644 index 0000000..223ef09 --- /dev/null +++ b/src/qml/UIComponents/Controls/SettingsButton.qml @@ -0,0 +1,21 @@ +import player 1.0 + +SmoothButton { + id: settingsButton + iconSource: "icons/" + appearance.themeName + "/settings.svg" + onClicked: { + switch (appearance.themeName) { + case "YouTube": + appearance.themeName = "RoosterTeeth" + break + case "RoosterTeeth": + appearance.themeName = "Niconico" + break + case "Niconico": + appearance.themeName = "YouTube" + break + default: + appearance.themeName = "YouTube" + } + } +} diff --git a/src/qml/UIComponents/Controls/SmoothButton.qml b/src/qml/UIComponents/Controls/SmoothButton.qml new file mode 100644 index 0000000..de5f798 --- /dev/null +++ b/src/qml/UIComponents/Controls/SmoothButton.qml @@ -0,0 +1,50 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.3 +import QtGraphicalEffects 1.0 + +Control { + id: root + hoverEnabled: true + property alias iconSource: buttonImage.source + property alias containsMouse: mouseArea.containsMouse + + background: null + + focusPolicy: Qt.NoFocus + + signal clicked + leftPadding: root.height / (appearance.themeName == "Niconico" ? 2.5 : 12) + rightPadding: root.leftPadding + + contentItem: Image { + id: buttonImage + smooth: false + fillMode: Image.PreserveAspectFit + sourceSize.height: Math.floor( + root.parent.height / (appearance.themeName == "Niconico" ? 1.8 : 1.25)) + sourceSize.width: Math.floor( + root.parent.height / (appearance.themeName == "Niconico" ? 1.8 : 1.25)) + } + ColorOverlay { + id: colorOverlay + anchors.fill: buttonImage + source: buttonImage + color: getAppearanceValueForTheme(appearance.themeName, "buttonColor") + cached: true + Binding on color { + when: root.hovered + value: root.hovered ? getAppearanceValueForTheme( + appearance.themeName, + "buttonHoverColor") : getAppearanceValueForTheme( + appearance.themeName, "buttonColor") + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + propagateComposedEvents: true + onClicked: root.clicked() + } +} diff --git a/src/qml/UIComponents/Controls/SpeedText.qml b/src/qml/UIComponents/Controls/SpeedText.qml new file mode 100644 index 0000000..7100a3e --- /dev/null +++ b/src/qml/UIComponents/Controls/SpeedText.qml @@ -0,0 +1,32 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.3 +import player 1.0 + +Text { + id: speedText + text: "1x" + verticalAlignment: Text.AlignVCenter + color: speedStatusMouseArea.containsMouse ? getAppearanceValueForTheme( + appearance.themeName, + "buttonHoverColor") : getAppearanceValueForTheme( + appearance.themeName, + "buttonColor") + font { + family: appearance.fontName + pixelSize: layout.height / 2.5 + } + Connections { + target: player + onSpeedChanged: function (speed) { + text = String(speed) + "x" + } + } + MouseArea { + id: speedStatusMouseArea + anchors.fill: parent + height: parent.height + hoverEnabled: true + propagateComposedEvents: true + acceptedButtons: Qt.NoButton + } +} diff --git a/src/qml/UIComponents/Controls/TimeLabel.qml b/src/qml/UIComponents/Controls/TimeLabel.qml new file mode 100644 index 0000000..c92c368 --- /dev/null +++ b/src/qml/UIComponents/Controls/TimeLabel.qml @@ -0,0 +1,18 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.3 + +Text { + id: timeLabel + text: "0:00 / 0:00" + color: "white" + font.family: appearance.fontName + font.pixelSize: layout.height / 2.5 + verticalAlignment: Text.AlignVCenter + renderType: Text.NativeRendering + Connections { + target: player + onDurationStringChanged: function (durationString) { + timeLabel.text = durationString + } + } +} diff --git a/src/qml/UIComponents/Controls/VerticalVolume.qml b/src/qml/UIComponents/Controls/VerticalVolume.qml new file mode 100644 index 0000000..5099bce --- /dev/null +++ b/src/qml/UIComponents/Controls/VerticalVolume.qml @@ -0,0 +1,71 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.3 +import player 1.0 + +Rectangle { + id: volumeSliderArea + height: visible ? 70 : 0 + color: getAppearanceValueForTheme(appearance.themeName, "mainBackground") + visible: false + Slider { + id: volumeSlider + anchors.fill: parent + to: 100 + value: 100 + + orientation: Qt.Vertical + + implicitWidth: Math.max( + background ? background.implicitWidth : 0, + (handle ? handle.implicitWidth : 0) + leftPadding + rightPadding) + implicitHeight: Math.max(background.implicitHeight, + handle.implicitHeight + topPadding + bottomPadding) + + padding: 6 + + Connections { + target: player + onVolumeChanged: function (volume) { + volumeSlider.value = volume + } + } + + onMoved: { + player.playerCommand(Enums.Commands.SetVolume, + Math.round(volumeSlider.value).toString()) + } + + handle: Rectangle { + x: volumeSlider.leftPadding + ((volumeSlider.availableWidth - width) / 2) + y: volumeSlider.topPadding + (volumeSlider.visualPosition + * (volumeSlider.availableHeight - height)) + implicitWidth: 10 + implicitHeight: 10 + radius: width / 2 + color: "white" + border.width: 0 + } + + background: Rectangle { + x: volumeSlider.leftPadding + ((volumeSlider.availableWidth - width) / 2) + y: volumeSlider.topPadding + implicitWidth: 4 + implicitHeight: 70 + width: implicitWidth + height: volumeSlider.availableHeight + radius: 3 + color: getAppearanceValueForTheme(appearance.themeName, + "progressBackgroundColor") + + Rectangle { + y: volumeSlider.visualPosition * parent.height + width: 4 + height: volumeSlider.position * parent.height + + radius: 3 + color: getAppearanceValueForTheme(appearance.themeName, + "volumeSliderBackground") + } + } + } +} diff --git a/src/qml/UIComponents/Controls/VideoProgress.qml b/src/qml/UIComponents/Controls/VideoProgress.qml new file mode 100644 index 0000000..4d59de8 --- /dev/null +++ b/src/qml/UIComponents/Controls/VideoProgress.qml @@ -0,0 +1,207 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.3 +import player 1.0 + +Slider { + id: progressBar + objectName: "progressBar" + property string currentMediaURL: "" + property bool playing: false + property bool center: false + to: 1 + value: 0.0 + + Rectangle { + id: timestampBox + visible: false + width: hoverProgressLabel.width + height: hoverProgressLabel.height + z: 100 + color: getAppearanceValueForTheme(appearance.themeName, "mainBackground") + Text { + id: hoverProgressLabel + text: "0:00" + color: "white" + font.family: appearance.fontName + font.pixelSize: mainWindow.virtualHeight / 50 + horizontalAlignment: Text.AlignHCenter + renderType: Text.NativeRendering + } + } + + Connections { + target: player + onPlayStatusChanged: function (status) { + if (status == Enums.PlayStatus.Playing) { + progressBar.playing = true + } else if (status == Enums.PlayStatus.Paused) { + progressBar.playing = false + } + } + onPositionChanged: function (position) { + if (!pressed) { + progressBar.value = position + } + } + onDurationChanged: function (duration) { + progressBar.to = duration + } + onCachedDurationChanged: function (duration) { + cachedLength.duration = duration + } + } + onMoved: { + player.playerCommand(Enums.Commands.SeekAbsolute, value) + } + + function getProgressBarHeight(nyan, isMouse) { + var x = fun.nyanCat ? mainWindow.virtualHeight / 64 : mainWindow.virtualHeight / 380 + if (appearance.themeName == "Niconico" && !fun.nyanCat) { + return x * 2 + } else if (isMouse & !fun.nyanCat) { + return x * 2 + } else { + return x + } + } + function getHandleVisibility(themeName, isMouse) { + if (fun.nyanCat) { + return true + } + if (appearance.themeName == "Niconico") { + return isMouse + } else { + return true + } + } + MouseArea { + id: mouseAreaProgressBar + width: progressBar.width + height: parent.height + anchors.fill: parent + + hoverEnabled: true + propagateComposedEvents: true + acceptedButtons: Qt.NoButton + z: 100 + property string currentTime: "" + + onEntered: timestampBox.visible = true + onExited: timestampBox.visible = false + + onPositionChanged: { + // code taken from https://github.com/qt/qtquickcontrols2/blob/39892547145ba4e73bebee86352bd384732b5d19/src/quicktemplates2/qquickslider.cpp#L138 + var a = ((mouseAreaProgressBar.mouseX - (handleRect.width / 2)) + / (progressBar.availableWidth - handleRect.width)) * progressBar.to + hoverProgressLabel.text = utils.createTimestamp(a) + timestampBox.x = mouseAreaProgressBar.mouseX - (timestampBox.width / 2) + timestampBox.y = progressBackground.y - timestampBox.height * 2 + } + } + + background: Rectangle { + anchors.bottom: parent.bottom + anchors.bottomMargin: progressBar.center ? (progressBar.height / 2) - (height / 2) : 0 + id: progressBackground + z: 30 + width: progressBar.availableWidth + height: progressBar.getProgressBarHeight(fun.nyanCat, + mouseAreaProgressBar.containsMouse) + color: getAppearanceValueForTheme(appearance.themeName, + "progressBackgroundColor") + + ProgressBar { + id: cachedLength + background: null + contentItem: Item { + Rectangle { + width: cachedLength.visualPosition * parent.width + height: parent.height + color: getAppearanceValueForTheme(appearance.themeName, + "progressCachedColor") + AnimatedImage { + visible: fun.nyanCat + height: parent.height + id: nyancacheimation + smooth: false + anchors.fill: parent + source: "qrc:/icons/nyancache.gif" + fillMode: Image.TileHorizontally + } + } + } + z: 40 + to: progressBar.to + property int duration + value: progressBar.value + duration + anchors.fill: parent + } + + Item { + anchors.fill: parent + id: chapterMarkers + z: 60 + Connections { + target: player + onChaptersChanged: function (chapters) { + for (var i = 0, len = chapters.length; i < len; i++) { + var component = Qt.createComponent("ChapterMarkerItem.qml") + + var marker = component.createObject(chapterMarkers, { + "time": chapters[i]["time"] + }) + if (marker == null) { + console.error(component.errorString()) + } + } + } + } + } + + Rectangle { + id: progressLength + z: 50 + anchors.left: progressBackground.left + width: progressBar.visualPosition * parent.width + height: parent.height + color: getAppearanceValueForTheme(appearance.themeName, + "progressSliderColor") + Image { + visible: fun.nyanCat + id: rainbow + anchors.fill: parent + height: parent.height + width: parent.width + source: "qrc:/icons/rainbow.png" + fillMode: Image.TileHorizontally + } + } + } + + handle: Rectangle { + z: 70 + id: handleRect + x: progressBar.visualPosition * (progressBar.availableWidth - width) + anchors.verticalCenter: parent.background.verticalCenter + implicitHeight: radius + implicitWidth: radius + radius: mainWindow.virtualHeight / 59 + color: appearance.themeName + == "RoosterTeeth" ? "white" : fun.nyanCat ? "transparent" : getAppearanceValueForTheme( + appearance.themeName, + "progressSliderColor") + visible: getHandleVisibility(appearance.themeName, + mouseAreaProgressBar.containsMouse) + AnimatedImage { + z: 80 + visible: fun.nyanCat + paused: progressBar.pressed + height: mainWindow.virtualHeight / 28 + id: nyanimation + smooth: false + anchors.centerIn: parent + source: "qrc:/icons/nyancat.gif" + fillMode: Image.PreserveAspectFit + } + } +} diff --git a/src/qml/UIComponents/Controls/VolumeButton.qml b/src/qml/UIComponents/Controls/VolumeButton.qml new file mode 100644 index 0000000..36fa59f --- /dev/null +++ b/src/qml/UIComponents/Controls/VolumeButton.qml @@ -0,0 +1,22 @@ +import QtQuick 2.0 +import player 1.0 + +SmoothButton { + id: volumeButton + iconSource: "icons/" + appearance.themeName + "/volume-up.svg" + onClicked: { + player.playerCommand(Enums.Commands.ToggleMute) + } + Connections { + target: player + onVolumeStatusChanged: function (status) { + if (status == Enums.VolumeStatus.Muted) { + volumeButton.iconSource = "qrc:/icons/" + appearance.themeName + "/volume-mute.svg" + } else if (status == Enums.VolumeStatus.Low) { + volumeButton.iconSource = "qrc:/icons/" + appearance.themeName + "/volume-down.svg" + } else if (status == Enums.VolumeStatus.Normal) { + volumeButton.iconSource = "qrc:/icons/" + appearance.themeName + "/volume-up.svg" + } + } + } +} diff --git a/src/qml/UIComponents/Controls/VolumeSlider.qml b/src/qml/UIComponents/Controls/VolumeSlider.qml new file mode 100644 index 0000000..28f1295 --- /dev/null +++ b/src/qml/UIComponents/Controls/VolumeSlider.qml @@ -0,0 +1,69 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.3 +import player 1.0 + +Slider { + id: volumeBar + to: 100 + value: 100 + palette.dark: "#f00" + hoverEnabled: true + + implicitWidth: Math.max( + background ? background.implicitWidth : 0, + (handle ? handle.implicitWidth : 0) + leftPadding + rightPadding) + implicitHeight: Math.max( + background ? background.implicitHeight : 0, + (handle ? handle.implicitHeight : 0) + topPadding + bottomPadding) + onMoved: { + player.playerCommand(Enums.Commands.SetVolume, + Math.round(volumeBar.value).toString()) + } + Connections { + target: player + onVolumeChanged: function (volume) { + volumeBar.value = volume + } + } + handle: Rectangle { + x: volumeBar.leftPadding + volumeBar.visualPosition * (volumeBar.availableWidth - width) + y: volumeBar.topPadding + volumeBar.availableHeight / 2 - height / 2 + implicitWidth: height + implicitHeight: layout.height / 2.6 + radius: height + visible: appearance.themeName == "Niconico" ? false : true + color: "#f6f6f6" + border.color: "#f6f6f6" + } + + background: Rectangle { + x: volumeBar.leftPadding + y: volumeBar.topPadding + volumeBar.availableHeight / 2 - height / 2 + implicitWidth: layout.width / 11 + implicitHeight: appearance.themeName == "Niconico" ? layout.height / 6 : layout.height / 10 + width: volumeBar.availableWidth + height: implicitHeight + color: getAppearanceValueForTheme(appearance.themeName, + "progressBackgroundColor") + Rectangle { + width: volumeBar.visualPosition * parent.width + height: parent.height + color: getAppearanceValueForTheme(appearance.themeName, + "volumeSliderBackground") + } + + MouseArea { + acceptedButtons: Qt.NoButton + z: 10 + anchors.fill: parent + propagateComposedEvents: true + onWheel: { + if (wheel.angleDelta.y < 0) { + volumeBar.value -= 5 + } else { + volumeBar.value += 5 + } + } + } + } +} diff --git a/src/qml/UIComponents/ControlsBar/ControlsBar.qml b/src/qml/UIComponents/ControlsBar/ControlsBar.qml new file mode 100644 index 0000000..a54520d --- /dev/null +++ b/src/qml/UIComponents/ControlsBar/ControlsBar.qml @@ -0,0 +1,87 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.2 +import player 1.0 + +Item { + id: controlsBarItem + property var combinedHeight: progressBar.height + controlsBackground.height + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + } + + Connections { + target: appearance + onThemeNameChanged: setControlsTheme(appearance.themeName) + } + + function setControlsTheme(themeName) { + for (var i = 0; i < controlsBar.children.length; ++i) { + if (controlsBar.children[i].objectName == "buttonLayout") { + controlsBar.children[i].destroy() + } + } + + var component = Qt.createComponent(themeName + "ButtonLayout.qml") + if (component.status == Component.Error) { + console.error("Error loading component: " + component.errorString()) + } + component.createObject(controlsBar, {}) + } + + VideoProgress { + id: progressBar + visible: mainWindow.controlsShowing + && appearance.themeName != "RoosterTeeth" + bottomPadding: 0 + rightPadding: 0 + leftPadding: 0 + z: 20 + anchors { + bottom: controlsBackground.top + left: controlsBackground.left + right: controlsBackground.right + leftMargin: parent.width / 128 + rightMargin: parent.width / 128 + bottomMargin: 0 + } + } + + Rectangle { + id: controlsBackground + height: controlsBar.visible ? controlsBar.height + + (appearance.themeName + == "RoosterTeeth" ? 0 : progressBar.topPadding) : 0 + Layout.fillWidth: true + Layout.fillHeight: true + color: getAppearanceValueForTheme(appearance.themeName, "mainBackground") + visible: mainWindow.controlsShowing + z: 10 + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + } + } + + Item { + id: controlsBar + height: mainWindow.controlsShowing ? mainWindow.virtualHeight / 20 : 0 + visible: mainWindow.controlsShowing + z: 30 + anchors { + right: parent.right + rightMargin: parent.width / 128 + left: parent.left + leftMargin: parent.width / 128 + bottom: parent.bottom + bottomMargin: 0 + } + } + + Component.onCompleted: { + setControlsTheme(appearance.themeName) + } +} diff --git a/src/qml/UIComponents/ControlsLayouts/NiconicoButtonLayout.qml b/src/qml/UIComponents/ControlsLayouts/NiconicoButtonLayout.qml new file mode 100644 index 0000000..338db53 --- /dev/null +++ b/src/qml/UIComponents/ControlsLayouts/NiconicoButtonLayout.qml @@ -0,0 +1,90 @@ +import QtQuick 2.0 +import player 1.0 + +Item { + objectName: "buttonLayout" + id: layout + anchors.fill: controlsBar + + PlayPauseButton { + id: playPauseButton + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + } + } + VolumeButton { + id: volumeButton + anchors { + left: playPauseButton.right + top: parent.top + bottom: parent.bottom + } + } + VolumeSlider { + anchors { + left: volumeButton.right + top: parent.top + bottom: parent.bottom + } + } + + PlaylistPrevButton { + id: playlistPrevButton + anchors { + right: backwardButton.left + top: parent.top + bottom: parent.bottom + } + } + BackwardButton { + id: backwardButton + anchors { + right: timeLabel.left + top: parent.top + bottom: parent.bottom + } + } + TimeLabel { + id: timeLabel + anchors { + centerIn: parent + top: parent.top + bottom: parent.bottom + } + } + ForwardButton { + id: forwardButton + anchors { + left: timeLabel.right + top: parent.top + bottom: parent.bottom + } + } + PlaylistNextButton { + id: playlistNextButton + anchors { + left: forwardButton.right + top: parent.top + bottom: parent.bottom + } + } + + FullscreenButton { + id: fullscreenButton + anchors { + right: settingsButton.left + top: parent.top + bottom: parent.bottom + } + } + SettingsButton { + id: settingsButton + anchors { + right: parent.right + top: parent.top + bottom: parent.bottom + } + } +} diff --git a/src/qml/UIComponents/ControlsLayouts/RoosterTeethButtonLayout.qml b/src/qml/UIComponents/ControlsLayouts/RoosterTeethButtonLayout.qml new file mode 100644 index 0000000..2e7b8ae --- /dev/null +++ b/src/qml/UIComponents/ControlsLayouts/RoosterTeethButtonLayout.qml @@ -0,0 +1,111 @@ +import QtQuick 2.0 +import player 1.0 + +Item { + objectName: "buttonLayout" + id: layout + anchors.fill: controlsBar + + PlayPauseButton { + id: playPauseButton + anchors { + top: parent.top + bottom: parent.bottom + left: parent.left + } + } + + MouseArea { + id: mouseAreaVolumeArea + anchors { + right: volumeSliderArea.right + bottom: volumeButton.bottom + left: volumeButton.left + } + height: parent.height + (volumeSliderArea.visible ? volumeSliderArea.height : 0) + hoverEnabled: true + z: 500 + propagateComposedEvents: true + acceptedButtons: Qt.NoButton + onEntered: { + mouseAreaPlayerTimer.stop() + } + onExited: { + mouseAreaPlayerTimer.restart() + } + } + + VolumeButton { + id: volumeButton + anchors { + left: playPauseButton.right + top: parent.top + bottom: parent.bottom + } + hoverEnabled: true + } + + VerticalVolume { + id: volumeSliderArea + anchors { + bottom: volumeButton.top + left: volumeButton.left + right: volumeButton.right + } + width: volumeButton.width + visible: mouseAreaVolumeArea.containsMouse || volumeButton.hovered + } + + TimeLabel { + id: timeLabel + anchors { + left: volumeButton.right + top: parent.top + bottom: parent.bottom + } + } + + VideoProgress { + id: videoProgressRoosterTeeth + anchors { + top: parent.top + bottom: parent.bottom + left: timeLabel.right + leftMargin: parent.width / 128 + right: speedText.left + rightMargin: parent.width / 128 + } + rightPadding: 0 + leftPadding: 0 + height: parent.height + to: progressBar.to + value: progressBar.value + center: true + } + + SpeedText { + id: speedText + anchors { + top: parent.top + bottom: parent.bottom + right: fullscreenButton.left + } + } + + FullscreenButton { + id: fullscreenButton + anchors { + right: settingsButton.left + top: parent.top + bottom: parent.bottom + } + } + SettingsButton { + id: settingsButton + anchors { + right: parent.right + top: parent.top + bottom: parent.bottom + } + } +} diff --git a/src/qml/UIComponents/ControlsLayouts/YouTubeButtonLayout.qml b/src/qml/UIComponents/ControlsLayouts/YouTubeButtonLayout.qml new file mode 100644 index 0000000..99d5ab2 --- /dev/null +++ b/src/qml/UIComponents/ControlsLayouts/YouTubeButtonLayout.qml @@ -0,0 +1,92 @@ +import QtQuick 2.0 +import player 1.0 + +Item { + objectName: "buttonLayout" + id: layout + anchors.fill: controlsBar + height: parent.height + + PlaylistPrevButton { + id: playlistPrevButton + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + } + width: visible ? playlistNextButton.width : 0 + } + PlayPauseButton { + id: playPauseButton + anchors { + left: playlistPrevButton.right + top: parent.top + bottom: parent.bottom + } + leftPadding: 14 + } + PlaylistNextButton { + id: playlistNextButton + anchors { + left: playPauseButton.right + top: parent.top + bottom: parent.bottom + } + } + + MouseArea { + id: mouseAreaVolumeArea + anchors.fill: volumeSlider + width: volumeSlider.width + hoverEnabled: true + propagateComposedEvents: true + acceptedButtons: Qt.NoButton + z: 100 + } + + VolumeButton { + id: volumeButton + anchors { + left: playlistNextButton.right + top: parent.top + bottom: parent.bottom + } + z: 50 + } + VolumeSlider { + id: volumeSlider + anchors { + left: volumeButton.right + top: parent.top + bottom: parent.bottom + } + height: parent.height + visible: mouseAreaVolumeArea.containsMouse || volumeButton.hovered + width: visible ? implicitWidth : 0 + } + TimeLabel { + anchors { + left: volumeSlider.right + top: parent.top + bottom: parent.bottom + leftMargin: parent.width / 128 + } + } + + SettingsButton { + id: settingsButton + anchors { + right: fullscreenButton.left + top: parent.top + bottom: parent.bottom + } + } + FullscreenButton { + id: fullscreenButton + anchors { + right: parent.right + top: parent.top + bottom: parent.bottom + } + } +} diff --git a/src/qml/UIComponents/MenuBar/CustomMenu.qml b/src/qml/UIComponents/MenuBar/CustomMenu.qml new file mode 100644 index 0000000..c7e7884 --- /dev/null +++ b/src/qml/UIComponents/MenuBar/CustomMenu.qml @@ -0,0 +1,12 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.3 + +Menu { + width: 300 + background: Rectangle { + implicitWidth: parent.width + implicitHeight: 10 + color: getAppearanceValueForTheme(appearance.themeName, "mainBackground") + } + delegate: CustomMenuItem {} +} diff --git a/src/qml/UIComponents/MenuBar/MainMenu.qml b/src/qml/UIComponents/MenuBar/MainMenu.qml new file mode 100644 index 0000000..9f1c5c1 --- /dev/null +++ b/src/qml/UIComponents/MenuBar/MainMenu.qml @@ -0,0 +1,603 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.3 +import QtQuick.Dialogs 1.3 +import Qt.labs.platform 1.0 as LabsPlatform +import player 1.0 + +import "codes.js" as LanguageCodes + +MenuBar { + id: menuBar + height: mainWindow.virtualHeight / 32 + function anythingOpen() { + for (var i = 0, len = menuBar.count; i < len; i++) { + if (menuBar.menuAt(i).opened) { + return true + } + } + return false + } + + Connections { + target: player + onTracksChanged: function (tracks) { + menuBar.updateTracks(tracks) + } + } + + function updateTracks(tracks) { + var newTracks = tracks + var trackMenus = [audioMenu, videoMenu, subMenu] + for (var a = 0; a < trackMenus.length; a++) { + var menu = trackMenus[a] + for (var i = 0, len = menu.count; i < len; i++) { + var action = menu.actionAt(i) + if (action) { + if (action.trackID != "no") { + menu.removeAction(action) + } + } + } + } + + for (var i = 0, len = newTracks.length; i < len; i++) { + var track = newTracks[i] + var trackID = track["id"] + var trackType = track["type"] + var trackLang = LanguageCodes.localeCodeToEnglish(String(track["lang"])) + trackLang = trackLang == undefined ? "" : trackLang + var trackTitle = track["title"] == undefined ? "" : track["title"] + " " + var component = Qt.createComponent("TrackItem.qml") + var menu, menuGroup, itemText, type; + if (trackType == "sub") { + menu = subMenu; + menuGroup = subMenuGroup; + itemText = trackLang; + } else if (trackType == "audio") { + menu = audioMenu; + menuGroup = audioMenuGroup; + itemText = trackTitle + trackLang; + } else if (trackType == "video") { + menu = videoMenu; + menuGroup = videoMenuGroup; + itemText = "Video " + trackID + trackTitle; + } + var action = component.createObject(menu, { + "text": itemText, + "trackID": String(trackID), + "trackType": trackType == "sub" ? "sid" : trackType == "video" ? "vid" : "aid", + "checked": track["selected"] + }) + action.ActionGroup.group = menuGroup + menu.addAction(action) + } + } + + FileDialog { + id: fileDialog + title: translate.getTranslation("OPEN_FILE", i18n.language) + nameFilters: ["All files (*)"] + selectMultiple: false + onAccepted: { + player.playerCommand(Enums.Commands.LoadFile, String(fileDialog.fileUrl)) + fileDialog.close() + } + onRejected: { + fileDialog.close() + } + } + + Dialog { + id: loadDialog + title: translate.getTranslation("URL_FILE_PATH", i18n.language) + standardButtons: StandardButton.Cancel | StandardButton.Open + onAccepted: { + player.playerCommand(Enums.Commands.LoadFile, pathText.text) + pathText.text = "" + } + TextField { + id: pathText + placeholderText: translate.getTranslation("URL_FILE_PATH", i18n.language) + } + } + + Loader { + id: playlistDialogLoader + active: false + source: "PlaylistDialog.qml" + } + Connections { + target: playlistDialogLoader.item + onDone: { + playlistDialogLoader.active = false + } + } + + Loader { + id: settingsDialogLoader + active: false + source: "SettingsDialog.qml" + } + Connections { + target: settingsDialogLoader.item + onDone: { + settingsDialogLoader.active = false + } + } + + delegate: MenuBarItem { + id: menuBarItem + padding: 4 + topPadding: padding + leftPadding: padding + rightPadding: padding + bottomPadding: padding + + contentItem: Text { + id: menuBarItemText + text: menuBarItem.text + font { + family: appearance.fontName + pixelSize: menuBar.height / 2 + bold: menuBarItem.highlighted + } + opacity: 1 + color: menuBarItem.highlighted ? "#5a50da" : "white" + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + renderType: Text.NativeRendering + } + + background: Rectangle { + implicitWidth: 10 + implicitHeight: 10 + opacity: 1 + color: menuBarItem.highlighted ? "#c0c0f0" : "transparent" + } + } + + background: Rectangle { + width: parent.width + implicitHeight: 10 + color: getAppearanceValueForTheme(appearance.themeName, "mainBackground") + } + + CustomMenu { + id: fileMenuBarItem + title: translate.getTranslation("FILE_MENU", i18n.language) + font.family: appearance.fontName + + Action { + text: translate.getTranslation("OPEN_FILE", i18n.language) + onTriggered: fileDialog.open() + shortcut: keybinds.openFile + } + Action { + text: translate.getTranslation("OPEN_URL", i18n.language) + onTriggered: loadDialog.open() + shortcut: keybinds.openURI + } + Action { + text: translate.getTranslation("UPDATE_APPIMAGE", i18n.language) + onTriggered: utils.updateAppImage() + } + Action { + text: translate.getTranslation("EXIT", i18n.language) + onTriggered: Qt.quit() + shortcut: keybinds.quit + } + } + + CustomMenu { + id: playbackMenuBarItem + title: translate.getTranslation("PLAYBACK", i18n.language) + Action { + text: translate.getTranslation("PLAY_PAUSE", i18n.language) + onTriggered: { + player.playerCommand(Enums.Commands.TogglePlayPause) + } + shortcut: String(keybinds.playPause) + } + Action { + text: translate.getTranslation("REWIND_10S", i18n.language) + onTriggered: { + player.playerCommand(Enums.Commands.Seek, "-10") + } + shortcut: keybinds.rewind10 + } + Action { + text: translate.getTranslation("FORWARD_10S", i18n.language) + onTriggered: { + player.playerCommand(Enums.Commands.Seek, "10") + } + shortcut: keybinds.forward10 + } + Action { + text: translate.getTranslation("REWIND_5S", i18n.language) + onTriggered: { + player.playerCommand(Enums.Commands.Seek, "-5") + } + shortcut: keybinds.rewind5 + } + Action { + text: translate.getTranslation("FORWARD_5S", i18n.language) + onTriggered: { + player.playerCommand(Enums.Commands.Seek, "5") + } + shortcut: keybinds.forward5 + } + Action { + text: translate.getTranslation("SPEED_DECREASE_POINT_ONE", i18n.language) + onTriggered: { + player.playerCommand(Enums.Commands.SubtractSpeed, 0.1) + } + shortcut: keybinds.decreaseSpeedByPointOne + } + Action { + text: translate.getTranslation("SPEED_INCREASE_POINT_ONE", i18n.language) + onTriggered: { + player.playerCommand(Enums.Commands.AddSpeed, 0.1) + } + shortcut: keybinds.increaseSpeedByPointOne + } + Action { + text: translate.getTranslation("HALVE_SPEED", i18n.language) + onTriggered: { + player.playerCommand(Enums.Commands.ChangeSpeed, 0.5) + } + shortcut: keybinds.halveSpeed + } + Action { + text: translate.getTranslation("DOUBLE_SPEED", i18n.language) + onTriggered: { + player.playerCommand(Enums.Commands.ChangeSpeed, 2) + } + shortcut: keybinds.doubleSpeed + } + Action { + text: translate.getTranslation("FORWARD_FRAME", i18n.language) + onTriggered: { + player.playerCommand(Enums.Commands.ForwardFrame) + } + shortcut: keybinds.forwardFrame + } + Action { + text: translate.getTranslation("BACKWARD_FRAME", i18n.language) + onTriggered: { + player.playerCommand(Enums.Commands.BackwardFrame) + } + shortcut: keybinds.backwardFrame + } + Action { + text: translate.getTranslation("PREVIOUS_CHAPTER", i18n.language) + onTriggered: { + player.playerCommand(Enums.Commands.PreviousChapter) + } + shortcut: keybinds.previousChapter + } + Action { + text: translate.getTranslation("NEXT_CHAPTER", i18n.language) + onTriggered: { + player.playerCommand(Enums.Commands.NextChapter) + } + shortcut: keybinds.nextChapter + } + } + + CustomMenu { + id: audioMenuBarItem + title: translate.getTranslation("AUDIO", i18n.language) + Action { + text: translate.getTranslation("CYCLE_AUDIO_TRACK", i18n.language) + onTriggered: { + player.playerCommand(Enums.Commands.NextAudioTrack) + } + shortcut: keybinds.cycleAudio + } + Action { + text: translate.getTranslation("INCREASE_VOLUME", i18n.language) + onTriggered: { + player.playerCommand(Enums.Commands.AddVolume, "2") + } + shortcut: keybinds.increaseVolume + } + Action { + text: translate.getTranslation("DECREASE_VOLUME", i18n.language) + onTriggered: { + player.playerCommand(Enums.Commands.AddVolume, "-2") + } + shortcut: keybinds.decreaseVolume + } + Action { + text: translate.getTranslation("MUTE_VOLUME", i18n.language) + onTriggered: { + player.playerCommand(Enums.Commands.ToggleMute) + } + shortcut: keybinds.mute + } + + MenuSeparator {} + + CustomMenu { + title: translate.getTranslation("AUDIO_DEVICES", i18n.language) + id: audioDeviceMenu + objectName: "audioDeviceMenu" + + Connections { + target: player + onAudioDevicesChanged: function (ad) { + audioDeviceMenu.updateAudioDevices(ad) + } + } + function updateAudioDevices(audioDevices) { + for (var i = 0, len = audioDeviceMenu.count; i < len; i++) { + audioDeviceMenu.takeAction(0) + } + for (var thing in audioDevices) { + var audioDevice = audioDevices[thing] + var component = Qt.createComponent("AudioDeviceItem.qml") + var action = component.createObject(audioDeviceMenu, { + "text": audioDevices[thing]["description"], + "deviceID": String( + audioDevices[thing]["name"]) + }) + action.ActionGroup.group = audioDeviceMenuGroup + audioDeviceMenu.addAction(action) + } + } + ScrollView { + clip: true + ActionGroup { + id: audioDeviceMenuGroup + } + } + } + + MenuSeparator {} + + CustomMenu { + title: translate.getTranslation("AUDIO", i18n.language) + id: audioMenu + ActionGroup { + id: audioMenuGroup + } + TrackItem { + text: translate.getTranslation("DISABLE_TRACK", i18n.language) + trackType: "aid" + trackID: "no" + ActionGroup.group: audioMenuGroup + } + } + } + + CustomMenu { + id: videoMenuBarItem + title: translate.getTranslation("VIDEO", i18n.language) + Action { + text: translate.getTranslation("CYCLE_VIDEO", i18n.language) + onTriggered: { + player.playerCommand(Enums.Commands.NextVideoTrack) + } + shortcut: keybinds.cycleVideo + } + MenuSeparator {} + + CustomMenu { + title: translate.getTranslation("VIDEO", i18n.language) + id: videoMenu + ActionGroup { + id: videoMenuGroup + } + TrackItem { + text: translate.getTranslation("DISABLE_TRACK", i18n.language) + trackType: "vid" + trackID: "no" + ActionGroup.group: videoMenuGroup + } + } + } + CustomMenu { + id: subsMenuBarItem + title: translate.getTranslation("SUBTITLES", i18n.language) + Action { + text: translate.getTranslation("CYCLE_SUB_TRACK", i18n.language) + onTriggered: { + player.playerCommand(Enums.Commands.NextSubtitleTrack) + } + shortcut: keybinds.cycleSub + } + MenuSeparator {} + + CustomMenu { + title: translate.getTranslation("SUBTITLES", i18n.language) + id: subMenu + ActionGroup { + id: subMenuGroup + } + TrackItem { + text: translate.getTranslation("DISABLE_TRACK", i18n.language) + trackType: "sid" + trackID: "no" + ActionGroup.group: subMenuGroup + } + } + } + + CustomMenu { + id: viewMenuBarItem + title: translate.getTranslation("VIEW", i18n.language) + + CustomMenu { + title: translate.getTranslation("THEME", i18n.language) + id: themeMenu + Action { + text: "YouTube" + onTriggered: appearance.themeName = text + checkable: true + checked: appearance.themeName == text + } + Action { + text: "Niconico" + onTriggered: appearance.themeName = text + checkable: true + checked: appearance.themeName == text + } + Action { + text: "RoosterTeeth" + onTriggered: appearance.themeName = text + checkable: true + checked: appearance.themeName == text + } + } + + Action { + text: translate.getTranslation("FULLSCREEN", i18n.language) + onTriggered: { + toggleFullscreen() + } + shortcut: keybinds.fullscreen + } + Action { + text: translate.getTranslation("STATS", i18n.language) + onTriggered: { + statsForNerdsText.visible = !statsForNerdsText.visible + } + shortcut: keybinds.statsForNerds + } + + Action { + text: translate.getTranslation("TOGGLE_NYAN_CAT", i18n.language) + onTriggered: { + fun.nyanCat = !fun.nyanCat + } + shortcut: keybinds.nyanCat + } + Action { + text: translate.getTranslation("TOGGLE_ALWAYS_ON_TOP", i18n.language) + onTriggered: { + player.toggleOnTop() + } + } + Action { + text: translate.getTranslation("PLAYLIST_MENU", i18n.language) + onTriggered: { + playlistDialogLoader.active = true + } + } + Action { + text: translate.getTranslation("SETTINGS", i18n.language) + onTriggered: { + settingsDialogLoader.active = true + } + } + Action { + // Pretty sure for legal reasons this is needed unless I buy a Qt License + text: translate.getTranslation("ABOUT_QT", i18n.language) + onTriggered: { + utils.launchAboutQt() + } + } + } + + Item { + id: skipToNinthDuration + property var duration: 0 + Connections { + target: player + onDurationChanged: function (duration) { + skipToNinthDuration.duration = duration + } + } + } + + function skipToNinth(val) { + var skipto = 0 + if (val != 0) { + skipto = Math.floor(skipToNinthDuration.duration / 9 * val) + } + player.playerCommand(Enums.Commands.SeekAbsolute, skipto) + } + + Action { + onTriggered: skipToNinth(parseInt(shortcut)) + shortcut: "1" + } + Action { + onTriggered: skipToNinth(parseInt(shortcut)) + shortcut: "2" + } + Action { + onTriggered: skipToNinth(parseInt(shortcut)) + shortcut: "3" + } + Action { + onTriggered: skipToNinth(parseInt(shortcut)) + shortcut: "4" + } + Action { + onTriggered: skipToNinth(parseInt(shortcut)) + shortcut: "5" + } + Action { + onTriggered: skipToNinth(parseInt(shortcut)) + shortcut: "6" + } + Action { + onTriggered: skipToNinth(parseInt(shortcut)) + shortcut: "7" + } + Action { + onTriggered: skipToNinth(parseInt(shortcut)) + shortcut: "8" + } + Action { + onTriggered: skipToNinth(parseInt(shortcut)) + shortcut: "9" + } + Action { + onTriggered: skipToNinth(parseInt(shortcut)) + shortcut: "0" + } + + Action { + onTriggered: player.command(keybinds.customKeybind0Command) + shortcut: keybinds.customKeybind0 + } + Action { + onTriggered: player.command(keybinds.customKeybind1Command) + shortcut: keybinds.customKeybind1 + } + Action { + onTriggered: player.command(keybinds.customKeybind2Command) + shortcut: keybinds.customKeybind2 + } + Action { + onTriggered: player.command(keybinds.customKeybind3Command) + shortcut: keybinds.customKeybind3 + } + Action { + onTriggered: player.command(keybinds.customKeybind4Command) + shortcut: keybinds.customKeybind4 + } + Action { + onTriggered: player.command(keybinds.customKeybind5Command) + shortcut: keybinds.customKeybind5 + } + Action { + onTriggered: player.command(keybinds.customKeybind6Command) + shortcut: keybinds.customKeybind6 + } + Action { + onTriggered: player.command(keybinds.customKeybind7Command) + shortcut: keybinds.customKeybind7 + } + Action { + onTriggered: player.command(keybinds.customKeybind8Command) + shortcut: keybinds.customKeybind8 + } + Action { + onTriggered: player.command(keybinds.customKeybind9Command) + shortcut: keybinds.customKeybind9 + } +} diff --git a/src/qml/UIComponents/MenuBar/MenuTitleBar.qml b/src/qml/UIComponents/MenuBar/MenuTitleBar.qml new file mode 100644 index 0000000..1b3ac44 --- /dev/null +++ b/src/qml/UIComponents/MenuBar/MenuTitleBar.qml @@ -0,0 +1,68 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.3 +import QtQuick.Window 2.2 + +Item { + id: menuTitleBar + height: menuBar.height + visible: mainWindow.controlsShowing + + function anythingOpen() { + return menuBar.anythingOpen() + } + + anchors { + left: parent.left + right: parent.right + top: parent.top + } + + MainMenu { + id: menuBar + } + + Rectangle { + height: menuBar.height + color: getAppearanceValueForTheme(appearance.themeName, "mainBackground") + anchors { + right: parent.right + left: menuBar.right + top: parent.top + } + + Text { + id: titleLabel + objectName: "titleLabel" + text: translate.getTranslation("TITLE", i18n.language) + color: "white" + width: parent.width + height: parent.height + fontSizeMode: Text.VerticalFit + opacity: 1 + visible: menuTitleBar.visible + && ((!appearance.titleOnlyOnFullscreen) + || (mainWindow.visibility == Window.FullScreen + || mainWindow.visibility == Window.Maximized)) + font { + family: appearance.fontName + bold: true + pixelSize: appearance.scaleFactor * (height - anchors.topMargin - anchors.bottomMargin - 2) + } + anchors { + left: parent.left + leftMargin: 4 + bottom: parent.bottom + bottomMargin: 4 + top: parent.top + } + + Connections { + target: player + onTitleChanged: function (title) { + titleLabel.text = title + mainWindow.title = "VideoPlayer - " + title + } + } + } + } +} diff --git a/src/qml/Utils/Translator.qml b/src/qml/Utils/Translator.qml new file mode 100644 index 0000000..c9682c7 --- /dev/null +++ b/src/qml/Utils/Translator.qml @@ -0,0 +1,20 @@ +import QtQuick 2.0 +import "translations.js" as Translations + +Item { + function getTranslation(code, language) { + var lang = Translations.translations[language] + if (lang == undefined || lang == "undefined") { + return "TranslationNotFound" + } + var text = String(Translations.translations[i18n.language][code]) + if (text == "undefined") { + console.warn(code, "missing for language", language) + } + var args = Array.prototype.slice.call(arguments, 1) + var i = 0 + return text.replace(/%s/g, function () { + return args[i++] + }) + } +} diff --git a/src/qml/Utils/codes.js b/src/qml/Utils/codes.js new file mode 100644 index 0000000..7af518d --- /dev/null +++ b/src/qml/Utils/codes.js @@ -0,0 +1,954 @@ +function localeCodeToEnglish(loc) { + if (typeof loc !== 'string') + throw new TypeError('Input must be string') + var parts = loc.split('-'), ISO639_1 = { + ab: "Abkhazian", + aa: "Afar", + af: "Afrikaans", + ak: "Akan", + sq: "Albanian", + am: "Amharic", + ar: "Arabic", + an: "Aragonese", + hy: "Armenian", + as: "Assamese", + av: "Avaric", + ae: "Avestan", + ay: "Aymara", + az: "Azerbaijani", + bm: "Bambara", + ba: "Bashkir", + eu: "Basque", + be: "Belarusian", + bn: "Bengali", + bh: "Bihari languages", + bi: "Bislama", + nb: "Norwegian Bokmål", + bs: "Bosnian", + br: "Breton", + bg: "Bulgarian", + my: "Burmese", + es: "Spanish", + ca: "Valencian", + km: "Central Khmer", + ch: "Chamorro", + ce: "Chechen", + ny: "Nyanja", + zh: "Chinese", + za: "Zhuang", + cu: "Old Slavonic", + cv: "Chuvash", + kw: "Cornish", + co: "Corsican", + cr: "Cree", + hr: "Croatian", + cs: "Czech", + da: "Danish", + dv: "Maldivian", + nl: "Flemish", + dz: "Dzongkha", + en: "English", + eo: "Esperanto", + et: "Estonian", + ee: "Ewe", + fo: "Faroese", + fj: "Fijian", + fi: "Finnish", + fr: "French", + ff: "Fulah", + gd: "Scottish Gaelic", + gl: "Galician", + lg: "Ganda", + ka: "Georgian", + de: "German", + ki: "Kikuyu", + el: "Greek, Modern (1453-)", + kl: "Kalaallisut", + gn: "Guarani", + gu: "Gujarati", + ht: "Haitian Creole", + ha: "Hausa", + he: "Hebrew", + hz: "Herero", + hi: "Hindi", + ho: "Hiri Motu", + hu: "Hungarian", + is: "Icelandic", + io: "Ido", + ig: "Igbo", + id: "Indonesian", + ia: "Interlingua (International Auxiliary Language Association)", + ie: "Occidental", + iu: "Inuktitut", + ik: "Inupiaq", + ga: "Irish", + it: "Italian", + ja: "Japanese", + jv: "Javanese", + kn: "Kannada", + kr: "Kanuri", + ks: "Kashmiri", + kk: "Kazakh", + rw: "Kinyarwanda", + ky: "Kyrgyz", + kv: "Komi", + kg: "Kongo", + ko: "Korean", + kj: "Kwanyama", + ku: "Kurdish", + lo: "Lao", + la: "Latin", + lv: "Latvian", + lb: "Luxembourgish", + li: "Limburgish", + ln: "Lingala", + lt: "Lithuanian", + lu: "Luba-Katanga", + mk: "Macedonian", + mg: "Malagasy", + ms: "Malay", + ml: "Malayalam", + mt: "Maltese", + gv: "Manx", + mi: "Maori", + mr: "Marathi", + mh: "Marshallese", + ro: "Romanian", + mn: "Mongolian", + na: "Nauru", + nv: "Navajo", + nd: "North Ndebele", + nr: "South Ndebele", + ng: "Ndonga", + ne: "Nepali", + se: "Northern Sami", + no: "Norwegian", + nn: "Nynorsk, Norwegian", + ii: "Sichuan Yi", + oc: "Occitan (post 1500)", + oj: "Ojibwa", + or: "Oriya", + om: "Oromo", + os: "Ossetic", + pi: "Pali", + pa: "Punjabi", + ps: "Pushto", + fa: "Persian", + pl: "Polish", + pt: "Portuguese", + qu: "Quechua", + rm: "Romansh", + rn: "Rundi", + ru: "Russian", + sm: "Samoan", + sg: "Sango", + sa: "Sanskrit", + sc: "Sardinian", + sr: "Serbian", + sn: "Shona", + sd: "Sindhi", + si: "Sinhalese", + sk: "Slovak", + sl: "Slovenian", + so: "Somali", + st: "Sotho, Southern", + su: "Sundanese", + sw: "Swahili", + ss: "Swati", + sv: "Swedish", + tl: "Tagalog", + ty: "Tahitian", + tg: "Tajik", + ta: "Tamil", + tt: "Tatar", + te: "Telugu", + th: "Thai", + bo: "Tibetan", + ti: "Tigrinya", + to: "Tonga (Tonga Islands)", + ts: "Tsonga", + tn: "Tswana", + tr: "Turkish", + tk: "Turkmen", + tw: "Twi", + ug: "Uyghur", + uk: "Ukrainian", + ur: "Urdu", + uz: "Uzbek", + ve: "Venda", + vi: "Vietnamese", + vo: "Volapük", + wa: "Walloon", + cy: "Welsh", + fy: "Western Frisian", + wo: "Wolof", + xh: "Xhosa", + yi: "Yiddish", + yo: "Yoruba", + zu: "Zulu" + }, ISO639_2 = { + abk: "Abkhazian", + ace: "Achinese", + ach: "Acoli", + ada: "Adangme", + ady: "Adyghe", + aar: "Afar", + afh: "Afrihili", + afr: "Afrikaans", + afa: "Afro-Asiatic languages", + ain: "Ainu", + aka: "Akan", + akk: "Akkadian", + alb: "Albanian", + sqi: "Albanian", + gsw: "Swiss German", + ale: "Aleut", + alg: "Algonquian languages", + tut: "Altaic languages", + amh: "Amharic", + anp: "Angika", + apa: "Apache languages", + ara: "Arabic", + arg: "Aragonese", + arp: "Arapaho", + arw: "Arawak", + arm: "Armenian", + hye: "Armenian", + rup: "Macedo-Romanian", + art: "Artificial languages", + asm: "Assamese", + ast: "Leonese", + ath: "Athapascan languages", + aus: "Australian languages", + map: "Austronesian languages", + ava: "Avaric", + ave: "Avestan", + awa: "Awadhi", + aym: "Aymara", + aze: "Azerbaijani", + ban: "Balinese", + bat: "Baltic languages", + bal: "Baluchi", + bam: "Bambara", + bai: "Bamileke languages", + bad: "Banda languages", + bnt: "Bantu languages", + bas: "Basa", + bak: "Bashkir", + baq: "Basque", + eus: "Basque", + btk: "Batak languages", + bej: "Beja", + bel: "Belarusian", + bem: "Bemba", + ben: "Bengali", + ber: "Berber languages", + bho: "Bhojpuri", + bih: "Bihari languages", + bik: "Bikol", + byn: "Blin", + bin: "Edo", + bis: "Bislama", + zbl: "Blissymbols", + nob: "Norwegian Bokmål", + bos: "Bosnian", + bra: "Braj", + bre: "Breton", + bug: "Buginese", + bul: "Bulgarian", + bua: "Buriat", + bur: "Burmese", + mya: "Burmese", + cad: "Caddo", + spa: "Spanish", + cat: "Valencian", + cau: "Caucasian languages", + ceb: "Cebuano", + cel: "Celtic languages", + cai: "Central American Indian languages", + khm: "Central Khmer", + chg: "Chagatai", + cmc: "Chamic languages", + cha: "Chamorro", + che: "Chechen", + chr: "Cherokee", + nya: "Nyanja", + chy: "Cheyenne", + chb: "Chibcha", + chi: "Chinese", + zho: "Chinese", + chn: "Chinook jargon", + chp: "Dene Suline", + cho: "Choctaw", + zha: "Zhuang", + chu: "Old Slavonic", + chk: "Chuukese", + chv: "Chuvash", + nwc: "Old Newari", + syc: "Classical Syriac", + rar: "Rarotongan", + cop: "Coptic", + cor: "Cornish", + cos: "Corsican", + cre: "Cree", + mus: "Creek", + crp: "Creoles and pidgins", + cpe: "Creoles and pidgins, English based", + cpf: "Creoles and pidgins, French-based", + cpp: "Creoles and pidgins, Portuguese-based", + crh: "Crimean Turkish", + hrv: "Croatian", + cus: "Cushitic languages", + cze: "Czech", + ces: "Czech", + dak: "Dakota", + dan: "Danish", + dar: "Dargwa", + del: "Delaware", + div: "Maldivian", + zza: "Zazaki", + din: "Dinka", + doi: "Dogri", + dgr: "Dogrib", + dra: "Dravidian languages", + dua: "Duala", + dut: "Flemish", + nld: "Flemish", + dum: "Dutch, Middle (ca.1050-1350)", + dyu: "Dyula", + dzo: "Dzongkha", + frs: "Eastern Frisian", + efi: "Efik", + egy: "Egyptian (Ancient)", + eka: "Ekajuk", + elx: "Elamite", + eng: "English", + enm: "English, Middle (1100-1500)", + ang: "English, Old (ca.450-1100)", + myv: "Erzya", + epo: "Esperanto", + est: "Estonian", + ewe: "Ewe", + ewo: "Ewondo", + fan: "Fang", + fat: "Fanti", + fao: "Faroese", + fij: "Fijian", + fil: "Pilipino", + fin: "Finnish", + fiu: "Finno-Ugrian languages", + fon: "Fon", + fre: "French", + fra: "French", + frm: "French, Middle (ca.1400-1600)", + fro: "French, Old (842-ca.1400)", + fur: "Friulian", + ful: "Fulah", + gaa: "Ga", + gla: "Scottish Gaelic", + car: "Galibi Carib", + glg: "Galician", + lug: "Ganda", + gay: "Gayo", + gba: "Gbaya", + gez: "Geez", + geo: "Georgian", + kat: "Georgian", + ger: "German", + deu: "German", + nds: "Saxon, Low", + gmh: "German, Middle High (ca.1050-1500)", + goh: "German, Old High (ca.750-1050)", + gem: "Germanic languages", + kik: "Kikuyu", + gil: "Gilbertese", + gon: "Gondi", + gor: "Gorontalo", + got: "Gothic", + grb: "Grebo", + grc: "Greek, Ancient (to 1453)", + gre: "Greek, Modern (1453-)", + ell: "Greek, Modern (1453-)", + kal: "Kalaallisut", + grn: "Guarani", + guj: "Gujarati", + gwi: "Gwich'in", + hai: "Haida", + hat: "Haitian Creole", + hau: "Hausa", + haw: "Hawaiian", + heb: "Hebrew", + her: "Herero", + hil: "Hiligaynon", + him: "Western Pahari languages", + hin: "Hindi", + hmo: "Hiri Motu", + hit: "Hittite", + hmn: "Mong", + hun: "Hungarian", + hup: "Hupa", + iba: "Iban", + ice: "Icelandic", + isl: "Icelandic", + ido: "Ido", + ibo: "Igbo", + ijo: "Ijo languages", + ilo: "Iloko", + arc: "Official Aramaic (700-300 BCE)", + smn: "Inari Sami", + inc: "Indic languages", + ine: "Indo-European languages", + ind: "Indonesian", + inh: "Ingush", + ina: "Interlingua (International Auxiliary Language Association)", + ile: "Occidental", + iku: "Inuktitut", + ipk: "Inupiaq", + ira: "Iranian languages", + gle: "Irish", + mga: "Irish, Middle (900-1200)", + sga: "Irish, Old (to 900)", + iro: "Iroquoian languages", + ita: "Italian", + jpn: "Japanese", + jav: "Javanese", + kac: "Kachin", + jrb: "Judeo-Arabic", + jpr: "Judeo-Persian", + kbd: "Kabardian", + kab: "Kabyle", + xal: "Oirat", + kam: "Kamba", + kan: "Kannada", + kau: "Kanuri", + pam: "Pampanga", + kaa: "Kara-Kalpak", + krc: "Karachay-Balkar", + krl: "Karelian", + kar: "Karen languages", + kas: "Kashmiri", + csb: "Kashubian", + kaw: "Kawi", + kaz: "Kazakh", + kha: "Khasi", + khi: "Khoisan languages", + kho: "Sakan", + kmb: "Kimbundu", + kin: "Kinyarwanda", + kir: "Kyrgyz", + tlh: "tlhIngan-Hol", + kom: "Komi", + kon: "Kongo", + kok: "Konkani", + kor: "Korean", + kos: "Kosraean", + kpe: "Kpelle", + kro: "Kru languages", + kua: "Kwanyama", + kum: "Kumyk", + kur: "Kurdish", + kru: "Kurukh", + kut: "Kutenai", + lad: "Ladino", + lah: "Lahnda", + lam: "Lamba", + day: "Land Dayak languages", + lao: "Lao", + lat: "Latin", + lav: "Latvian", + ltz: "Luxembourgish", + lez: "Lezghian", + lim: "Limburgish", + lin: "Lingala", + lit: "Lithuanian", + jbo: "Lojban", + dsb: "Lower Sorbian", + loz: "Lozi", + lub: "Luba-Katanga", + lua: "Luba-Lulua", + lui: "Luiseno", + smj: "Lule Sami", + lun: "Lunda", + luo: "Luo (Kenya and Tanzania)", + lus: "Lushai", + mac: "Macedonian", + mkd: "Macedonian", + mad: "Madurese", + mag: "Magahi", + mai: "Maithili", + mak: "Makasar", + mlg: "Malagasy", + may: "Malay", + msa: "Malay", + mal: "Malayalam", + mlt: "Maltese", + mnc: "Manchu", + mdr: "Mandar", + man: "Mandingo", + mni: "Manipuri", + mno: "Manobo languages", + glv: "Manx", + mao: "Maori", + mri: "Maori", + arn: "Mapudungun", + mar: "Marathi", + chm: "Mari", + mah: "Marshallese", + mwr: "Marwari", + mas: "Masai", + myn: "Mayan languages", + men: "Mende", + mic: "Micmac", + min: "Minangkabau", + mwl: "Mirandese", + moh: "Mohawk", + mdf: "Moksha", + rum: "Romanian", + ron: "Romanian", + mkh: "Mon-Khmer languages", + lol: "Mongo", + mon: "Mongolian", + mos: "Mossi", + mul: "Multiple languages", + mun: "Munda languages", + nqo: "N'Ko", + nah: "Nahuatl languages", + nau: "Nauru", + nav: "Navajo", + nde: "North Ndebele", + nbl: "South Ndebele", + ndo: "Ndonga", + nap: "Neapolitan", + new: "Newari", + nep: "Nepali", + nia: "Nias", + nic: "Niger-Kordofanian languages", + ssa: "Nilo-Saharan languages", + niu: "Niuean", + zxx: "Not applicable", + nog: "Nogai", + non: "Norse, Old", + nai: "North American Indian languages", + frr: "Northern Frisian", + sme: "Northern Sami", + nso: "Sotho, Northern", + nor: "Norwegian", + nno: "Nynorsk, Norwegian", + nub: "Nubian languages", + iii: "Sichuan Yi", + nym: "Nyamwezi", + nyn: "Nyankole", + nyo: "Nyoro", + nzi: "Nzima", + oci: "Occitan (post 1500)", + pro: "Provençal, Old (to 1500)", + oji: "Ojibwa", + ori: "Oriya", + orm: "Oromo", + osa: "Osage", + oss: "Ossetic", + oto: "Otomian languages", + pal: "Pahlavi", + pau: "Palauan", + pli: "Pali", + pag: "Pangasinan", + pan: "Punjabi", + pap: "Papiamento", + paa: "Papuan languages", + pus: "Pushto", + per: "Persian", + fas: "Persian", + peo: "Persian, Old (ca.600-400 B.C.)", + phi: "Philippine languages", + phn: "Phoenician", + pon: "Pohnpeian", + pol: "Polish", + por: "Portuguese", + pra: "Prakrit languages", + que: "Quechua", + raj: "Rajasthani", + rap: "Rapanui", + roa: "Romance languages", + roh: "Romansh", + rom: "Romany", + run: "Rundi", + rus: "Russian", + sal: "Salishan languages", + sam: "Samaritan Aramaic", + smi: "Sami languages", + smo: "Samoan", + sad: "Sandawe", + sag: "Sango", + san: "Sanskrit", + sat: "Santali", + srd: "Sardinian", + sas: "Sasak", + sco: "Scots", + sel: "Selkup", + sem: "Semitic languages", + srp: "Serbian", + srr: "Serer", + shn: "Shan", + sna: "Shona", + scn: "Sicilian", + sid: "Sidamo", + sgn: "Sign Languages", + bla: "Siksika", + snd: "Sindhi", + sin: "Sinhalese", + sit: "Sino-Tibetan languages", + sio: "Siouan languages", + sms: "Skolt Sami", + den: "Slave (Athapascan)", + sla: "Slavic languages", + slo: "Slovak", + slk: "Slovak", + slv: "Slovenian", + sog: "Sogdian", + som: "Somali", + son: "Songhai languages", + snk: "Soninke", + wen: "Sorbian languages", + sot: "Sotho, Southern", + sai: "South American Indian languages", + alt: "Southern Altai", + sma: "Southern Sami", + srn: "Sranan Tongo", + suk: "Sukuma", + sux: "Sumerian", + sun: "Sundanese", + sus: "Susu", + swa: "Swahili", + ssw: "Swati", + swe: "Swedish", + syr: "Syriac", + tgl: "Tagalog", + tah: "Tahitian", + tai: "Tai languages", + tgk: "Tajik", + tmh: "Tamashek", + tam: "Tamil", + tat: "Tatar", + tel: "Telugu", + ter: "Tereno", + tet: "Tetum", + tha: "Thai", + tib: "Tibetan", + bod: "Tibetan", + tig: "Tigre", + tir: "Tigrinya", + tem: "Timne", + tiv: "Tiv", + tli: "Tlingit", + tpi: "Tok Pisin", + tkl: "Tokelau", + tog: "Tonga (Nyasa)", + ton: "Tonga (Tonga Islands)", + tsi: "Tsimshian", + tso: "Tsonga", + tsn: "Tswana", + tum: "Tumbuka", + tup: "Tupi languages", + tur: "Turkish", + ota: "Turkish, Ottoman (1500-1928)", + tuk: "Turkmen", + tvl: "Tuvalu", + tyv: "Tuvinian", + twi: "Twi", + udm: "Udmurt", + uga: "Ugaritic", + uig: "Uyghur", + ukr: "Ukrainian", + umb: "Umbundu", + mis: "Uncoded languages", + und: "Undetermined", + hsb: "Upper Sorbian", + urd: "Urdu", + uzb: "Uzbek", + vai: "Vai", + ven: "Venda", + vie: "Vietnamese", + vol: "Volapük", + vot: "Votic", + wak: "Wakashan languages", + wln: "Walloon", + war: "Waray", + was: "Washo", + wel: "Welsh", + cym: "Welsh", + fry: "Western Frisian", + wal: "Wolaytta", + wol: "Wolof", + xho: "Xhosa", + sah: "Yakut", + yao: "Yao", + yap: "Yapese", + yid: "Yiddish", + yor: "Yoruba", + ypk: "Yupik languages", + znd: "Zande languages", + zap: "Zapotec", + zen: "Zenaga", + zul: "Zulu", + zun: "Zuni" + }, ISO3166_1 = { + AF: "AFGHANISTAN", + AX: "ÅLAND ISLANDS", + AL: "ALBANIA", + DZ: "ALGERIA", + AS: "AMERICAN SAMOA", + AD: "ANDORRA", + AO: "ANGOLA", + AI: "ANGUILLA", + AQ: "ANTARCTICA", + AG: "ANTIGUA AND BARBUDA", + AR: "ARGENTINA", + AM: "ARMENIA", + AW: "ARUBA", + AU: "AUSTRALIA", + AT: "AUSTRIA", + AZ: "AZERBAIJAN", + BS: "BAHAMAS", + BH: "BAHRAIN", + BD: "BANGLADESH", + BB: "BARBADOS", + BY: "BELARUS", + BE: "BELGIUM", + BZ: "BELIZE", + BJ: "BENIN", + BM: "BERMUDA", + BT: "BHUTAN", + BO: "BOLIVIA, PLURINATIONAL STATE OF", + BQ: "BONAIRE, SINT EUSTATIUS AND SABA", + BA: "BOSNIA AND HERZEGOVINA", + BW: "BOTSWANA", + BV: "BOUVET ISLAND", + BR: "BRAZIL", + IO: "BRITISH INDIAN OCEAN TERRITORY", + BN: "BRUNEI DARUSSALAM", + BG: "BULGARIA", + BF: "BURKINA FASO", + BI: "BURUNDI", + KH: "CAMBODIA", + CM: "CAMEROON", + CA: "CANADA", + CV: "CAPE VERDE", + KY: "CAYMAN ISLANDS", + CF: "CENTRAL AFRICAN REPUBLIC", + TD: "CHAD", + CL: "CHILE", + CN: "CHINA", + CX: "CHRISTMAS ISLAND", + CC: "COCOS (KEELING) ISLANDS", + CO: "COLOMBIA", + KM: "COMOROS", + CG: "CONGO", + CD: "CONGO, THE DEMOCRATIC REPUBLIC OF THE", + CK: "COOK ISLANDS", + CR: "COSTA RICA", + CI: "CÔTE D'IVOIRE", + HR: "CROATIA", + CU: "CUBA", + CW: "CURAÇAO", + CY: "CYPRUS", + CZ: "CZECH REPUBLIC", + DK: "DENMARK", + DJ: "DJIBOUTI", + DM: "DOMINICA", + DO: "DOMINICAN REPUBLIC", + EC: "ECUADOR", + EG: "EGYPT", + SV: "EL SALVADOR", + GQ: "EQUATORIAL GUINEA", + ER: "ERITREA", + EE: "ESTONIA", + ET: "ETHIOPIA", + FK: "FALKLAND ISLANDS (MALVINAS)", + FO: "FAROE ISLANDS", + FJ: "FIJI", + FI: "FINLAND", + FR: "FRANCE", + GF: "FRENCH GUIANA", + PF: "FRENCH POLYNESIA", + TF: "FRENCH SOUTHERN TERRITORIES", + GA: "GABON", + GM: "GAMBIA", + GE: "GEORGIA", + DE: "GERMANY", + GH: "GHANA", + GI: "GIBRALTAR", + GR: "GREECE", + GL: "GREENLAND", + GD: "GRENADA", + GP: "GUADELOUPE", + GU: "GUAM", + GT: "GUATEMALA", + GG: "GUERNSEY", + GN: "GUINEA", + GW: "GUINEA-BISSAU", + GY: "GUYANA", + HT: "HAITI", + HM: "HEARD ISLAND AND MCDONALD ISLANDS", + VA: "HOLY SEE (VATICAN CITY STATE)", + HN: "HONDURAS", + HK: "HONG KONG", + HU: "HUNGARY", + IS: "ICELAND", + IN: "INDIA", + ID: "INDONESIA", + IR: "IRAN, ISLAMIC REPUBLIC OF", + IQ: "IRAQ", + IE: "IRELAND", + IM: "ISLE OF MAN", + IL: "ISRAEL", + IT: "ITALY", + JM: "JAMAICA", + JP: "JAPAN", + JE: "JERSEY", + JO: "JORDAN", + KZ: "KAZAKHSTAN", + KE: "KENYA", + KI: "KIRIBATI", + KP: "KOREA, DEMOCRATIC PEOPLE'S REPUBLIC OF", + KR: "KOREA, REPUBLIC OF", + KW: "KUWAIT", + KG: "KYRGYZSTAN", + LA: "LAO PEOPLE'S DEMOCRATIC REPUBLIC", + LV: "LATVIA", + LB: "LEBANON", + LS: "LESOTHO", + LR: "LIBERIA", + LY: "LIBYA", + LI: "LIECHTENSTEIN", + LT: "LITHUANIA", + LU: "LUXEMBOURG", + MO: "MACAO", + MK: "MACEDONIA, THE FORMER YUGOSLAV REPUBLIC OF", + MG: "MADAGASCAR", + MW: "MALAWI", + MY: "MALAYSIA", + MV: "MALDIVES", + ML: "MALI", + MT: "MALTA", + MH: "MARSHALL ISLANDS", + MQ: "MARTINIQUE", + MR: "MAURITANIA", + MU: "MAURITIUS", + YT: "MAYOTTE", + MX: "MEXICO", + FM: "MICRONESIA, FEDERATED STATES OF", + MD: "MOLDOVA, REPUBLIC OF", + MC: "MONACO", + MN: "MONGOLIA", + ME: "MONTENEGRO", + MS: "MONTSERRAT", + MA: "MOROCCO", + MZ: "MOZAMBIQUE", + MM: "MYANMAR", + NA: "NAMIBIA", + NR: "NAURU", + NP: "NEPAL", + NL: "NETHERLANDS", + NC: "NEW CALEDONIA", + NZ: "NEW ZEALAND", + NI: "NICARAGUA", + NE: "NIGER", + NG: "NIGERIA", + NU: "NIUE", + NF: "NORFOLK ISLAND", + MP: "NORTHERN MARIANA ISLANDS", + NO: "NORWAY", + OM: "OMAN", + PK: "PAKISTAN", + PW: "PALAU", + PS: "PALESTINIAN TERRITORY, OCCUPIED", + PA: "PANAMA", + PG: "PAPUA NEW GUINEA", + PY: "PARAGUAY", + PE: "PERU", + PH: "PHILIPPINES", + PN: "PITCAIRN", + PL: "POLAND", + PT: "PORTUGAL", + PR: "PUERTO RICO", + QA: "QATAR", + RE: "RÉUNION", + RO: "ROMANIA", + RU: "RUSSIAN FEDERATION", + RW: "RWANDA", + BL: "SAINT BARTHÉLEMY", + SH: "SAINT HELENA, ASCENSION AND TRISTAN DA CUNHA", + KN: "SAINT KITTS AND NEVIS", + LC: "SAINT LUCIA", + MF: "SAINT MARTIN (FRENCH PART)", + PM: "SAINT PIERRE AND MIQUELON", + VC: "SAINT VINCENT AND THE GRENADINES", + WS: "SAMOA", + SM: "SAN MARINO", + ST: "SAO TOME AND PRINCIPE", + SA: "SAUDI ARABIA", + SN: "SENEGAL", + RS: "SERBIA", + SC: "SEYCHELLES", + SL: "SIERRA LEONE", + SG: "SINGAPORE", + SX: "SINT MAARTEN (DUTCH PART)", + SK: "SLOVAKIA", + SI: "SLOVENIA", + SB: "SOLOMON ISLANDS", + SO: "SOMALIA", + ZA: "SOUTH AFRICA", + GS: "SOUTH GEORGIA AND THE SOUTH SANDWICH ISLANDS", + SS: "SOUTH SUDAN", + ES: "SPAIN", + LK: "SRI LANKA", + SD: "SUDAN", + SR: "SURINAME", + SJ: "SVALBARD AND JAN MAYEN", + SZ: "SWAZILAND", + SE: "SWEDEN", + CH: "SWITZERLAND", + SY: "SYRIAN ARAB REPUBLIC", + TW: "TAIWAN, PROVINCE OF CHINA", + TJ: "TAJIKISTAN", + TZ: "TANZANIA, UNITED REPUBLIC OF", + TH: "THAILAND", + TL: "TIMOR-LESTE", + TG: "TOGO", + TK: "TOKELAU", + TO: "TONGA", + TT: "TRINIDAD AND TOBAGO", + TN: "TUNISIA", + TR: "TURKEY", + TM: "TURKMENISTAN", + TC: "TURKS AND CAICOS ISLANDS", + TV: "TUVALU", + UG: "UGANDA", + UA: "UKRAINE", + AE: "UNITED ARAB EMIRATES", + GB: "UNITED KINGDOM", + US: "UNITED STATES", + UM: "UNITED STATES MINOR OUTLYING ISLANDS", + UY: "URUGUAY", + UZ: "UZBEKISTAN", + VU: "VANUATU", + VE: "VENEZUELA, BOLIVARIAN REPUBLIC OF", + VN: "VIET NAM", + VG: "VIRGIN ISLANDS, BRITISH", + VI: "VIRGIN ISLANDS, U.S.", + WF: "WALLIS AND FUTUNA", + EH: "WESTERN SAHARA", + YE: "YEMEN", + ZM: "ZAMBIA", + ZW: "ZIMBABWE" + } + if (parts.length > 2) + throw new SyntaxError('Unexpected number of segments ' + parts.length) + if (parts.length > 1) + return (ISO639_1[parts[0]] || ISO639_2[parts[0]] + || parts[0]) + ', ' + (ISO3166_1[parts[1]] || parts[1]) + if (parts.length > 0) + return ISO639_1[parts[0]] || ISO639_2[parts[0]] || ISO3166_1[parts[0]] + || parts[0] + return '' +} diff --git a/src/qml/Utils/translations.js b/src/qml/Utils/translations.js new file mode 100644 index 0000000..d816e1a --- /dev/null +++ b/src/qml/Utils/translations.js @@ -0,0 +1,471 @@ +var languages = { + "english": "English", + "spanish": "Español", + "german": "Deutsch", + "french": "Française", + "italian": "Italiano", + "russian": "Русский", + "norwegian": "Norwegian", + "tokipona": "toki pona", + "telugu": "తెలుగు", + "vietnamese": "Tiếng Việt" +} + +var translations = { + english: { + OPEN_FILE: "Open file", + URL_FILE_PATH: "URL / File Path", + FILE_MENU: "File", + OPEN_URL: "Open URL", + EXIT: "Exit", + PLAYBACK: "Playback", + PLAY_PAUSE: "Play/Pause", + REWIND_10S: "Rewind 10s", + FORWARD_10S: "Forward 10s", + REWIND_5S: "Rewind 5s", + FORWARD_5S: "Forward 5s", + SPEED_DECREASE_POINT_ONE: "Speed -0.1", + SPEED_INCREASE_POINT_ONE: "Speed +0.1", + HALVE_SPEED: "Halve Speed", + DOUBLE_SPEED: "Double Speed", + BACKWARD_FRAME: "Back a frame", + FORWARD_FRAME: "Forward a frame", + AUDIO: "Audio", + INCREASE_VOLUME: "Increase Volume", + DECREASE_VOLUME: "Decrease Volume", + MUTE_VOLUME: "Mute", + VIDEO: "Video", + CYCLE_VIDEO: "Cycle video", + SUBTITLES: "Subtitles", + CYCLE_SUB_TRACK: "Cycle Subtitle Track", + CYCLE_AUDIO_TRACK: "Cycle Audio Track", + VIEW: "View", + FULLSCREEN: "Fullscreen", + STATS: "Statistics", + TOGGLE_NYAN_CAT: "Toggle Nyan Cat", + ABOUT: "About", + ABOUT_QT: "About Qt", + TITLE: "Title", + TOGGLE_ALWAYS_ON_TOP: "Toggle Always On Top", + DISABLE_TRACK: "Disable Track", + AUDIO_DEVICES: "Audio Devices", + PLAYLIST_MENU: "Playlist Menu", + THEME: "Theme", + SETTINGS: "Settings", + LANGUAGE: "Language", + APPEARANCE: "Appearance", + TITLE_ONLY_ON_FULLSCREEN: "Show title when only in fullscreen", + CLICK_TO_PAUSE: "Tap/Click to pause", + DOUBLE_TAP_TO_SEEK: "Double Tap/Click to seek", + DOUBLE_TAP_TO_SEEK_BY: "Seek by", + FONT: "Font", + SUBTITLES_FONT_SIZE: "Subtitles Font Size", + UI_FADE_TIME: "UI Fade Time (ms)", + UPDATE_APPIMAGE: "Update (AppImage Only)", + PREVIOUS_CHAPTER: "Previous Chapter", + NEXT_CHAPTER: "Next Chapter" + }, + spanish: { + OPEN_FILE: "Abrir archivo", + URL_FILE_PATH: "URL / Dirección de archivo", + FILE_MENU: "Archivo", + OPEN_URL: "Abrir URL", + EXIT: "Salir", + PLAYBACK: "Volver a reproducir", + PLAY_PAUSE: "Reproducir/Pausar", + REWIND_10S: "Retrasar 10s", + FORWARD_10S: "Adelantar 10s", + REWIND_5S: "Retrasar 5s", + FORWARD_5S: "Adelantar 5s", + SPEED_DECREASE_POINT_ONE: "Velocidad -0.1", + SPEED_INCREASE_POINT_ONE: "Velocidad +0.1", + HALVE_SPEED: "Dividir velocidad a la mitad", + DOUBLE_SPEED: "Duplicar velocidad", + BACKWARD_FRAME: "Retrasar un frame", + FORWARD_FRAME: "Adelantar un frame", + AUDIO: "Audio", + INCREASE_VOLUME: "Subir Volumen", + DECREASE_VOLUME: "Bajar Volumen", + MUTE_VOLUME: "Silenciar", + VIDEO: "Vídeo", + CYCLE_VIDEO: "Siguiente vídeo", + SUBTITLES: "Subtítulos", + CYCLE_SUB_TRACK: "Siguientes subtítulos", + CYCLE_AUDIO_TRACK: "Siguiente pista de audio", + VIEW: "Ver", + FULLSCREEN: "Pantalla completa", + STATS: "Estadísticas", + TOGGLE_NYAN_CAT: "Activar Nyan Cat", + ABOUT: "Ayuda", + ABOUT_QT: "Sobre Qt", + TITLE: "Título", + TOGGLE_ALWAYS_ON_TOP: "Fijar ventana", + DISABLE_TRACK: "Eliminar pista" + }, + german: { + OPEN_FILE: "Datei öffnen", + URL_FILE_PATH: "URL / Datei-Pfad", + FILE_MENU: "Datei", + OPEN_URL: "URL öffnen", + EXIT: "Beenden", + PLAYBACK: "Wiedergabe", + PLAY_PAUSE: "Wiedergabe/Pause", + REWIND_10S: "Rückspulen um 10s", + FORWARD_10S: "Vorspulen um 10s", + REWIND_5S: "Rückspulen um 5s", + FORWARD_5S: "Vorspulen um 5s", + SPEED_DECREASE_POINT_ONE: "Geschwindigkeit -0.1", + SPEED_INCREASE_POINT_ONE: "Geschwindigkeit +0.1", + HALVE_SPEED: "Halbe Geschwindigkeit", + DOUBLE_SPEED: "Doppelte Geschwindigkeit", + BACKWARD_FRAME: "Ein Bild zurück", + FORWARD_FRAME: "Ein Bild vor", + AUDIO: "Audio", + INCREASE_VOLUME: "Lautstärke erhöhen", + DECREASE_VOLUME: "Lautstärke verringern", + MUTE_VOLUME: "Stumm", + VIDEO: "Video", + CYCLE_VIDEO: "Wechsle Video", + SUBTITLES: "Untertitel", + CYCLE_SUB_TRACK: "Wechsle Untertitel-Spur", + CYCLE_AUDIO_TRACK: "Wechsle Audiospur", + VIEW: "Ansicht", + FULLSCREEN: "Vollbild", + STATS: "Statistiken", + TOGGLE_NYAN_CAT: "Schalte Nyan Cat An/Aus", + ABOUT: "Über", + ABOUT_QT: "Über QT", + TITLE: "Titel", + TOGGLE_ALWAYS_ON_TOP: "Immer im Vordergrund", + DISABLE_TRACK: "Spur deaktivieren", + AUDIO_DEVICES: "Audiogeräte", + PLAYLIST_MENU: "Wiedergabeliste", + THEME: "Darstellung", + SETTINGS: "Einstellungen", + LANGUAGE: "Sprache", + APPEARANCE: "Aussehen", + TITLE_ONLY_ON_FULLSCREEN: "Titel nur im Vollbildmodus anzeigen", + CLICK_TO_PAUSE: "Berühren/Klicken zum Pausieren", + DOUBLE_TAP_TO_SEEK: "Doppeltap/Klicken zum Spulen", + DOUBLE_TAP_TO_SEEK_BY: "Spulen um (s)", + FONT: "Schriftart", + SUBTITLES_FONT_SIZE: "Untertitel-Schriftgröße", + UI_FADE_TIME: "Ausblendzeit Benutzeroberfläche (ms)" + }, + french: { + OPEN_FILE: "Ouvrir le fichier", + URL_FILE_PATH: "URL / Chemin du fichier", + FILE_MENU: "Fichier", + OPEN_URL: "Ouvrir l'URL", + EXIT: "Fermer", + PLAYBACK: "Lecture", + PLAY_PAUSE: "Jouer/Mettre en pause", + REWIND_10S: "Reculer de 10s", + FORWARD_10S: "Avancer de 10s", + REWIND_5S: "Reculer de 5s", + FORWARD_5S: "Avancer de 5s", + SPEED_DECREASE_POINT_ONE: "Vitesse -0.1", + SPEED_INCREASE_POINT_ONE: "Vitesse +0.1", + HALVE_SPEED: "Réduire de moitié la vitesse", + DOUBLE_SPEED: "Doubler la vitesse", + BACKWARD_FRAME: "Reculer d'une image", + FORWARD_FRAME: "Avancer d'une image", + AUDIO: "Audio", + CYCLE_AUDIO_TRACK: "Chancer de piste audio", + INCREASE_VOLUME: "Augmenter le volume", + DECREASE_VOLUME: "Réduire le volume", + MUTE_VOLUME: "Muet", + VIDEO: "Vidéo", + CYCLE_VIDEO: "Changer de vidéo", + SUBTITLES: "Sous-titres", + CYCLE_SUB_TRACK: "Changer de piste de sous-titres", + VIEW: "Voir", + FULLSCREEN: "Plein écran", + STATS: "Statistiques", + TOGGLE_NYAN_CAT: "basculer Nyan Cat", + ABOUT: "A propos", + ABOUT_QT: "A propos de Qt", + TITLE: "Titre", + TOGGLE_ALWAYS_ON_TOP: "Toujours au-dessu", + DISABLE_TRACK: "Désactiver la piste", + AUDIO_DEVICES: "Périphériques audio", + PLAYLIST_MENU: "Menu de la liste de lecture", + THEME: "Thème" + }, + italian: { + OPEN_FILE: "Apri file", + URL_FILE_PATH: "URL / Percorso file", + FILE_MENU: "File", + OPEN_URL: "Apri URL", + EXIT: "Esci", + PLAYBACK: "Riproduzione", + PLAY_PAUSE: "Play/Pausa", + REWIND_10S: "Indietro 10s", + FORWARD_10S: "Avanti 10s", + REWIND_5S: "Indietro 5s", + FORWARD_5S: "Avanti 5s", + SPEED_DECREASE_POINT_ONE: "Velocità -0.1", + SPEED_INCREASE_POINT_ONE: "Velocità +0.1", + HALVE_SPEED: "Dimezza velocità", + DOUBLE_SPEED: "Raddoppia velocità", + BACKWARD_FRAME: "Indietro di un fotogramma", + FORWARD_FRAME: "Avanti di un fotogramma", + AUDIO: "Audio", + INCREASE_VOLUME: "Aumenta Volume", + DECREASE_VOLUME: "Diminuisci Volume", + MUTE_VOLUME: "Muto", + VIDEO: "Video", + CYCLE_VIDEO: "Ripeti video", + SUBTITLES: "Sottotitoli", + CYCLE_SUB_TRACK: "Ripeti traccia sottotitoli", + CYCLE_AUDIO_TRACK: "Ripeti traccia audio", + VIEW: "Vedi", + FULLSCREEN: "Schermo intero", + STATS: "Statistiche", + TOGGLE_NYAN_CAT: "Attiva Nyan Cat", + ABOUT: "Info", + ABOUT_QT: "Info su Qt", + TITLE: "Titolo", + TOGGLE_ALWAYS_ON_TOP: "Mantieni in primo piano", + DISABLE_TRACK: "Disabilita traccia", + AUDIO_DEVICES: "Dispositivi audio", + PLAYLIST_MENU: "Menu Playlist", + THEME: "Tema", + SETTINGS: "Impostazioni", + LANGUAGE: "Lingua", + APPEARANCE: "Appearance", //gonna need more context for this + TITLE_ONLY_ON_FULLSCREEN: "Mostra il titolo solo in modalità a schermo intero", //kinda long but can't think of a shorter way + CLICK_TO_PAUSE: "Tap/Click per mettere in pausa", + DOUBLE_TAP_TO_SEEK: "Doppio Tap/Click per andare avanti", + DOUBLE_TAP_TO_SEEK_BY: "vai avanti di (secondi)", + FONT: "Font", + SUBTITLES_FONT_SIZE: "Dimensione Sottotitoli", + UI_FADE_TIME: "Fade Interfaccia (millisecondi)" + }, + russian: { + OPEN_FILE: "Открыть...", + URL_FILE_PATH: "Ввести URL / путь к файлу...", + FILE_MENU: "Файл", + OPEN_URL: "Открыть URL...", + EXIT: "Выход", + PLAYBACK: "Воспроизведение", + PLAY_PAUSE: "Воспроизвести/Остановить", + REWIND_10S: "Перемотать назад на 10 секунд", + FORWARD_10S: "Перемотать вперед на 10 секунд", + REWIND_5S: "Перемотать назад на 5 секунд", + FORWARD_5S: "Перемотать вперед на 5 секунд", + SPEED_DECREASE_POINT_ONE: "Уменьшить скорость на 0.1", + SPEED_INCREASE_POINT_ONE: "Увеличить скорость на 0.1", + HALVE_SPEED: "Замедлить воспроизведение в 2 раза", + DOUBLE_SPEED: "Ускорить воспроизведение в 2 раза", + BACKWARD_FRAME: "Вернуться к предыдущему кадру", + FORWARD_FRAME: "Перейти к следующему кадру", + AUDIO: "Звук", + INCREASE_VOLUME: "Увеличить громкость", + DECREASE_VOLUME: "Уменьшить громкость", + MUTE_VOLUME: "Заглушить звук", + VIDEO: "Видео", + CYCLE_VIDEO: "Зациклить воспроизведение видео", + SUBTITLES: "Субтитры", + CYCLE_SUB_TRACK: "Зациклить дорожку субтитров", + CYCLE_AUDIO_TRACK: "Зациклить аудиодорожку", + VIEW: "Вид", + FULLSCREEN: "Во весь экран", + STATS: "Статистика", + TOGGLE_NYAN_CAT: "Нян-кот", + ABOUT: "О программе", + ABOUT_QT: "О используемой версии Qt...", + TITLE: "Заголовок", + SETTINGS: "Настройки" + }, + norwegian: { + OPEN_FILE: "Åpne Fil", + URL_FILE_PATH: "Lenke / Filbane", + FILE_MENU: "Fil", + OPEN_URL: "Åpne lenke", + EXIT: "Avslutt", + PLAYBACK: "Avspilling", + PLAY_PAUSE: "Spill/Pause", + REWIND_10S: "Spol Tilbake 10s", + FORWARD_10S: "Spol Frem 10s", + REWIND_5S: "Spol Tilbake 5s", + FORWARD_5S: "Spol Frem 5s", + SPEED_DECREASE_POINT_ONE: "Hastighet -0.1", + SPEED_INCREASE_POINT_ONE: "Hastighet +0.1", + HALVE_SPEED: "Halv Hastighet", + DOUBLE_SPEED: "Dobbel Hastighet", + BACKWARD_FRAME: "Gå Tilbake Én Ramme", + FORWARD_FRAME: "Gå Frem Én Ramme", + AUDIO: "Lydspor", + INCREASE_VOLUME: "Øk Lydstyrke", + DECREASE_VOLUME: "Senk Lydstyrke", + MUTE_VOLUME: "Demp", + VIDEO: "Videospor", + CYCLE_VIDEO: "Sirkuler Gjennom Videospor", + SUBTITLES: "Undertekster", + CYCLE_SUB_TRACK: "Sirkuler Gjennom Undertekster", + CYCLE_AUDIO_TRACK: "Sirkuler Gjennom Lydspor", + VIEW: "Vis", + FULLSCREEN: "Fullskjerm", + STATS: "Statistikk", + TOGGLE_NYAN_CAT: "Skru Av/På Nyan Cat", + ABOUT: "Om", + ABOUT_QT: "Om Qt", + TITLE: "Tittel", + TOGGLE_ALWAYS_ON_TOP: "Alltid Øverst", + DISABLE_TRACK: "Skru Av Spor" + }, + tokipona: { + OPEN_FILE: "open e lipu", + URL_FILE_PATH: "URL / nasin lipu", + FILE_MENU: "lipu", + OPEN_URL: "open e URL", + EXIT: "pini", + PLAYBACK: "tawa", + PLAY_PAUSE: "tawa/awen", + REWIND_10S: "tawa monsi mute", + FORWARD_10S: "tawa sinpin mute", + REWIND_5S: "tawa monsi lili", + FORWARD_5S: "tawa sinpin lili", + SPEED_DECREASE_POINT_ONE: "lili lili e tawa", + SPEED_INCREASE_POINT_ONE: "mute lili e tawa", + HALVE_SPEED: "lili mute e tawa", + DOUBLE_SPEED: "mute mute e tawa", + BACKWARD_FRAME: "monsi pi sitelen wan", + FORWARD_FRAME: "sinpin pi sitelen wan", + AUDIO: "kalama", + INCREASE_VOLUME: "mute e kalama", + DECREASE_VOLUME: "lili e kalama", + MUTE_VOLUME: "awen e kalama", + VIDEO: "sitelen tawa", + CYCLE_VIDEO: "ante e sitelen tawa", + SUBTITLES: "sitelen anpa", + CYCLE_SUB_TRACK: "ante e sitelen anpa", + CYCLE_AUDIO_TRACK: "ante e kalama", + VIEW: "lukin", + FULLSCREEN: "lupa lukin ale", + STATS: "nanpa", + TOGGLE_NYAN_CAT: "open anu awen e soweli Nian", + ABOUT: "ni li seme?", + ABOUT_QT: "QT li seme?", + TITLE: "nimi", + TOGGLE_ALWAYS_ON_TOP: "sewi ala sewi e lipu", + DISABLE_TRACK: "awen e linja", + AUDIO_DEVICES: "ila kalama", + PLAYLIST_MENU: "lipu musi", + THEME: "len ijo", + SETTINGS: "ken ijo", + LANGUAGE: "toki", + APPEARANCE: "lukin ijo", + TITLE_ONLY_ON_FULLSCREEN: "lupa lukin ale taso la, sitelen e nimi", + CLICK_TO_PAUSE: "luka la, awen", + DOUBLE_TAP_TO_SEEK: "luka tu la, lukin", + DOUBLE_TAP_TO_SEEK_BY: "luka tu la, lukin kepeken", + FONT: "sitelen", + SUBTITLES_FONT_SIZE: "suli pi sitelen anpa", + UI_FADE_TIME: "tenpo pi wawa UI", + UPDATE_APPIMAGE: "sin e ijo (AppImage taso)" + }, + telugu: { + OPEN_FILE: "ఫైల్ తెరువు", + URL_FILE_PATH: "లింకు / ఫైల్ ఉన్న ప్రదేశం", + FILE_MENU: "ఫైల్ మెనూ", + OPEN_URL: "URL తెరువు", + EXIT: "నిష్క్రమణ", + PLAYBACK: "ప్లేబ్యాక్", + PLAY_PAUSE: "ప్లే/పౌస్", + REWIND_10S: "10సె వెనక్కి", + FORWARD_10S: "10సె ముందుకి", + REWIND_5S: "5సె వెనక్కి", + FORWARD_5S: "5సె వెనక్కి", + SPEED_DECREASE_POINT_ONE: "వేగం -0.1", + SPEED_INCREASE_POINT_ONE: "వేగం +0.1", + HALVE_SPEED: "వేగం సగానికి", + DOUBLE_SPEED: "వేగం రెట్టింపు", + BACKWARD_FRAME: "ఫ్రేము వెనక్కి", + FORWARD_FRAME: "ఫ్రేము ముందుకి", + AUDIO: "ఆడియో మెనూ", + INCREASE_VOLUME: "ధ్వని పెంచు", + DECREASE_VOLUME: "ధ్వని తగ్గించు", + MUTE_VOLUME: "మ్యూట్ చేయి", + VIDEO: "వీడియో మెనూ", + CYCLE_VIDEO: "వీడియో ని సైకిల్ చేయి", + SUBTITLES: "ఉపశీర్షిక/subtitles", + CYCLE_SUB_TRACK: "ఉపశీర్షిక ట్రాక్ ని సైకిల్ చేయి", + CYCLE_AUDIO_TRACK: "ఆడియో ట్రాక్ ని సైకిల్ చేయి", + VIEW: "దర్శన", + FULLSCREEN: "స్క్రీన్ నింపు", + STATS: "గణాంకాలు", + TOGGLE_NYAN_CAT: "Nyan Cat నుండి/కు మార్చు", + ABOUT: "మా గురుంచి", + ABOUT_QT: "Qt గురించి", + TITLE: "టైటిల్", + TOGGLE_ALWAYS_ON_TOP: "పైవైపు నుండి/కు మార్చు", + DISABLE_TRACK: "ట్రాక్ ని ఆపు", + AUDIO_DEVICES: "ఆడియో పరికరాలు", + PLAYLIST_MENU: "ప్లేయలిస్ట్ మెనూ", + THEME: "థీమ్ ", + SETTINGS: "సెట్టింగులు", + LANGUAGE: "భాష ", + APPEARANCE: "ప్రదర్శన", + TITLE_ONLY_ON_FULLSCREEN: "స్క్రీన్ నింపినప్పుడే టైటిల్ చూపు", + CLICK_TO_PAUSE: "పౌస్ చేయడానికి క్లిక్/టాప్ చేయండి", + DOUBLE_TAP_TO_SEEK: "సీక్ చేయడానికి డబల్ టాప్/క్లిక్ చేయండి", + DOUBLE_TAP_TO_SEEK_BY: "సీక్ ఆధారం", + FONT: "పదాలు/ఫాంట్", + SUBTITLES_FONT_SIZE: "సుబ్టైటల్ పదాలు/ఫాంట్ సైజు", + UI_FADE_TIME: "UI వీడిపోవు(మిల్లీ సె)", + UPDATE_APPIMAGE: "అప్డేట్ చేయి (AppImage Only)" + }, + vietnamese: { + OPEN_FILE: "Mở Tập Tin", + URL_FILE_PATH: "URL / Đường dẫn tập tin", + FILE_MENU: "Tập Tin", + OPEN_URL: "Mở URL", + EXIT: "Thoát", + PLAYBACK: "Phát lại", + PLAY_PAUSE: "Phát/Tạm dừng", + REWIND_10S: "Tua lại 10 giây", + FORWARD_10S: "Tua tiến 10 giây", + REWIND_5S: "Tua lại 5 giây", + FORWARD_5S: "Tua tiến 5 giây", + SPEED_DECREASE_POINT_ONE: "Tốc độ -0.1", + SPEED_INCREASE_POINT_ONE: "Tốc độ +0.1", + HALVE_SPEED: "Tốc độ phân đôi", + DOUBLE_SPEED: "Tốc độ nhân đôi", + BACKWARD_FRAME: "Khung trước", + FORWARD_FRAME: "Khung tiếp theo", + AUDIO: "Âm thanh", + INCREASE_VOLUME: "Tăng âm lượng", + DECREASE_VOLUME: "Giảm âm lượng", + MUTE_VOLUME: "Tắt âm thanh", + VIDEO: "Video", + CYCLE_VIDEO: "Đảo video", + SUBTITLES: "Phụ đề", + CYCLE_SUB_TRACK: "Đảo Subtitle Track", + CYCLE_AUDIO_TRACK: "Đảo Audio Track", + VIEW: "Nhìn", + FULLSCREEN: "Toàn màn hình", + STATS: "Thống kê", + TOGGLE_NYAN_CAT: "Bật/Tắt Nyan Cat", + ABOUT: "Giới thiệu", + ABOUT_QT: "Giới thiệu về Qt", + TITLE: "Tiêu đề", + TOGGLE_ALWAYS_ON_TOP: "Bật/Tắt luôn ở trên", + DISABLE_TRACK: "Tắt Track", + AUDIO_DEVICES: "Thiết bị Audio", + PLAYLIST_MENU: "Menu danh sách phát", + THEME: "Diện mạo", + SETTINGS: "Tùy chỉnh", + LANGUAGE: "Ngôn ngữ", + APPEARANCE: "Giao diện", + TITLE_ONLY_ON_FULLSCREEN: "Chỉ hiện thị tiêu đè khi ở toàn màn hình", + CLICK_TO_PAUSE: "Chạm/Click để tạm dừng", + DOUBLE_TAP_TO_SEEK: "Chạm/Click đúp để lướt", + DOUBLE_TAP_TO_SEEK_BY: "Lướt đến", + FONT: "Kiểu chữ", + SUBTITLES_FONT_SIZE: "Kích thước kiểu chữ của phụ đề", + UI_FADE_TIME: "Thời gian làm mợ giao diện (ms)", + UPDATE_APPIMAGE: "Cập nhật (Chỉ dành cho AppImage)" + } +} diff --git a/src/qml/icon.png b/src/qml/icon.png new file mode 100644 index 0000000..7ae0d77 Binary files /dev/null and b/src/qml/icon.png differ diff --git a/src/qml/icons/Niconico/backward.svg b/src/qml/icons/Niconico/backward.svg new file mode 100644 index 0000000..1f4120b --- /dev/null +++ b/src/qml/icons/Niconico/backward.svg @@ -0,0 +1,2 @@ + + diff --git a/src/qml/icons/Niconico/forward.svg b/src/qml/icons/Niconico/forward.svg new file mode 100644 index 0000000..b6e1557 --- /dev/null +++ b/src/qml/icons/Niconico/forward.svg @@ -0,0 +1 @@ + diff --git a/src/qml/icons/Niconico/fullscreen.svg b/src/qml/icons/Niconico/fullscreen.svg new file mode 100644 index 0000000..ca141bd --- /dev/null +++ b/src/qml/icons/Niconico/fullscreen.svg @@ -0,0 +1 @@ + diff --git a/src/qml/icons/Niconico/next.svg b/src/qml/icons/Niconico/next.svg new file mode 100644 index 0000000..7ff4a9a --- /dev/null +++ b/src/qml/icons/Niconico/next.svg @@ -0,0 +1 @@ + diff --git a/src/qml/icons/Niconico/pause.svg b/src/qml/icons/Niconico/pause.svg new file mode 100644 index 0000000..48620df --- /dev/null +++ b/src/qml/icons/Niconico/pause.svg @@ -0,0 +1 @@ + diff --git a/src/qml/icons/Niconico/play.svg b/src/qml/icons/Niconico/play.svg new file mode 100644 index 0000000..c9676e3 --- /dev/null +++ b/src/qml/icons/Niconico/play.svg @@ -0,0 +1 @@ + diff --git a/src/qml/icons/Niconico/prev.svg b/src/qml/icons/Niconico/prev.svg new file mode 100644 index 0000000..01b2d8a --- /dev/null +++ b/src/qml/icons/Niconico/prev.svg @@ -0,0 +1 @@ + diff --git a/src/qml/icons/Niconico/settings.svg b/src/qml/icons/Niconico/settings.svg new file mode 100644 index 0000000..273b638 --- /dev/null +++ b/src/qml/icons/Niconico/settings.svg @@ -0,0 +1 @@ + diff --git a/src/qml/icons/Niconico/volume-down.svg b/src/qml/icons/Niconico/volume-down.svg new file mode 100644 index 0000000..1b8ff4a --- /dev/null +++ b/src/qml/icons/Niconico/volume-down.svg @@ -0,0 +1 @@ + diff --git a/src/qml/icons/Niconico/volume-mute.svg b/src/qml/icons/Niconico/volume-mute.svg new file mode 100644 index 0000000..4a94e03 --- /dev/null +++ b/src/qml/icons/Niconico/volume-mute.svg @@ -0,0 +1 @@ + diff --git a/src/qml/icons/Niconico/volume-up.svg b/src/qml/icons/Niconico/volume-up.svg new file mode 100644 index 0000000..1b8ff4a --- /dev/null +++ b/src/qml/icons/Niconico/volume-up.svg @@ -0,0 +1 @@ + diff --git a/src/qml/icons/RoosterTeeth/backward.svg b/src/qml/icons/RoosterTeeth/backward.svg new file mode 100644 index 0000000..633505a --- /dev/null +++ b/src/qml/icons/RoosterTeeth/backward.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qml/icons/RoosterTeeth/forward.svg b/src/qml/icons/RoosterTeeth/forward.svg new file mode 100644 index 0000000..3b31b1b --- /dev/null +++ b/src/qml/icons/RoosterTeeth/forward.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qml/icons/RoosterTeeth/fullscreen.svg b/src/qml/icons/RoosterTeeth/fullscreen.svg new file mode 100644 index 0000000..1b18f46 --- /dev/null +++ b/src/qml/icons/RoosterTeeth/fullscreen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qml/icons/RoosterTeeth/next.svg b/src/qml/icons/RoosterTeeth/next.svg new file mode 100644 index 0000000..091d846 --- /dev/null +++ b/src/qml/icons/RoosterTeeth/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qml/icons/RoosterTeeth/pause.svg b/src/qml/icons/RoosterTeeth/pause.svg new file mode 100644 index 0000000..f251bc1 --- /dev/null +++ b/src/qml/icons/RoosterTeeth/pause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qml/icons/RoosterTeeth/play.svg b/src/qml/icons/RoosterTeeth/play.svg new file mode 100755 index 0000000..98cc016 --- /dev/null +++ b/src/qml/icons/RoosterTeeth/play.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qml/icons/RoosterTeeth/playlist.svg b/src/qml/icons/RoosterTeeth/playlist.svg new file mode 100644 index 0000000..58726ee --- /dev/null +++ b/src/qml/icons/RoosterTeeth/playlist.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qml/icons/RoosterTeeth/prev.svg b/src/qml/icons/RoosterTeeth/prev.svg new file mode 100644 index 0000000..7281c18 --- /dev/null +++ b/src/qml/icons/RoosterTeeth/prev.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qml/icons/RoosterTeeth/settings.svg b/src/qml/icons/RoosterTeeth/settings.svg new file mode 100644 index 0000000..ef27416 --- /dev/null +++ b/src/qml/icons/RoosterTeeth/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qml/icons/RoosterTeeth/subtitles.svg b/src/qml/icons/RoosterTeeth/subtitles.svg new file mode 100644 index 0000000..2377483 --- /dev/null +++ b/src/qml/icons/RoosterTeeth/subtitles.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qml/icons/RoosterTeeth/volume-down.svg b/src/qml/icons/RoosterTeeth/volume-down.svg new file mode 100644 index 0000000..ba28a46 --- /dev/null +++ b/src/qml/icons/RoosterTeeth/volume-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qml/icons/RoosterTeeth/volume-mute.svg b/src/qml/icons/RoosterTeeth/volume-mute.svg new file mode 100644 index 0000000..0957a30 --- /dev/null +++ b/src/qml/icons/RoosterTeeth/volume-mute.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qml/icons/RoosterTeeth/volume-up.svg b/src/qml/icons/RoosterTeeth/volume-up.svg new file mode 100644 index 0000000..dfd49ab --- /dev/null +++ b/src/qml/icons/RoosterTeeth/volume-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qml/icons/YouTube/backward.svg b/src/qml/icons/YouTube/backward.svg new file mode 100644 index 0000000..633505a --- /dev/null +++ b/src/qml/icons/YouTube/backward.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qml/icons/YouTube/forward.svg b/src/qml/icons/YouTube/forward.svg new file mode 100644 index 0000000..3b31b1b --- /dev/null +++ b/src/qml/icons/YouTube/forward.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qml/icons/YouTube/fullscreen.svg b/src/qml/icons/YouTube/fullscreen.svg new file mode 100644 index 0000000..1b18f46 --- /dev/null +++ b/src/qml/icons/YouTube/fullscreen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qml/icons/YouTube/next.svg b/src/qml/icons/YouTube/next.svg new file mode 100644 index 0000000..091d846 --- /dev/null +++ b/src/qml/icons/YouTube/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qml/icons/YouTube/pause.svg b/src/qml/icons/YouTube/pause.svg new file mode 100644 index 0000000..f251bc1 --- /dev/null +++ b/src/qml/icons/YouTube/pause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qml/icons/YouTube/play.svg b/src/qml/icons/YouTube/play.svg new file mode 100755 index 0000000..98cc016 --- /dev/null +++ b/src/qml/icons/YouTube/play.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qml/icons/YouTube/playlist.svg b/src/qml/icons/YouTube/playlist.svg new file mode 100644 index 0000000..58726ee --- /dev/null +++ b/src/qml/icons/YouTube/playlist.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qml/icons/YouTube/prev.svg b/src/qml/icons/YouTube/prev.svg new file mode 100644 index 0000000..7281c18 --- /dev/null +++ b/src/qml/icons/YouTube/prev.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qml/icons/YouTube/settings.svg b/src/qml/icons/YouTube/settings.svg new file mode 100644 index 0000000..ef27416 --- /dev/null +++ b/src/qml/icons/YouTube/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qml/icons/YouTube/subtitles.svg b/src/qml/icons/YouTube/subtitles.svg new file mode 100644 index 0000000..2377483 --- /dev/null +++ b/src/qml/icons/YouTube/subtitles.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qml/icons/YouTube/volume-down.svg b/src/qml/icons/YouTube/volume-down.svg new file mode 100644 index 0000000..ba28a46 --- /dev/null +++ b/src/qml/icons/YouTube/volume-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qml/icons/YouTube/volume-mute.svg b/src/qml/icons/YouTube/volume-mute.svg new file mode 100644 index 0000000..0957a30 --- /dev/null +++ b/src/qml/icons/YouTube/volume-mute.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qml/icons/YouTube/volume-up.svg b/src/qml/icons/YouTube/volume-up.svg new file mode 100644 index 0000000..dfd49ab --- /dev/null +++ b/src/qml/icons/YouTube/volume-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/qml/icons/nyancache.gif b/src/qml/icons/nyancache.gif new file mode 100644 index 0000000..cc18c7c Binary files /dev/null and b/src/qml/icons/nyancache.gif differ diff --git a/src/qml/icons/nyancat.gif b/src/qml/icons/nyancat.gif new file mode 100644 index 0000000..bf0314f Binary files /dev/null and b/src/qml/icons/nyancat.gif differ diff --git a/src/qml/icons/rainbow.png b/src/qml/icons/rainbow.png new file mode 100644 index 0000000..1c16a54 Binary files /dev/null and b/src/qml/icons/rainbow.png differ diff --git a/src/qml/main.qml b/src/qml/main.qml new file mode 100644 index 0000000..1fa95ff --- /dev/null +++ b/src/qml/main.qml @@ -0,0 +1,473 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.3 +import QtQuick.Window 2.2 +import Qt.labs.settings 1.0 +import player 1.0 + +Window { + id: mainWindow + title: "VideoPlayer" + visible: true + width: Math.min(appearance.defaultWidth, Screen.width) + height: Math.min(appearance.defaultHeight, Screen.height) + property bool controlsShowing: true + property int virtualHeight: Screen.height * appearance.scaleFactor + property int virtualWidth: Screen.width * appearance.scaleFactor + property bool onTop: false + + DebugDialog { + + } + + + QMLDebugger { + id: qmlDebugger + } + + Item { + id: globalConnections + signal showUI + signal hideUI + } + + function getAppearanceValueForTheme(themeName, name) { + switch (themeName) { + case "YouTube": + return youTubeAppearance[name] + case "Niconico": + return nicoNicoAppearance[name] + case "RoosterTeeth": + return roosterTeethAppearance[name] + default: + appearance.themeName = "YouTube" + return youTubeAppearance[name] + } + } + + Translator { + id: translate + } + + Settings { + id: loggingSettings + category: "Logging" + property string logFile: "/tmp/VideoPlayer.log" + property bool logBackend: true + } + + Settings { + id: backendSettings + category: "Backend" + property string backend: "mpv" + property bool fbo: true + property bool direct: false + } + + Settings { + id: appearance + category: "Appearance" + property bool titleOnlyOnFullscreen: true + property string themeName: "YouTube" + property string fontName: "Roboto" + property double scaleFactor: 1.0 + property int subtitlesFontSize: 24 + property int defaultHeight: 405 + property int defaultWidth: 720 + property int uiFadeTimer: 1000 + property bool doubleTapToSeek: true + property double doubleTapToSeekBy: 5 + property bool swipeToResize: true + // Can fix some screen tearing on some devices. + property bool maximizeInsteadOfFullscreen: false + } + + Settings { + id: youTubeAppearance + category: "YouTubeAppearance" + property string mainBackground: "#9C000000" + property string progressBackgroundColor: "#33FFFFFF" + property string progressCachedColor: "#66FFFFFF" + property string buttonColor: "white" + property string buttonHoverColor: "white" + property string progressSliderColor: "red" + property string chapterMarkerColor: "#fc0" + property string volumeSliderBackground: "white" + } + + Settings { + id: nicoNicoAppearance + category: "NicoNicoAppearance" + property string mainBackground: "#9C000000" + property string progressBackgroundColor: "#444" + property string progressCachedColor: "#66FFFFFF" + property string buttonColor: "white" + property string buttonHoverColor: "white" + property string progressSliderColor: "#007cff" + property string chapterMarkerColor: "#fc0" + property string volumeSliderBackground: "#007cff" + } + + Settings { + id: roosterTeethAppearance + category: "RoosterTeethAppearance" + property string mainBackground: "#CC2B333F" + property string progressBackgroundColor: "#444" + property string progressCachedColor: "white" + property string buttonColor: "white" + property string buttonHoverColor: "#c9373f" + property string progressSliderColor: "#c9373f" + property string chapterMarkerColor: "#fc0" + property string volumeSliderBackground: "#c9373f" + } + + Settings { + id: i18n + category: "I18N" + property string language: "english" + } + + Settings { + id: fun + category: "Fun" + property bool nyanCat: false + } + + Settings { + id: keybinds + category: "Keybinds" + property string playPause: "K" + property string forward10: "L" + property string rewind10: "J" + property string forward5: "Right" + property string rewind5: "Left" + property string openFile: "Ctrl+O" + property string openURI: "Ctrl+Shift+O" + property string quit: "Ctrl+Q" + property string fullscreen: "F" + property string tracks: "Ctrl+T" + property string statsForNerds: "I" + property string forwardFrame: "." + property string backwardFrame: "," + property string cycleSub: "Alt+S" + property string cycleSubBackwards: "Alt+Shift+S" + property string cycleAudio: "A" + property string cycleVideo: "V" + property string cycleVideoAspect: "Shift+A" + property string previousChapter: "Ctrl+Left" + property string nextChapter: "Ctrl+Right" + property string nyanCat: "Ctrl+N" + property string decreaseSpeedByPointOne: "[" + property string increaseSpeedByPointOne: "]" + property string halveSpeed: "{" + property string doubleSpeed: "}" + property string increaseVolume: "*" + property string decreaseVolume: "/" + property string mute: "m" + property string increaseScale: "Ctrl+Shift+=" + property string resetScale: "Ctrl+Shift+0" + property string decreaseScale: "Ctrl+Shift+-" + property string customKeybind0: "" + property string customKeybind0Command: "" + property string customKeybind1: "" + property string customKeybind1Command: "" + property string customKeybind2: "" + property string customKeybind2Command: "" + property string customKeybind3: "" + property string customKeybind3Command: "" + property string customKeybind4: "" + property string customKeybind4Command: "" + property string customKeybind5: "" + property string customKeybind5Command: "" + property string customKeybind6: "" + property string customKeybind6Command: "" + property string customKeybind7: "" + property string customKeybind7Command: "" + property string customKeybind8: "" + property string customKeybind8Command: "" + property string customKeybind9: "" + property string customKeybind9Command: "" + } + + property int lastScreenVisibility + + function toggleFullscreen() { + var fs = Window.FullScreen + if (appearance.maximizeInsteadOfFullscreen) { + fs = Window.Maximized + } + + if (mainWindow.visibility != fs) { + lastScreenVisibility = mainWindow.visibility + mainWindow.visibility = fs + } else { + mainWindow.visibility = lastScreenVisibility + } + } + + Utils { + id: utils + } + + PlayerBackend { + id: player + anchors.fill: parent + width: parent.width + height: parent.height + logging: loggingSettings.logBackend + z: 1 + + Action { + onTriggered: { + appearance.scaleFactor += 0.1 + } + shortcut: keybinds.increaseScale + } + Action { + onTriggered: { + appearance.scaleFactor = 1 + } + shortcut: keybinds.resetScale + } + Action { + onTriggered: { + appearance.scaleFactor -= 0.1 + } + shortcut: keybinds.decreaseScale + } + + function startPlayer() { + //console.info(qmlDebugger.properties(player)) + console.info("OwO!") + + var args = Qt.application.arguments + var len = Qt.application.arguments.length + var argNo = 0 + if (len > 1) { + for (argNo = 1; argNo < len; argNo++) { + var argument = args[argNo] + if (argument.indexOf("VideoPlayer") !== -1) { + continue + } + if (argument.startsWith("--")) { + argument = argument.substr(2) + if (argument.length > 0) { + var splitArg = argument.split(/=(.+)/) + if (splitArg[0] == "screen" || splitArg[0] == "fs-screen") { + for (var i = 0, len = Qt.application.screens.length; i < len; i++) { + var screen = Qt.application.screens[i] + console.log( + "Screen Name: " + screen["name"] + " Screen Number: " + String( + i)) + if (screen["name"] == splitArg[1] || String( + i) == splitArg[1]) { + console.log("Switching to screen: " + screen["name"]) + mainWindow.screen = screen + mainWindow.width = mainWindow.screen.width / 2 + mainWindow.height = mainWindow.screen.height / 2 + mainWindow.x = mainWindow.screen.virtualX + mainWindow.width / 2 + mainWindow.y = mainWindow.screen.virtualY + mainWindow.height / 2 + if (splitArg[0] == "fs-screen") { + toggleFullscreen() + } + continue + } + } + continue + } + if (splitArg[0] == "fullscreen") { + toggleFullscreen() + continue + } + if (splitArg[1] == undefined || splitArg[1].length == 0) { + splitArg[1] = "yes" + } + player.setOption(splitArg[0], splitArg[1]) + } + } else { + player.playerCommand(Enums.Commands.AppendFile, argument) + } + } + } + } + } + + Item { + id: controlsOverlay + anchors.centerIn: player + height: player.height + width: player.width + z: 2 + + MouseArea { + id: mouseAreaBar + width: parent.width + height: controlsBar.combinedHeight * 1.5 + hoverEnabled: true + anchors { + bottom: parent.bottom + bottomMargin: 0 + } + onEntered: { + mouseAreaPlayerTimer.stop() + } + } + + MouseArea { + id: mouseAreaPlayer + z: 10 + focus: true + width: parent.width + hoverEnabled: true + propagateComposedEvents: true + cursorShape: mainWindow.controlsShowing ? Qt.ArrowCursor : Qt.BlankCursor + property real velocity: 0.0 + property int xStart: 0 + property int xPrev: 0 + anchors { + bottom: mouseAreaBar.top + bottomMargin: 10 + right: parent.right + rightMargin: 0 + left: parent.left + leftMargin: 0 + top: topBar.bottom + topMargin: 0 + } + + Timer { + id: mouseTapTimer + interval: 200 + onTriggered: { + mainWindow.controlsShowing = !mainWindow.controlsShowing || topBar.anythingOpen() || mouseAreaTopBar.containsMouse + mouseAreaPlayerTimer.restart() + } + } + + function doubleMouseClick(mouse) { + if (appearance.doubleTapToSeek) { + if (mouse.x > (mouseAreaPlayer.width / 2)) { + player.playerCommand(Enums.Commands.Seek, + String(appearance.doubleTapToSeekBy)) + } else { + player.playerCommand(Enums.Commands.Seek, + "-" + String(appearance.doubleTapToSeekBy)) + } + } else { + toggleFullscreen() + } + } + onClicked: function (mouse) { + xStart = mouse.x + xPrev = mouse.x + velocity = 0 + if (mouseTapTimer.running) { + doubleMouseClick(mouse) + mouseTapTimer.stop() + } else { + mouseTapTimer.restart() + } + } + onReleased: { + var isLongEnough = Math.sqrt(xStart * xStart, + mouse.x * mouse.x) > mainWindow.width * 0.3 + if (velocity > 2 && isLongEnough) { + appearance.scaleFactor += 0.2 + } else if (velocity < -2 && isLongEnough) { + if (appearance.scaleFactor > 0.8) { + appearance.scaleFactor -= 0.2 + } + } + } + onPositionChanged: { + if (mouseAreaPlayer.containsPress) { + var currVel = (mouse.x - xPrev) + velocity = (velocity + currVel) / 2.0 + xPrev = mouse.x + } + mainWindow.controlsShowing = true + mouseAreaPlayerTimer.restart() + } + Action { + onTriggered: { + toggleFullscreen() + } + shortcut: "Esc" + } + + Timer { + id: mouseAreaPlayerTimer + interval: appearance.uiFadeTimer + running: (!appearance.uiFadeTimer == 0) + repeat: false + onTriggered: { + if (!(appearance.uiFadeTimer == 0)) { + mainWindow.controlsShowing = !mainWindow.controlsShowing + } + } + } + } + + Timer { + id: statsUpdater + interval: 1000 + running: statsForNerdsText.visible + repeat: true + onTriggered: { + statsForNerdsText.text = player.getStats() + } + } + + Text { + id: statsForNerdsText + text: "" + color: "white" + visible: false + height: parent.height + width: parent.width + textFormat: Text.RichText + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignTop + renderType: Text.NativeRendering + lineHeight: 1 + font { + family: appearance.fontName + pixelSize: mainWindow.virtualHeight / 50 + } + anchors { + fill: parent + topMargin: mainWindow.virtualHeight / 20 + leftMargin: mainWindow.virtualHeight / 20 + } + Component.onCompleted: { + console.error(statsForNerdsText.lineHeight, font.pixelSize) + } + } + + MenuTitleBar { + id: topBar + z: 200 + } + + ControlsBar { + id: controlsBar + } + } + MouseArea { + id: mouseAreaTopBar + anchors { + top: parent.top + left: parent.left + right: parent.right + } + height: topBar.height * 3 + hoverEnabled: true + propagateComposedEvents: true + } + + MouseArea { + anchors.fill: parent + onExited: { + mouseAreaPlayerTimer.start() + } + } +} diff --git a/src/qml/qml.qrc b/src/qml/qml.qrc new file mode 100644 index 0000000..3ed79dc --- /dev/null +++ b/src/qml/qml.qrc @@ -0,0 +1,81 @@ + + + icon.png + main.qml + UIComponents/ControlsLayouts/YouTubeButtonLayout.qml + UIComponents/ControlsLayouts/NiconicoButtonLayout.qml + UIComponents/ControlsLayouts/RoosterTeethButtonLayout.qml + UIComponents/ControlsBar/ControlsBar.qml + UIComponents/MenuBar/MainMenu.qml + UIComponents/MenuBar/CustomMenu.qml + UIComponents/MenuBar/MenuTitleBar.qml + UIComponents/Controls/SmoothButton.qml + UIComponents/Controls/VerticalVolume.qml + UIComponents/Controls/SpeedText.qml + UIComponents/Controls/ForwardButton.qml + UIComponents/Controls/BackwardButton.qml + UIComponents/Controls/PlaylistPrevButton.qml + UIComponents/Controls/PlayPauseButton.qml + UIComponents/Controls/VideoProgress.qml + UIComponents/Controls/VolumeButton.qml + UIComponents/Controls/VolumeSlider.qml + UIComponents/Controls/TimeLabel.qml + UIComponents/Controls/SettingsButton.qml + UIComponents/Controls/FullscreenButton.qml + UIComponents/Controls/PlaylistNextButton.qml + Utils/codes.js + Utils/Translator.qml + Utils/translations.js + Items/ChapterMarkerItem.qml + Items/TrackItem.qml + Items/AudioDeviceItem.qml + Items/ThumbnailProcess.qml + Items/TitleProcess.qml + Items/CustomMenuItem.qml + Dialogs/SettingsDialog.qml + Dialogs/SettingsItems/LanguageSettings.qml + Dialogs/PlaylistDialog.qml + Dialogs/DebugDialog.qml + icons/YouTube/play.svg + icons/YouTube/pause.svg + icons/YouTube/forward.svg + icons/YouTube/backward.svg + icons/YouTube/settings.svg + icons/YouTube/fullscreen.svg + icons/YouTube/volume-up.svg + icons/YouTube/volume-mute.svg + icons/YouTube/volume-down.svg + icons/YouTube/next.svg + icons/YouTube/prev.svg + icons/YouTube/subtitles.svg + icons/YouTube/playlist.svg + icons/RoosterTeeth/play.svg + icons/RoosterTeeth/pause.svg + icons/RoosterTeeth/forward.svg + icons/RoosterTeeth/backward.svg + icons/RoosterTeeth/settings.svg + icons/RoosterTeeth/fullscreen.svg + icons/RoosterTeeth/volume-up.svg + icons/RoosterTeeth/volume-mute.svg + icons/RoosterTeeth/volume-down.svg + icons/RoosterTeeth/next.svg + icons/RoosterTeeth/prev.svg + icons/RoosterTeeth/subtitles.svg + icons/RoosterTeeth/playlist.svg + + icons/Niconico/play.svg + icons/Niconico/pause.svg + icons/Niconico/settings.svg + icons/Niconico/volume-up.svg + icons/Niconico/volume-down.svg + icons/Niconico/volume-mute.svg + icons/Niconico/prev.svg + icons/Niconico/next.svg + icons/Niconico/fullscreen.svg + icons/Niconico/forward.svg + icons/Niconico/backward.svg + icons/nyancat.gif + icons/nyancache.gif + icons/rainbow.png + + diff --git a/src/qml/qml.qrc.depends b/src/qml/qml.qrc.depends new file mode 100644 index 0000000..2ca6d2a --- /dev/null +++ b/src/qml/qml.qrc.depends @@ -0,0 +1,77 @@ + + + icon.png + main.qml + CustomMenu.qml + ControlsBar.qml + MainMenu.qml + YouTubeButtonLayout.qml + NiconicoButtonLayout.qml + RoosterTeethButtonLayout.qml + UIComponents/SmoothButton.qml + UIComponents/ButtonImage.qml + UIComponents/VerticalVolume.qml + UIComponents/SpeedText.qml + UIComponents/ForwardButton.qml + UIComponents/BackwardButton.qml + UIComponents/PlaylistPrevButton.qml + UIComponents/PlayPauseButton.qml + UIComponents/VideoProgress.qml + UIComponents/VolumeButton.qml + UIComponents/VolumeSlider.qml + UIComponents/TimeLabel.qml + UIComponents/SettingsButton.qml + UIComponents/FullscreenButton.qml + UIComponents/PlaylistNextButton.qml + Utils/codes.js + Utils/Translator.qml + Utils/translations.js + Items/ChapterMarkerItem.qml + Items/TrackItem.qml + Items/AudioDeviceItem.qml + Items/ThumbnailProcess.qml + Items/TitleProcess.qml + Items/CustomMenuItem.qml + Dialogs/PlaylistDialog.qml + icons/YouTube/play.svg + icons/YouTube/pause.svg + icons/YouTube/forward.svg + icons/YouTube/backward.svg + icons/YouTube/settings.svg + icons/YouTube/fullscreen.svg + icons/YouTube/volume-up.svg + icons/YouTube/volume-mute.svg + icons/YouTube/volume-down.svg + icons/YouTube/next.svg + icons/YouTube/prev.svg + icons/YouTube/subtitles.svg + icons/YouTube/playlist.svg + icons/RoosterTeeth/play.svg + icons/RoosterTeeth/pause.svg + icons/RoosterTeeth/forward.svg + icons/RoosterTeeth/backward.svg + icons/RoosterTeeth/settings.svg + icons/RoosterTeeth/fullscreen.svg + icons/RoosterTeeth/volume-up.svg + icons/RoosterTeeth/volume-mute.svg + icons/RoosterTeeth/volume-down.svg + icons/RoosterTeeth/next.svg + icons/RoosterTeeth/prev.svg + icons/RoosterTeeth/subtitles.svg + icons/RoosterTeeth/playlist.svg + + icons/Niconico/play.svg + icons/Niconico/pause.svg + icons/Niconico/settings.svg + icons/Niconico/volume-up.svg + icons/Niconico/volume-down.svg + icons/Niconico/volume-mute.svg + icons/Niconico/prev.svg + icons/Niconico/next.svg + icons/Niconico/fullscreen.svg + icons/Niconico/forward.svg + icons/Niconico/backward.svg + icons/nyancat.gif + icons/rainbow.png + + diff --git a/src/qmldebugger.cpp b/src/qmldebugger.cpp new file mode 100644 index 0000000..f24e56d --- /dev/null +++ b/src/qmldebugger.cpp @@ -0,0 +1,34 @@ +#include "qmldebugger.h" +#include +#include +#include +#include + +QString +QMLDebugger::properties(QQuickItem* item, bool linebreak) +{ + const QMetaObject* meta = item->metaObject(); + + QHash list; + for (int i = 0; i < meta->propertyCount(); i++) { + QMetaProperty property = meta->property(i); + const char* name = property.name(); + QVariant value = item->property(name); + list[name] = value; + } + + QString out; + QHashIterator i(list); + while (i.hasNext()) { + i.next(); + if (!out.isEmpty()) { + out += ", "; + if (linebreak) + out += "\n"; + } + out.append(i.key()); + out.append(": "); + out.append(i.value().toString()); + } + return out; +} diff --git a/src/qmldebugger.h b/src/qmldebugger.h new file mode 100644 index 0000000..65e5ec8 --- /dev/null +++ b/src/qmldebugger.h @@ -0,0 +1,15 @@ +#ifndef QMLDEBUGGER_H +#define QMLDEBUGGER_H + +#include +#include +#include + +class QMLDebugger : public QObject { + Q_OBJECT +public: + Q_INVOKABLE static QString properties(QQuickItem* item, + bool linebreak = true); +}; + +#endif // QMLDEBUGGER_H diff --git a/src/qthelper.hpp b/src/qthelper.hpp new file mode 100644 index 0000000..7bd4193 --- /dev/null +++ b/src/qthelper.hpp @@ -0,0 +1,368 @@ +#ifndef LIBMPV_QTHELPER_H_ +#define LIBMPV_QTHELPER_H_ + +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace mpv { +namespace qt { + + // Wrapper around mpv_handle. Does refcounting under the hood. + class Handle { + struct container { + container(mpv_handle* h) + : mpv(h) + { + } + ~container() { mpv_terminate_destroy(mpv); } + mpv_handle* mpv; + }; + QSharedPointer sptr; + + public: + // Construct a new Handle from a raw mpv_handle with refcount 1. If the + // last Handle goes out of scope, the mpv_handle will be destroyed with + // mpv_terminate_destroy(). + // Never destroy the mpv_handle manually when using this wrapper. You + // will create dangling pointers. Just let the wrapper take care of + // destroying the mpv_handle. + // Never create multiple wrappers from the same raw mpv_handle; copy the + // wrapper instead (that's what it's for). + static Handle FromRawHandle(mpv_handle* handle) + { + Handle h; + h.sptr = QSharedPointer(new container(handle)); + return h; + } + + // Return the raw handle; for use with the libmpv C API. + operator mpv_handle*() const { return sptr ? (*sptr).mpv : 0; } + }; + + static inline QVariant node_to_variant(const mpv_node* node) + { + switch (node->format) { + case MPV_FORMAT_STRING: + return QVariant(QString::fromUtf8(node->u.string)); + case MPV_FORMAT_FLAG: + return QVariant(static_cast(node->u.flag)); + case MPV_FORMAT_INT64: + return QVariant(static_cast(node->u.int64)); + case MPV_FORMAT_DOUBLE: + return QVariant(node->u.double_); + case MPV_FORMAT_NODE_ARRAY: { + mpv_node_list* list = node->u.list; + QVariantList qlist; + for (int n = 0; n < list->num; n++) + qlist.append(node_to_variant(&list->values[n])); + return QVariant(qlist); + } + case MPV_FORMAT_NODE_MAP: { + mpv_node_list* list = node->u.list; + QVariantMap qmap; + for (int n = 0; n < list->num; n++) { + qmap.insert(QString::fromUtf8(list->keys[n]), + node_to_variant(&list->values[n])); + } + return QVariant(qmap); + } + default: // MPV_FORMAT_NONE, unknown values (e.g. future extensions) + return QVariant(); + } + } + + struct node_builder { + node_builder(const QVariant& v) + { + set(&node_, v); + } + ~node_builder() + { + free_node(&node_); + } + mpv_node* node() { return &node_; } + + private: + Q_DISABLE_COPY(node_builder) + mpv_node node_; + mpv_node_list* create_list(mpv_node* dst, bool is_map, int num) + { + dst->format = is_map ? MPV_FORMAT_NODE_MAP : MPV_FORMAT_NODE_ARRAY; + mpv_node_list* list = new mpv_node_list(); + dst->u.list = list; + if (!list) + goto err; + list->values = new mpv_node[num](); + if (!list->values) + goto err; + if (is_map) { + list->keys = new char*[num](); + if (!list->keys) + goto err; + } + return list; + err: + free_node(dst); + return NULL; + } + char* dup_qstring(const QString& s) + { + QByteArray b = s.toUtf8(); + char* r = new char[b.size() + 1]; + if (r) + std::memcpy(r, b.data(), b.size() + 1); + return r; + } + bool test_type(const QVariant& v, QMetaType::Type t) + { + // The Qt docs say: "Although this function is declared as returning + // "QVariant::Type(obsolete), the return value should be interpreted + // as QMetaType::Type." + // So a cast really seems to be needed to avoid warnings (urgh). + return static_cast(v.type()) == static_cast(t); + } + void set(mpv_node* dst, const QVariant& src) + { + if (test_type(src, QMetaType::QString)) { + dst->format = MPV_FORMAT_STRING; + dst->u.string = dup_qstring(src.toString()); + if (!dst->u.string) + goto fail; + } else if (test_type(src, QMetaType::Bool)) { + dst->format = MPV_FORMAT_FLAG; + dst->u.flag = src.toBool() ? 1 : 0; + } else if (test_type(src, QMetaType::Int) || test_type(src, QMetaType::LongLong) || test_type(src, QMetaType::UInt) || test_type(src, QMetaType::ULongLong)) { + dst->format = MPV_FORMAT_INT64; + dst->u.int64 = src.toLongLong(); + } else if (test_type(src, QMetaType::Double)) { + dst->format = MPV_FORMAT_DOUBLE; + dst->u.double_ = src.toDouble(); + } else if (src.canConvert()) { + QVariantList qlist = src.toList(); + mpv_node_list* list = create_list(dst, false, qlist.size()); + if (!list) + goto fail; + list->num = qlist.size(); + for (int n = 0; n < qlist.size(); n++) + set(&list->values[n], qlist[n]); + } else if (src.canConvert()) { + QVariantMap qmap = src.toMap(); + mpv_node_list* list = create_list(dst, true, qmap.size()); + if (!list) + goto fail; + list->num = qmap.size(); + for (int n = 0; n < qmap.size(); n++) { + list->keys[n] = dup_qstring(qmap.keys()[n]); + if (!list->keys[n]) { + free_node(dst); + goto fail; + } + set(&list->values[n], qmap.values()[n]); + } + } else { + goto fail; + } + return; + fail: + dst->format = MPV_FORMAT_NONE; + } + void free_node(mpv_node* dst) + { + switch (dst->format) { + case MPV_FORMAT_STRING: + delete[] dst->u.string; + break; + case MPV_FORMAT_NODE_ARRAY: + case MPV_FORMAT_NODE_MAP: { + mpv_node_list* list = dst->u.list; + if (list) { + for (int n = 0; n < list->num; n++) { + if (list->keys) + delete[] list->keys[n]; + if (list->values) + free_node(&list->values[n]); + } + delete[] list->keys; + delete[] list->values; + } + delete list; + break; + } + default:; + } + dst->format = MPV_FORMAT_NONE; + } + }; + + /** + * RAII wrapper that calls mpv_free_node_contents() on the pointer. + */ + struct node_autofree { + mpv_node* ptr; + node_autofree(mpv_node* a_ptr) + : ptr(a_ptr) + { + } + ~node_autofree() { mpv_free_node_contents(ptr); } + }; + + /** + * Return the given property as mpv_node converted to QVariant, or QVariant() + * on error. + * + * @deprecated use get_property() instead + * + * @param name the property name + */ + static inline QVariant get_property_variant(mpv_handle* ctx, const QString& name) + { + mpv_node node; + if (mpv_get_property(ctx, name.toUtf8().data(), MPV_FORMAT_NODE, &node) < 0) + return QVariant(); + node_autofree f(&node); + return node_to_variant(&node); + } + + /** + * Set the given property as mpv_node converted from the QVariant argument. + + * @deprecated use set_property() instead + */ + static inline int set_property_variant(mpv_handle* ctx, const QString& name, + const QVariant& v) + { + node_builder node(v); + return mpv_set_property(ctx, name.toUtf8().data(), MPV_FORMAT_NODE, node.node()); + } + + /** + * Set the given option as mpv_node converted from the QVariant argument. + * + * @deprecated use set_property() instead + */ + static inline int set_option_variant(mpv_handle* ctx, const QString& name, + const QVariant& v) + { + node_builder node(v); + return mpv_set_option(ctx, name.toUtf8().data(), MPV_FORMAT_NODE, node.node()); + } + + /** + * mpv_command_node() equivalent. Returns QVariant() on error (and + * unfortunately, the same on success). + * + * @deprecated use command() instead + */ + static inline QVariant command_variant(mpv_handle* ctx, const QVariant& args) + { + node_builder node(args); + mpv_node res; + if (mpv_command_node(ctx, node.node(), &res) < 0) + return QVariant(); + node_autofree f(&res); + return node_to_variant(&res); + } + + /** + * This is used to return error codes wrapped in QVariant for functions which + * return QVariant. + * + * You can use get_error() or is_error() to extract the error status from a + * QVariant value. + */ + struct ErrorReturn { + /** + * enum mpv_error value (or a value outside of it if ABI was extended) + */ + int error; + + ErrorReturn() + : error(0) + { + } + explicit ErrorReturn(int err) + : error(err) + { + } + }; + + /** + * Return the mpv error code packed into a QVariant, or 0 (success) if it's not + * an error value. + * + * @return error code (<0) or success (>=0) + */ + static inline int get_error(const QVariant& v) + { + if (!v.canConvert()) + return 0; + return v.value().error; + } + + /** + * Return whether the QVariant carries a mpv error code. + */ + static inline bool is_error(const QVariant& v) + { + return get_error(v) < 0; + } + + /** + * Return the given property as mpv_node converted to QVariant, or QVariant() + * on error. + * + * @param name the property name + * @return the property value, or an ErrorReturn with the error code + */ + static inline QVariant get_property(mpv_handle* ctx, const QString& name) + { + mpv_node node; + int err = mpv_get_property(ctx, name.toUtf8().data(), MPV_FORMAT_NODE, &node); + if (err < 0) + return QVariant::fromValue(ErrorReturn(err)); + node_autofree f(&node); + return node_to_variant(&node); + } + + /** + * Set the given property as mpv_node converted from the QVariant argument. + * + * @return mpv error code (<0 on error, >= 0 on success) + */ + static inline int set_property(mpv_handle* ctx, const QString& name, + const QVariant& v) + { + node_builder node(v); + return mpv_set_property(ctx, name.toUtf8().data(), MPV_FORMAT_NODE, node.node()); + } + + /** + * mpv_command_node() equivalent. + * + * @param args command arguments, with args[0] being the command name as string + * @return the property value, or an ErrorReturn with the error code + */ + static inline QVariant command(mpv_handle* ctx, const QVariant& args) + { + node_builder node(args); + mpv_node res; + int err = mpv_command_node(ctx, node.node(), &res); + if (err < 0) + return QVariant::fromValue(ErrorReturn(err)); + node_autofree f(&res); + return node_to_variant(&res); + } +} +} + +Q_DECLARE_METATYPE(mpv::qt::ErrorReturn) + +#endif diff --git a/src/registerTypes.cpp b/src/registerTypes.cpp new file mode 100644 index 0000000..88f3316 --- /dev/null +++ b/src/registerTypes.cpp @@ -0,0 +1,38 @@ +#ifndef DISABLE_MPV_RENDER_API +#include "Backends/MPV/MPVBackend.hpp" +#endif +#include "Backends/MPVNoFBO/MPVNoFBOBackend.hpp" +#include "Process.h" +#include "ThumbnailCache.h" +#include "enums.hpp" +#include "qmldebugger.h" +#include "utils.hpp" +#include + +void registerTypes() +{ + QSettings settings; + + qmlRegisterUncreatableMetaObject( + Enums::staticMetaObject, "player", 1, 0, "Enums", "Error: only enums"); + qRegisterMetaType("Enums.PlayStatus"); + qRegisterMetaType("Enums.VolumeStatus"); + qRegisterMetaType("Enums.Backends"); + qRegisterMetaType("Enums.Commands"); + qmlRegisterType("player", 1, 0, "Process"); + + qmlRegisterType("player", 1, 0, "QMLDebugger"); + qmlRegisterType("player", 1, 0, "ThumbnailCache"); + + qmlRegisterType("player", 1, 0, "Utils"); + +#ifndef DISABLE_MPV_RENDER_API + if (settings.value("Backend/fbo", true).toBool()) { + qmlRegisterType("player", 1, 0, "PlayerBackend"); + } else { + qmlRegisterType("player", 1, 0, "PlayerBackend"); + } +#else + qmlRegisterType("player", 1, 0, "PlayerBackend"); +#endif +} \ No newline at end of file diff --git a/src/setenv_mingw.hpp b/src/setenv_mingw.hpp new file mode 100644 index 0000000..e47044c --- /dev/null +++ b/src/setenv_mingw.hpp @@ -0,0 +1,87 @@ +/* + * setenv.c + * + * Implementation of POSIX standard IEEE 1003.1-2008 setenv() function; + * may also be invoked inline, as "retval = setenv( varname, NULL, 1 )", + * to implement the complementary unsetenv() function. + * + * $Id$ + * + * Written by Keith Marshall + * Copyright (C) 2016, MinGW.org Project + * + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + +#include +#include +#include +#include + +int setenv(const char* var, const char* value, int overwrite) +{ + /* Core implementation for both setenv() and unsetenv() functions; + * at the outset, assume that the requested operation may fail. + */ + int retval = -1; + + /* The specified "var" name MUST be non-NULL, not a zero-length + * string, and must not include any '=' character. + */ + if (var && *var && (strchr(var, '=') == NULL)) { + /* A properly named variable may be added to, removed from, + * or modified within the environment, ONLY if "overwrite" + * mode is enabled, OR if the named variable does not yet + * exist... + */ + if (overwrite || getenv(var) == NULL) { + /* ... in which cases, we convert the specified name and + * value into the appropriate form for use with putenv(), + * (noting that we accept a NULL "value" as equivalent to + * a zero-length string, which renders putenv() as the + * equivalent of unsetenv()). + */ + const char* fmt = "%s=%s"; + const char* val = value ? value : ""; + char buf[1 + snprintf(NULL, 0, fmt, var, val)]; + snprintf(buf, sizeof(buf), fmt, var, val); + if ((retval = putenv(buf)) != 0) + /* + * If putenv() returns non-zero, indicating failure, the + * most probable explanation is that there wasn't enough + * free memory; ensure that errno is set accordingly. + */ + errno = ENOMEM; + } else + /* The named variable already exists, and overwrite mode + * was not enabled; there is nothing to be done. + */ + retval = 0; + } else + /* The specified environment variable name was invalid. + */ + errno = EINVAL; + + /* Succeed or fail, "retval" has now been set to indicate the + * appropriate status for return. + */ + return retval; +} diff --git a/src/utils.cpp b/src/utils.cpp new file mode 100644 index 0000000..6c7808a --- /dev/null +++ b/src/utils.cpp @@ -0,0 +1,143 @@ +#include "utils.hpp" +#include "logger.h" +#include "spdlog/logger.h" +#include +#include +#include +#include +#include +#include + +#if (defined(__linux__) || defined(__FreeBSD__)) && ENABLE_X11 +#include // IWYU pragma: keep +#include // IWYU pragma: keep +#include // IWYU pragma: keep +#include // IWYU pragma: keep +#include // IWYU pragma: keep +#undef Bool +#endif + +auto utilsLogger = initLogger("utils"); + +namespace Utils { +QString +getPlatformName() +{ + QGuiApplication* qapp = qobject_cast(QCoreApplication::instance()); + return qapp->platformName(); +} + +void launchAboutQt() +{ + QApplication* qapp = qobject_cast(QCoreApplication::instance()); + qapp->aboutQt(); +} + +void updateAppImage() +{ + QString program = QProcessEnvironment::systemEnvironment().value("APPDIR", "") + "/usr/bin/appimageupdatetool"; + QProcess updater; + updater.setProcessChannelMode(QProcess::ForwardedChannels); + updater.start(program, + QStringList() << QProcessEnvironment::systemEnvironment().value( + "APPIMAGE", "")); + updater.waitForFinished(); + qApp->exit(); +} + +// https://www.youtube.com/watch?v=nXaxk27zwlk&feature=youtu.be&t=56m34s +inline int +fast_mod(const int input, const int ceil) +{ + return input >= ceil ? input % ceil : input; +} + +QString +createTimestamp(const int seconds) +{ + + const int s = fast_mod(seconds, 60); + const int m = fast_mod(seconds, 3600) / 60; + const int h = fast_mod(seconds, 86400) / 3600; + + if (h > 0) { + return QString::asprintf("%02d:%02d:%02d", h, m, s); + } else { + return QString::asprintf("%02d:%02d", m, s); + } +} + +void SetScreensaver(WId wid, bool on) +{ + QProcess xdgScreensaver; + xdgScreensaver.setProcessChannelMode(QProcess::ForwardedChannels); + if (on) { + utilsLogger->info("Enabling screensaver."); + xdgScreensaver.start("xdg-screensaver", + QStringList() << "resume" << QString::number(wid)); + } else { + utilsLogger->info("Disabling screensaver."); + xdgScreensaver.start("xdg-screensaver", + QStringList() << "suspend" << QString::number(wid)); + } + xdgScreensaver.waitForFinished(); +} + +void SetDPMS(bool on) +{ +#if defined(__linux__) || defined(__FreeBSD__) + if (getPlatformName() != "xcb") { + return; + } + QProcess xsetProcess; + xsetProcess.setProcessChannelMode(QProcess::ForwardedChannels); + if (on) { + utilsLogger->info("Enabled DPMS."); + xsetProcess.start("xset", + QStringList() << "s" + << "on" + << "+dpms"); + } else { + utilsLogger->info("Disabled DPMS."); + xsetProcess.start("xset", + QStringList() << "s" + << "off" + << "-dpms"); + } + xsetProcess.waitForFinished(); +#else + utilsLogger->error("Can't set DPMS for platform: {}", + getPlatformName().toUtf8().constData()); +#endif +} + +void AlwaysOnTop(WId wid, bool on) +{ +#if (defined(__linux__) || defined(__FreeBSD__)) && ENABLE_X11 + Display* display = QX11Info::display(); + XEvent event; + event.xclient.type = ClientMessage; + event.xclient.serial = 0; + event.xclient.send_event = true; + event.xclient.display = display; + event.xclient.window = wid; + event.xclient.message_type = XInternAtom(display, "_NET_WM_STATE", False); + event.xclient.format = 32; + + event.xclient.data.l[0] = on; + event.xclient.data.l[1] = XInternAtom(display, "_NET_WM_STATE_ABOVE", False); + event.xclient.data.l[2] = 0; + event.xclient.data.l[3] = 0; + event.xclient.data.l[4] = 0; + + XSendEvent(display, + DefaultRootWindow(display), + False, + SubstructureRedirectMask | SubstructureNotifyMask, + &event); +#else + utilsLogger->error("Can't set on top for platform: {}", + getPlatformName().toUtf8().constData()); +#endif +} +} diff --git a/src/utils.hpp b/src/utils.hpp new file mode 100644 index 0000000..d7e32f3 --- /dev/null +++ b/src/utils.hpp @@ -0,0 +1,32 @@ +#ifndef UTILS_H +#define UTILS_H +#include +#include +#include + +namespace Utils { +Q_NAMESPACE +void SetDPMS(bool on); +void SetScreensaver(WId wid, bool on); +void AlwaysOnTop(WId wid, bool on); +QString +createTimestamp(int seconds); +void launchAboutQt(); +void updateAppImage(); +} + +class UtilsClass : public QObject { + Q_OBJECT +public slots: + void SetDPMS(bool on) { Utils::SetDPMS(on); }; + void AlwaysOnTop(WId wid, bool on) { Utils::AlwaysOnTop(wid, on); }; + void launchAboutQt() { Utils::launchAboutQt(); }; + void updateAppImage() { Utils::updateAppImage(); }; + + QString createTimestamp(int seconds) + { + return Utils::createTimestamp(seconds); + }; +}; + +#endif diff --git a/version b/version new file mode 100644 index 0000000..3cacc0b --- /dev/null +++ b/version @@ -0,0 +1 @@ +12 \ No newline at end of file