// SPDX-FileCopyrightText: 2020 George Florea Bănuș // SPDX-FileCopyrightText: 2026 Tri Vo Khac // SPDX-License-Identifier: GPL-3.0-or-later import QtQuick import QtQuick.Controls import org.kde.ki18n import org.kde.kirigami as Kirigami // The video, with Haruna's default mouse bindings and drag and drop. MpvItem { id: root required property Actions actions property bool fullscreen: false signal pointerMoved(x: real, y: real) signal pointerLeft() // Files dropped on the top part are queued; on the bottom part they play now. signal filesDropped(urls: list, append: bool) signal subtitleDropped(url: url) function isSubtitleFile(url: url): bool { return /\.(srt|ass|ssa|vtt|sub|sup)$/i.test(url.toString()) } // Plain-text and translated subtitles; styled ASS subtitles keep their own fonts. function applySubtitleStyle() { setPropertyAsync("sub-font-size", QtPotatoSettings.subtitleFontSize) setPropertyAsync("sub-outline-size", QtPotatoSettings.subtitleOutlineSize) } Kirigami.Theme.colorSet: Kirigami.Theme.Complementary Kirigami.Theme.inherit: false loadUserConfig: QtPotatoSettings.loadMpvConfig preferredAudioLanguages: QtPotatoSettings.preferredAudioLanguages preferredSubtitleLanguages: QtPotatoSettings.preferredSubtitleLanguages autoSelectSubtitle: QtPotatoSettings.autoSelectSubtitle // Set once the saved volume is applied; mpv reports its default volume before that. property bool volumeRestored: false // QtPotato's own settings win over mpv.conf. onUserConfigApplied: { applySubtitleStyle() volume = QtPotatoSettings.volume volumeRestored = true } onVolumeChanged: { if (volumeRestored) { QtPotatoSettings.volume = volume } } readonly property int subtitleFontSize: QtPotatoSettings.subtitleFontSize onSubtitleFontSizeChanged: applySubtitleStyle() readonly property int subtitleOutlineSize: QtPotatoSettings.subtitleOutlineSize onSubtitleOutlineSizeChanged: applySubtitleStyle() Rectangle { anchors.fill: parent color: Kirigami.Theme.backgroundColor visible: !root.fileLoaded z: -1 } Kirigami.PlaceholderMessage { anchors.centerIn: parent width: parent.width - Kirigami.Units.gridUnit * 4 visible: !root.fileLoaded && !dropArea.containsDrag icon.name: "video-symbolic" text: KI18n.i18nc("@info:placeholder", "No video open") explanation: KI18n.i18nc("@info:placeholder", "Open a file or drop one here") } Timer { id: hideCursorTimer running: root.fullscreen && mouseArea.containsMouse repeat: true interval: 2000 onTriggered: mouseArea.hideCursor = true } MouseArea { id: mouseArea property bool hideCursor: false anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton | Qt.ForwardButton | Qt.BackButton hoverEnabled: true cursorShape: hideCursor && root.fullscreen ? Qt.BlankCursor : Qt.ArrowCursor onPositionChanged: mouse => { hideCursor = false hideCursorTimer.restart() root.pointerMoved(mouse.x, mouse.y) } onExited: root.pointerLeft() onPressed: mouse => { root.forceActiveFocus(Qt.MouseFocusReason) switch (mouse.button) { case Qt.LeftButton: root.actions.playPause.trigger() break case Qt.MiddleButton: root.actions.mute.trigger() break case Qt.RightButton: root.actions.openContextMenu.trigger() break case Qt.ForwardButton: root.actions.seekForwardSmall.trigger() break case Qt.BackButton: root.actions.seekBackwardSmall.trigger() break } } onDoubleClicked: mouse => { if (mouse.button === Qt.LeftButton) { // Both presses of the double click toggled playback, so it is unchanged. root.actions.toggleFullscreen.trigger() } else if (mouse.button === Qt.MiddleButton) { root.actions.configure.trigger() } } onWheel: wheel => { if (wheel.angleDelta.y > 0) { root.actions.volumeUp.trigger() } else if (wheel.angleDelta.y < 0) { root.actions.volumeDown.trigger() } } } DropArea { id: dropArea readonly property bool onTopPart: containsDrag && drag.y <= height * 0.25 property bool onlySubtitles: false anchors.fill: parent keys: ["text/uri-list"] onEntered: drag => { onlySubtitles = drag.urls.length > 0 && drag.urls.every(url => root.isSubtitleFile(url)) } onDropped: drop => { if (!drop.hasUrls) { return } if (onlySubtitles) { root.subtitleDropped(drop.urls[0]) } else { root.filesDropped(drop.urls, onTopPart) } } Repeater { model: dropArea.containsDrag && !dropArea.onlySubtitles ? 2 : 0 delegate: Rectangle { id: dropZone required property int index readonly property bool isTop: index === 0 readonly property bool active: isTop === dropArea.onTopPart x: Kirigami.Units.smallSpacing y: isTop ? Kirigami.Units.smallSpacing : dropArea.height * 0.25 + Kirigami.Units.smallSpacing width: dropArea.width - Kirigami.Units.smallSpacing * 2 height: (isTop ? dropArea.height * 0.25 : dropArea.height * 0.75) - Kirigami.Units.smallSpacing * 2 color: "transparent" radius: Kirigami.Units.cornerRadius border.color: active ? Kirigami.Theme.activeTextColor : "transparent" border.width: active ? 3 : 0 Label { anchors.fill: parent text: dropZone.isTop ? KI18n.i18nc("@info:drag&drop append items", "Add to Playlist") : KI18n.i18nc("@info:drag&drop play now", "Play Now") elide: Text.ElideRight color: "white" style: Text.Outline styleColor: "black" opacity: dropZone.active ? 1 : 0.7 font.pointSize: Kirigami.Theme.defaultFont.pointSize * (dropZone.isTop ? 1.6 : 2.2) font.bold: true horizontalAlignment: Qt.AlignHCenter verticalAlignment: Qt.AlignVCenter } } } } }