feat: add Kirigami app skeleton with libmpv playback
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
1eb67b829c
8 files changed
+540
No files matched your search
@@ -0,0 +1,4 @@
|
|||||||
|
build/
|
||||||
|
.cache/
|
||||||
|
compile_commands.json
|
||||||
|
*.user
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.25)
|
||||||
|
|
||||||
|
project(qtpotato VERSION 0.1.0 LANGUAGES CXX)
|
||||||
|
|
||||||
|
set(QT_MIN_VERSION "6.8.0")
|
||||||
|
set(KF_MIN_VERSION "6.10.0")
|
||||||
|
|
||||||
|
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
|
||||||
|
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
|
||||||
|
|
||||||
|
include(KDEInstallDirs)
|
||||||
|
include(KDECMakeSettings)
|
||||||
|
include(KDECompilerSettings NO_POLICY_SCOPE)
|
||||||
|
include(ECMQmlModule)
|
||||||
|
include(FeatureSummary)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
find_package(Qt6 ${QT_MIN_VERSION} REQUIRED COMPONENTS Core Gui Qml Quick QuickControls2 Widgets)
|
||||||
|
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS Kirigami I18n CoreAddons Crash)
|
||||||
|
find_package(MpvQt 1.0 REQUIRED)
|
||||||
|
|
||||||
|
find_package(KF6KirigamiAddons 1.0)
|
||||||
|
set_package_properties(KF6KirigamiAddons PROPERTIES
|
||||||
|
TYPE RUNTIME
|
||||||
|
PURPOSE "FormCard About page")
|
||||||
|
|
||||||
|
add_subdirectory(src)
|
||||||
|
|
||||||
|
feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
add_executable(qtpotato
|
||||||
|
main.cpp
|
||||||
|
mpv/mpvitem.cpp
|
||||||
|
mpv/mpvitem.h
|
||||||
|
)
|
||||||
|
|
||||||
|
ecm_add_qml_module(qtpotato
|
||||||
|
URI dev.trivk.qtpotato
|
||||||
|
)
|
||||||
|
|
||||||
|
ecm_target_qml_sources(qtpotato SOURCES
|
||||||
|
qml/Main.qml
|
||||||
|
qml/PlayerPage.qml
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(qtpotato PRIVATE mpv)
|
||||||
|
target_compile_definitions(qtpotato PRIVATE QTPOTATO_VERSION_STRING="${PROJECT_VERSION}")
|
||||||
|
|
||||||
|
target_link_libraries(qtpotato PRIVATE
|
||||||
|
Qt6::Gui
|
||||||
|
Qt6::Qml
|
||||||
|
Qt6::Quick
|
||||||
|
Qt6::QuickControls2
|
||||||
|
Qt6::Widgets
|
||||||
|
KF6::I18n
|
||||||
|
KF6::I18nQml
|
||||||
|
KF6::CoreAddons
|
||||||
|
KF6::Crash
|
||||||
|
MpvQt::MpvQt
|
||||||
|
)
|
||||||
|
|
||||||
|
install(TARGETS qtpotato ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
#include <QApplication>
|
||||||
|
#include <QCommandLineParser>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QIcon>
|
||||||
|
#include <QQmlApplicationEngine>
|
||||||
|
#include <QQuickStyle>
|
||||||
|
#include <QQuickWindow>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
|
#include <KAboutData>
|
||||||
|
#include <KCrash>
|
||||||
|
#include <KLocalizedQmlContext>
|
||||||
|
#include <KLocalizedString>
|
||||||
|
|
||||||
|
#include <clocale>
|
||||||
|
|
||||||
|
using namespace Qt::Literals::StringLiterals;
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
// MpvQt renders through an OpenGL framebuffer object, so Qt Quick must use OpenGL
|
||||||
|
// (not Vulkan). Has to be set before any window exists.
|
||||||
|
QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL);
|
||||||
|
|
||||||
|
QApplication app(argc, argv);
|
||||||
|
|
||||||
|
// libmpv parses numbers with the C locale; Qt sets the locale from the environment.
|
||||||
|
std::setlocale(LC_NUMERIC, "C");
|
||||||
|
|
||||||
|
// Use the Plasma style unless the user picked another one explicitly.
|
||||||
|
if (qEnvironmentVariableIsEmpty("QT_QUICK_CONTROLS_STYLE")) {
|
||||||
|
QQuickStyle::setStyle(u"org.kde.desktop"_s);
|
||||||
|
}
|
||||||
|
|
||||||
|
KLocalizedString::setApplicationDomain("qtpotato");
|
||||||
|
|
||||||
|
KAboutData aboutData(u"qtpotato"_s,
|
||||||
|
i18nc("@title", "QtPotato"),
|
||||||
|
QStringLiteral(QTPOTATO_VERSION_STRING),
|
||||||
|
i18n("Video player with subtitle translation"),
|
||||||
|
KAboutLicense::GPL_V3,
|
||||||
|
i18n("© 2026 Tri Vo Khac"));
|
||||||
|
aboutData.addAuthor(i18n("Tri Vo Khac"), i18nc("@info:credit", "Author"), QString(), u"https://github.com/vokhactri"_s);
|
||||||
|
aboutData.setDesktopFileName(u"dev.trivk.qtpotato"_s);
|
||||||
|
aboutData.setOrganizationDomain("trivk.dev");
|
||||||
|
KAboutData::setApplicationData(aboutData);
|
||||||
|
QApplication::setWindowIcon(QIcon::fromTheme(u"multimedia-video-player"_s));
|
||||||
|
|
||||||
|
KCrash::initialize();
|
||||||
|
|
||||||
|
QCommandLineParser parser;
|
||||||
|
aboutData.setupCommandLine(&parser);
|
||||||
|
parser.addPositionalArgument(u"file"_s, i18n("Video file to open"), u"[file]"_s);
|
||||||
|
parser.process(app);
|
||||||
|
aboutData.processCommandLine(&parser);
|
||||||
|
|
||||||
|
QUrl initialUrl;
|
||||||
|
if (const QStringList args = parser.positionalArguments(); !args.isEmpty()) {
|
||||||
|
initialUrl = QUrl::fromUserInput(args.first(), QDir::currentPath(), QUrl::AssumeLocalFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
QQmlApplicationEngine engine;
|
||||||
|
KLocalization::setupLocalizedContext(&engine);
|
||||||
|
engine.setInitialProperties({{u"initialUrl"_s, initialUrl}});
|
||||||
|
engine.loadFromModule("dev.trivk.qtpotato", "Main");
|
||||||
|
|
||||||
|
if (engine.rootObjects().isEmpty()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.exec();
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
#include "mpvitem.h"
|
||||||
|
|
||||||
|
#include <MpvController>
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
MpvItem::MpvItem(QQuickItem *parent)
|
||||||
|
: MpvAbstractItem(parent)
|
||||||
|
{
|
||||||
|
// Leave the player on the last frame at end of file instead of unloading it.
|
||||||
|
setPropertyAsync(QStringLiteral("keep-open"), QStringLiteral("yes"));
|
||||||
|
setPropertyAsync(QStringLiteral("hwdec"), QStringLiteral("auto-safe"));
|
||||||
|
|
||||||
|
observeProperty(QStringLiteral("media-title"), MPV_FORMAT_STRING);
|
||||||
|
observeProperty(QStringLiteral("time-pos"), MPV_FORMAT_DOUBLE);
|
||||||
|
observeProperty(QStringLiteral("duration"), MPV_FORMAT_DOUBLE);
|
||||||
|
observeProperty(QStringLiteral("pause"), MPV_FORMAT_FLAG);
|
||||||
|
|
||||||
|
// MpvController lives on a worker thread; queue its signals back to the GUI thread.
|
||||||
|
connect(mpvController(), &MpvController::propertyChanged, this, &MpvItem::onPropertyChanged, Qt::QueuedConnection);
|
||||||
|
connect(
|
||||||
|
mpvController(),
|
||||||
|
&MpvController::fileLoaded,
|
||||||
|
this,
|
||||||
|
[this] {
|
||||||
|
setFileLoaded(true);
|
||||||
|
},
|
||||||
|
Qt::QueuedConnection);
|
||||||
|
connect(
|
||||||
|
mpvController(),
|
||||||
|
&MpvController::endFile,
|
||||||
|
this,
|
||||||
|
[this] {
|
||||||
|
setFileLoaded(false);
|
||||||
|
},
|
||||||
|
Qt::QueuedConnection);
|
||||||
|
|
||||||
|
// The render context only exists once Qt Quick has rendered the item for the first
|
||||||
|
// time; a file loaded before that plays without video. Hold it until ready() fires.
|
||||||
|
connect(this, &MpvAbstractItem::ready, this, [this] {
|
||||||
|
m_ready = true;
|
||||||
|
if (m_pendingUrl.isValid()) {
|
||||||
|
loadFile(std::exchange(m_pendingUrl, {}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QUrl MpvItem::currentUrl() const
|
||||||
|
{
|
||||||
|
return m_currentUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString MpvItem::mediaTitle() const
|
||||||
|
{
|
||||||
|
return m_mediaTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
double MpvItem::position() const
|
||||||
|
{
|
||||||
|
return m_position;
|
||||||
|
}
|
||||||
|
|
||||||
|
double MpvItem::duration() const
|
||||||
|
{
|
||||||
|
return m_duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MpvItem::paused() const
|
||||||
|
{
|
||||||
|
return m_paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MpvItem::setPaused(bool paused)
|
||||||
|
{
|
||||||
|
if (paused == m_paused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// m_paused is updated when mpv reports the "pause" property back.
|
||||||
|
setPropertyAsync(QStringLiteral("pause"), paused);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MpvItem::isFileLoaded() const
|
||||||
|
{
|
||||||
|
return m_fileLoaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MpvItem::loadFile(const QUrl &url)
|
||||||
|
{
|
||||||
|
if (!url.isValid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_currentUrl = url;
|
||||||
|
Q_EMIT currentUrlChanged();
|
||||||
|
|
||||||
|
if (!m_ready) {
|
||||||
|
m_pendingUrl = url;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString target = url.isLocalFile() ? url.toLocalFile() : url.toString();
|
||||||
|
commandAsync(QStringList{QStringLiteral("loadfile"), target});
|
||||||
|
setPropertyAsync(QStringLiteral("pause"), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MpvItem::togglePause()
|
||||||
|
{
|
||||||
|
setPaused(!m_paused);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MpvItem::seek(double seconds)
|
||||||
|
{
|
||||||
|
commandAsync(QStringList{QStringLiteral("seek"), QString::number(seconds), QStringLiteral("absolute")});
|
||||||
|
}
|
||||||
|
|
||||||
|
void MpvItem::onPropertyChanged(const QString &property, const QVariant &value)
|
||||||
|
{
|
||||||
|
if (property == u"media-title") {
|
||||||
|
m_mediaTitle = value.toString();
|
||||||
|
Q_EMIT mediaTitleChanged();
|
||||||
|
} else if (property == u"time-pos") {
|
||||||
|
m_position = value.toDouble();
|
||||||
|
Q_EMIT positionChanged();
|
||||||
|
} else if (property == u"duration") {
|
||||||
|
m_duration = value.toDouble();
|
||||||
|
Q_EMIT durationChanged();
|
||||||
|
} else if (property == u"pause") {
|
||||||
|
m_paused = value.toBool();
|
||||||
|
Q_EMIT pausedChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MpvItem::setFileLoaded(bool loaded)
|
||||||
|
{
|
||||||
|
if (loaded == m_fileLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_fileLoaded = loaded;
|
||||||
|
Q_EMIT fileLoadedChanged();
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <MpvAbstractItem>
|
||||||
|
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QtQml/qqmlregistration.h>
|
||||||
|
|
||||||
|
// QML video surface backed by libmpv (through MpvQt).
|
||||||
|
// mpv properties we care about are mirrored as Qt properties so QML can bind to them.
|
||||||
|
class MpvItem : public MpvAbstractItem
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
|
||||||
|
Q_PROPERTY(QUrl currentUrl READ currentUrl NOTIFY currentUrlChanged)
|
||||||
|
Q_PROPERTY(QString mediaTitle READ mediaTitle NOTIFY mediaTitleChanged)
|
||||||
|
Q_PROPERTY(double position READ position NOTIFY positionChanged)
|
||||||
|
Q_PROPERTY(double duration READ duration NOTIFY durationChanged)
|
||||||
|
Q_PROPERTY(bool paused READ paused WRITE setPaused NOTIFY pausedChanged)
|
||||||
|
Q_PROPERTY(bool fileLoaded READ isFileLoaded NOTIFY fileLoadedChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit MpvItem(QQuickItem *parent = nullptr);
|
||||||
|
~MpvItem() override = default;
|
||||||
|
|
||||||
|
QUrl currentUrl() const;
|
||||||
|
QString mediaTitle() const;
|
||||||
|
double position() const;
|
||||||
|
double duration() const;
|
||||||
|
bool paused() const;
|
||||||
|
void setPaused(bool paused);
|
||||||
|
bool isFileLoaded() const;
|
||||||
|
|
||||||
|
Q_INVOKABLE void loadFile(const QUrl &url);
|
||||||
|
Q_INVOKABLE void togglePause();
|
||||||
|
Q_INVOKABLE void seek(double seconds);
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void currentUrlChanged();
|
||||||
|
void mediaTitleChanged();
|
||||||
|
void positionChanged();
|
||||||
|
void durationChanged();
|
||||||
|
void pausedChanged();
|
||||||
|
void fileLoadedChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void onPropertyChanged(const QString &property, const QVariant &value);
|
||||||
|
void setFileLoaded(bool loaded);
|
||||||
|
|
||||||
|
bool m_ready = false;
|
||||||
|
QUrl m_pendingUrl;
|
||||||
|
QUrl m_currentUrl;
|
||||||
|
QString m_mediaTitle;
|
||||||
|
double m_position = 0.0;
|
||||||
|
double m_duration = 0.0;
|
||||||
|
bool m_paused = false;
|
||||||
|
bool m_fileLoaded = false;
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import QtQuick
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
import org.kde.kirigamiaddons.formcard as FormCard
|
||||||
|
|
||||||
|
Kirigami.ApplicationWindow {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// Set from C++ (main.cpp) when a file is passed on the command line.
|
||||||
|
required property url initialUrl
|
||||||
|
|
||||||
|
width: Kirigami.Units.gridUnit * 50
|
||||||
|
height: Kirigami.Units.gridUnit * 32
|
||||||
|
minimumWidth: Kirigami.Units.gridUnit * 20
|
||||||
|
minimumHeight: Kirigami.Units.gridUnit * 14
|
||||||
|
|
||||||
|
title: player.mediaTitle.length > 0 ? player.mediaTitle : "QtPotato"
|
||||||
|
|
||||||
|
pageStack.initialPage: PlayerPage {
|
||||||
|
id: player
|
||||||
|
initialUrl: root.initialUrl
|
||||||
|
onAboutRequested: root.pageStack.layers.push(aboutPage)
|
||||||
|
onFullScreenToggled: root.visibility = root.visibility === Window.FullScreen ? Window.Windowed : Window.FullScreen
|
||||||
|
}
|
||||||
|
|
||||||
|
pageStack.globalToolBar.style: Kirigami.ApplicationHeaderStyle.ToolBar
|
||||||
|
pageStack.columnView.columnResizeMode: Kirigami.ColumnView.SingleColumn
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: aboutPage
|
||||||
|
FormCard.AboutPage {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls as QQC2
|
||||||
|
import QtQuick.Dialogs
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
import org.kde.ki18n
|
||||||
|
|
||||||
|
Kirigami.Page {
|
||||||
|
id: page
|
||||||
|
|
||||||
|
property url initialUrl
|
||||||
|
readonly property alias mediaTitle: mpv.mediaTitle
|
||||||
|
|
||||||
|
signal aboutRequested()
|
||||||
|
signal fullScreenToggled()
|
||||||
|
|
||||||
|
title: KI18n.i18nc("@title", "Player")
|
||||||
|
padding: 0
|
||||||
|
|
||||||
|
actions: [
|
||||||
|
Kirigami.Action {
|
||||||
|
icon.name: "document-open"
|
||||||
|
text: KI18n.i18nc("@action", "Open File…")
|
||||||
|
shortcut: StandardKey.Open
|
||||||
|
onTriggered: fileDialog.open()
|
||||||
|
},
|
||||||
|
Kirigami.Action {
|
||||||
|
icon.name: "view-fullscreen"
|
||||||
|
text: KI18n.i18nc("@action", "Full Screen")
|
||||||
|
displayHint: Kirigami.DisplayHint.IconOnly
|
||||||
|
shortcut: "F"
|
||||||
|
onTriggered: page.fullScreenToggled()
|
||||||
|
},
|
||||||
|
Kirigami.Action {
|
||||||
|
icon.name: "help-about"
|
||||||
|
text: KI18n.i18nc("@action", "About QtPotato")
|
||||||
|
displayHint: Kirigami.DisplayHint.AlwaysHide
|
||||||
|
onTriggered: page.aboutRequested()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (page.initialUrl.toString().length > 0) {
|
||||||
|
mpv.loadFile(page.initialUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(totalSeconds: real): string {
|
||||||
|
const s = Math.max(0, Math.floor(totalSeconds))
|
||||||
|
const h = Math.floor(s / 3600)
|
||||||
|
const m = Math.floor((s % 3600) / 60)
|
||||||
|
const sec = String(s % 60).padStart(2, "0")
|
||||||
|
return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${sec}` : `${m}:${sec}`
|
||||||
|
}
|
||||||
|
|
||||||
|
Shortcut {
|
||||||
|
sequence: "Space"
|
||||||
|
onActivated: mpv.togglePause()
|
||||||
|
}
|
||||||
|
Shortcut {
|
||||||
|
sequence: "Left"
|
||||||
|
onActivated: mpv.seek(mpv.position - 5)
|
||||||
|
}
|
||||||
|
Shortcut {
|
||||||
|
sequence: "Right"
|
||||||
|
onActivated: mpv.seek(mpv.position + 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
FileDialog {
|
||||||
|
id: fileDialog
|
||||||
|
title: KI18n.i18nc("@title:window", "Open Video")
|
||||||
|
nameFilters: [
|
||||||
|
KI18n.i18nc("@item:inlistbox file filter", "Video files (*.mkv *.mp4 *.webm *.avi *.mov *.m4v *.ts *.flv *.wmv)"),
|
||||||
|
KI18n.i18nc("@item:inlistbox file filter", "All files (*)")
|
||||||
|
]
|
||||||
|
onAccepted: mpv.loadFile(selectedFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
MpvItem {
|
||||||
|
id: mpv
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "black"
|
||||||
|
visible: !mpv.fileLoaded
|
||||||
|
z: -1
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.PlaceholderMessage {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - Kirigami.Units.gridUnit * 4
|
||||||
|
visible: !mpv.fileLoaded
|
||||||
|
icon.name: "video-symbolic"
|
||||||
|
text: KI18n.i18nc("@info:placeholder", "No video open")
|
||||||
|
explanation: KI18n.i18nc("@info:placeholder", "Open a file or drop one here")
|
||||||
|
helpfulAction: page.actions[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: mpv.togglePause()
|
||||||
|
onDoubleClicked: page.fullScreenToggled()
|
||||||
|
}
|
||||||
|
|
||||||
|
DropArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onDropped: drop => {
|
||||||
|
if (drop.hasUrls) {
|
||||||
|
mpv.loadFile(drop.urls[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.ToolBar {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
position: QQC2.ToolBar.Footer
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
QQC2.ToolButton {
|
||||||
|
enabled: mpv.fileLoaded
|
||||||
|
focusPolicy: Qt.NoFocus
|
||||||
|
icon.name: mpv.paused || !mpv.fileLoaded ? "media-playback-start" : "media-playback-pause"
|
||||||
|
text: mpv.paused || !mpv.fileLoaded ? KI18n.i18nc("@action:button", "Play") : KI18n.i18nc("@action:button", "Pause")
|
||||||
|
display: QQC2.AbstractButton.IconOnly
|
||||||
|
onClicked: mpv.togglePause()
|
||||||
|
|
||||||
|
QQC2.ToolTip.text: text
|
||||||
|
QQC2.ToolTip.visible: hovered
|
||||||
|
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
text: page.formatTime(seekBar.pressed ? seekBar.value : mpv.position)
|
||||||
|
font.features: { "tnum": 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Slider {
|
||||||
|
id: seekBar
|
||||||
|
Layout.fillWidth: true
|
||||||
|
enabled: mpv.fileLoaded && mpv.duration > 0
|
||||||
|
focusPolicy: Qt.NoFocus
|
||||||
|
from: 0
|
||||||
|
to: Math.max(mpv.duration, 1)
|
||||||
|
onMoved: mpv.seek(value)
|
||||||
|
|
||||||
|
// Follow playback, but not while the user is dragging the handle.
|
||||||
|
Binding on value {
|
||||||
|
value: mpv.position
|
||||||
|
when: !seekBar.pressed
|
||||||
|
restoreMode: Binding.RestoreNone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC2.Label {
|
||||||
|
text: page.formatTime(mpv.duration)
|
||||||
|
font.features: { "tnum": 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user