mirror of https://code.videolan.org/videolan/vlc
540 lines
17 KiB
QML
540 lines
17 KiB
QML
/*****************************************************************************
|
|
* Copyright (C) 2019 VLC authors and VideoLAN
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* ( at your option ) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
|
*****************************************************************************/
|
|
import QtQuick 2.12
|
|
import QtQuick.Controls 2.12
|
|
import QtQuick.Templates 2.12 as T
|
|
import QtQuick.Layouts 1.12
|
|
import QtQml.Models 2.12
|
|
|
|
import org.videolan.vlc 0.1
|
|
import org.videolan.compat 0.1
|
|
|
|
import "qrc:///widgets/" as Widgets
|
|
import "qrc:///util" as Util
|
|
import "qrc:///util/Helpers.js" as Helpers
|
|
import "qrc:///style/"
|
|
|
|
T.Pane {
|
|
id: root
|
|
|
|
property var model: PlaylistListModel {
|
|
playlist: MainPlaylistController.playlist
|
|
}
|
|
readonly property ListSelectionModel selectionModel: listView ? listView.selectionModel : null
|
|
|
|
property bool useAcrylic: true
|
|
|
|
readonly property real minimumWidth: contentItem.Layout.minimumWidth +
|
|
leftPadding +
|
|
rightPadding +
|
|
2 * (VLCStyle.margin_xsmall)
|
|
|
|
readonly property ListView listView: contentItem.listView
|
|
|
|
property alias contextMenu: contextMenu
|
|
|
|
property alias dragItem: dragItem
|
|
|
|
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
|
|
contentWidth + leftPadding + rightPadding)
|
|
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
|
|
contentHeight + topPadding + bottomPadding)
|
|
|
|
verticalPadding: VLCStyle.margin_normal
|
|
|
|
Accessible.name: I18n.qtr("Playqueue")
|
|
|
|
readonly property ColorContext colorContext: ColorContext {
|
|
id: theme
|
|
colorSet: ColorContext.View
|
|
|
|
focused: root.activeFocus
|
|
hovered: root.hovered
|
|
enabled: root.enabled
|
|
}
|
|
|
|
function isDropAcceptable(drop, index) {
|
|
if (drop.source === dragItem)
|
|
return Helpers.itemsMovable(selectionModel.sortedSelectedIndexesFlat, index)
|
|
else if (Helpers.isValidInstanceOf(drop.source, Widgets.DragItem))
|
|
return true
|
|
else if (drop.hasUrls)
|
|
return true
|
|
else
|
|
return false
|
|
}
|
|
|
|
function acceptDrop(index, drop) {
|
|
const item = drop.source;
|
|
|
|
// NOTE: Move implementation.
|
|
if (dragItem === item) {
|
|
model.moveItemsPre(root.selectionModel.sortedSelectedIndexesFlat, index);
|
|
|
|
// NOTE: Dropping medialibrary content into the queue.
|
|
} else if (Helpers.isValidInstanceOf(item, Widgets.DragItem)) {
|
|
|
|
item.getSelectedInputItem()
|
|
.then((inputItems) => {
|
|
if (!Array.isArray(inputItems) || inputItems.length === 0) {
|
|
console.warn("can't convert items to input items");
|
|
return
|
|
}
|
|
MainPlaylistController.insert(index, inputItems, false)
|
|
})
|
|
|
|
// NOTE: Dropping an external item (i.e. filesystem) into the queue.
|
|
} else if (drop.hasUrls) {
|
|
const urlList = [];
|
|
|
|
for (let url in drop.urls)
|
|
urlList.push(drop.urls[url]);
|
|
|
|
MainPlaylistController.insert(index, urlList, false);
|
|
|
|
// NOTE This is required otherwise backend may handle the drop as well yielding double addition.
|
|
drop.accept(Qt.IgnoreAction);
|
|
}
|
|
|
|
listView.forceActiveFocus();
|
|
}
|
|
|
|
Widgets.DragItem {
|
|
id: dragItem
|
|
|
|
onRequestData: (indexes, resolve, reject) => {
|
|
resolve(indexes.map((index) => {
|
|
const item = root.model.itemAt(index)
|
|
return {
|
|
"title": item.title,
|
|
"cover": (!!item.artwork && item.artwork.toString() !== "") ? item.artwork : VLCStyle.noArtAlbumCover
|
|
}
|
|
}))
|
|
}
|
|
|
|
onRequestInputItems: (indexes, data, resolve, reject) => {
|
|
resolve(root.model.getItemsForIndexes(root.selectionModel.selectedIndexesFlat))
|
|
}
|
|
}
|
|
|
|
PlaylistContextMenu {
|
|
id: contextMenu
|
|
model: root.model
|
|
selectionModel: root.selectionModel
|
|
controler: MainPlaylistController
|
|
|
|
onJumpToCurrentPlaying: listView.positionViewAtIndex( MainPlaylistController.currentIndex, ItemView.Center)
|
|
}
|
|
|
|
background: Widgets.AcrylicBackground {
|
|
enabled: root.useAcrylic
|
|
tintColor: theme.bg.primary
|
|
}
|
|
|
|
contentItem: ColumnLayout {
|
|
spacing: VLCStyle.margin_xxsmall
|
|
|
|
Layout.minimumWidth: noContentInfoColumn.implicitWidth
|
|
|
|
readonly property ListView listView: listView
|
|
|
|
Column {
|
|
Layout.fillHeight: false
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: VLCStyle.margin_normal
|
|
|
|
spacing: VLCStyle.margin_xxxsmall
|
|
|
|
Widgets.SubtitleLabel {
|
|
text: I18n.qtr("Playqueue")
|
|
color: theme.fg.primary
|
|
font.weight: Font.Bold
|
|
font.pixelSize: VLCStyle.dp(24, VLCStyle.scale)
|
|
}
|
|
|
|
Widgets.CaptionLabel {
|
|
color: theme.fg.secondary
|
|
visible: model.count !== 0
|
|
text: I18n.qtr("%1 elements, %2").arg(model.count).arg(model.duration.formatLong())
|
|
}
|
|
}
|
|
|
|
Item {
|
|
// Spacer
|
|
|
|
implicitHeight: VLCStyle.margin_xsmall
|
|
}
|
|
|
|
RowLayout {
|
|
visible: model.count !== 0
|
|
|
|
Layout.fillHeight: false
|
|
Layout.leftMargin: VLCStyle.margin_normal
|
|
Layout.rightMargin: Math.max(listView.ScrollBar.vertical.width, VLCStyle.margin_normal)
|
|
|
|
spacing: VLCStyle.margin_large
|
|
|
|
Widgets.IconLabel {
|
|
// playlist cover column
|
|
Layout.preferredWidth: VLCStyle.icon_playlistArt
|
|
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
text: VLCIcons.album_cover
|
|
font.pixelSize: VLCStyle.icon_playlistHeader
|
|
|
|
color: theme.fg.secondary
|
|
|
|
Accessible.role: Accessible.ColumnHeader
|
|
Accessible.name: I18n.qtr("Cover")
|
|
Accessible.ignored: false
|
|
}
|
|
|
|
//Use Text here as we're redefining its Accessible.role
|
|
Text {
|
|
Layout.fillWidth: true
|
|
|
|
elide: Text.ElideRight
|
|
font.pixelSize: VLCStyle.fontSize_normal
|
|
textFormat: Text.PlainText
|
|
|
|
verticalAlignment: Text.AlignVCenter
|
|
text: I18n.qtr("Title")
|
|
color: theme.fg.secondary
|
|
|
|
Accessible.role: Accessible.ColumnHeader
|
|
Accessible.name: text
|
|
}
|
|
|
|
Widgets.IconLabel {
|
|
Layout.preferredWidth: durationMetric.width
|
|
|
|
text: VLCIcons.time
|
|
color: theme.fg.secondary
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
font.pixelSize: VLCStyle.icon_playlistHeader
|
|
|
|
Accessible.role: Accessible.ColumnHeader
|
|
Accessible.name: I18n.qtr("Duration")
|
|
Accessible.ignored: false
|
|
|
|
TextMetrics {
|
|
id: durationMetric
|
|
|
|
font.weight: Font.DemiBold
|
|
font.pixelSize: VLCStyle.fontSize_normal
|
|
text: "00:00"
|
|
}
|
|
}
|
|
}
|
|
|
|
Widgets.KeyNavigableListView {
|
|
id: listView
|
|
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
|
|
focus: true
|
|
|
|
clip: true // else out of view items will overlap with surronding items
|
|
|
|
model: root.model
|
|
|
|
BindingCompat on fadingEdge.enableBeginningFade {
|
|
when: (autoScroller.scrollingDirection === Util.ViewDragAutoScrollHandler.Direction.Backward)
|
|
value: false
|
|
}
|
|
|
|
BindingCompat on fadingEdge.enableEndFade {
|
|
when: (autoScroller.scrollingDirection === Util.ViewDragAutoScrollHandler.Direction.Forward)
|
|
value: false
|
|
}
|
|
|
|
fadingEdge.backgroundColor: root.background.usingAcrylic ? "transparent"
|
|
: listView.colorContext.bg.primary
|
|
|
|
contentWidth: width
|
|
|
|
property int shiftIndex: -1
|
|
property Item itemContainsDrag: null
|
|
|
|
onShowContextMenu: (globalPos) => {
|
|
contextMenu.popup(-1, globalPos)
|
|
}
|
|
|
|
Connections {
|
|
target: listView.model
|
|
|
|
onRowsInserted: {
|
|
if (listView.currentIndex === -1)
|
|
listView.currentIndex = 0
|
|
}
|
|
|
|
onModelReset: {
|
|
if (listView.currentIndex === -1 && root.model.count > 0)
|
|
listView.currentIndex = 0
|
|
}
|
|
}
|
|
|
|
Util.ViewDragAutoScrollHandler {
|
|
id: autoScroller
|
|
|
|
view: listView
|
|
dragging: !!listView.itemContainsDrag && listView.itemContainsDrag !== listView.footerItem
|
|
dragPosProvider: function () {
|
|
const source = listView.itemContainsDrag
|
|
const point = source.drag
|
|
return listView.mapFromItem(source, point.x, point.y)
|
|
}
|
|
}
|
|
|
|
footer: Item {
|
|
implicitWidth: parent.width
|
|
|
|
BindingCompat on implicitHeight {
|
|
delayed: true
|
|
value: Math.max(VLCStyle.icon_normal, listView.height - y)
|
|
}
|
|
|
|
property alias firstItemIndicatorVisible: firstItemIndicator.visible
|
|
|
|
readonly property bool containsDrag: dropArea.containsDrag
|
|
|
|
readonly property point drag: Qt.point(dropArea.drag.x, dropArea.drag.y)
|
|
|
|
onContainsDragChanged: {
|
|
if (root.model.count > 0) {
|
|
listView.updateItemContainsDrag(this, containsDrag)
|
|
} else if (!containsDrag && listView.itemContainsDrag === this) {
|
|
// In case model count is changed somehow while
|
|
// containsDrag is set
|
|
listView.updateItemContainsDrag(this, false)
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: firstItemIndicator
|
|
|
|
anchors.fill: parent
|
|
anchors.margins: VLCStyle.margin_small
|
|
|
|
border.width: VLCStyle.dp(2)
|
|
border.color: theme.accent
|
|
|
|
color: "transparent"
|
|
|
|
visible: (root.model.count === 0 && dropArea.containsDrag)
|
|
|
|
opacity: 0.8
|
|
|
|
Widgets.IconLabel {
|
|
anchors.centerIn: parent
|
|
|
|
text: VLCIcons.add
|
|
|
|
font.pixelSize: VLCStyle.fontHeight_xxxlarge
|
|
|
|
color: theme.accent
|
|
}
|
|
}
|
|
|
|
DropArea {
|
|
id: dropArea
|
|
|
|
anchors.fill: parent
|
|
|
|
onEntered: (drag) => {
|
|
if(!root.isDropAcceptable(drag, root.model.count)) {
|
|
drag.accepted = false
|
|
return
|
|
}
|
|
}
|
|
|
|
onDropped: (drop) => {
|
|
root.acceptDrop(root.model.count, drop)
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: dropIndicator
|
|
|
|
parent: listView.itemContainsDrag
|
|
|
|
z: 99
|
|
|
|
anchors {
|
|
left: !!parent ? parent.left : undefined
|
|
right: !!parent ? parent.right : undefined
|
|
top: !!parent ? (parent.bottomContainsDrag === true ? parent.bottom : parent.top)
|
|
: undefined
|
|
}
|
|
|
|
implicitHeight: VLCStyle.dp(1)
|
|
|
|
visible: !!parent
|
|
color: theme.accent
|
|
}
|
|
|
|
function updateItemContainsDrag(item, set) {
|
|
if (set) {
|
|
// This callLater is needed because in Qt 5.15,
|
|
// an item might set itemContainsDrag, before
|
|
// the owning item releases it.
|
|
Qt.callLater(function() {
|
|
if (itemContainsDrag)
|
|
console.debug(item + " set itemContainsDrag before it was released!")
|
|
itemContainsDrag = item
|
|
})
|
|
} else {
|
|
if (itemContainsDrag !== item)
|
|
console.debug(item + " released itemContainsDrag that is not owned!")
|
|
itemContainsDrag = null
|
|
}
|
|
}
|
|
|
|
delegate: PlaylistDelegate {
|
|
id: delegate
|
|
|
|
width: listView.contentWidth
|
|
rightPadding: Math.max(listView.ScrollBar.vertical.width, VLCStyle.margin_normal)
|
|
|
|
contextMenu: root.contextMenu
|
|
|
|
dragItem: root.dragItem
|
|
|
|
isDropAcceptable: root.isDropAcceptable
|
|
acceptDrop: root.acceptDrop
|
|
|
|
onContainsDragChanged: listView.updateItemContainsDrag(this, containsDrag)
|
|
}
|
|
|
|
add: Transition {
|
|
SequentialAnimation {
|
|
PropertyAction {
|
|
// TODO: Remove this >= Qt 5.15
|
|
property: "opacity"
|
|
value: 0.0
|
|
}
|
|
|
|
OpacityAnimator {
|
|
from: 0.0 // QTBUG-66475
|
|
to: 1.0
|
|
duration: VLCStyle.duration_long
|
|
easing.type: Easing.OutSine
|
|
}
|
|
}
|
|
}
|
|
|
|
displaced: Transition {
|
|
NumberAnimation {
|
|
// TODO: Use YAnimator >= Qt 6.0 (QTBUG-66475)
|
|
property: "y"
|
|
duration: VLCStyle.duration_long
|
|
easing.type: Easing.OutSine
|
|
}
|
|
}
|
|
|
|
Keys.onDeletePressed: model.removeItems(selectionModel.selectedIndexesFlat)
|
|
|
|
Navigation.parentItem: root
|
|
|
|
onActionAtIndex: (index) => {
|
|
if (index < 0)
|
|
return
|
|
|
|
MainPlaylistController.goTo(index, true)
|
|
}
|
|
|
|
Column {
|
|
id: noContentInfoColumn
|
|
|
|
anchors.centerIn: parent
|
|
|
|
visible: false
|
|
enabled: visible
|
|
|
|
opacity: (listView.activeFocus) ? 1.0 : 0.4
|
|
|
|
BindingCompat on visible {
|
|
delayed: true
|
|
value: (listView.model.count === 0 && !listView.footerItem.firstItemIndicatorVisible)
|
|
}
|
|
|
|
Widgets.IconLabel {
|
|
id: label
|
|
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
|
|
text: VLCIcons.playlist
|
|
|
|
color: theme.fg.primary
|
|
|
|
font.pixelSize: VLCStyle.dp(48, VLCStyle.scale)
|
|
}
|
|
|
|
T.Label {
|
|
anchors.topMargin: VLCStyle.margin_xlarge
|
|
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
|
|
text: I18n.qtr("No content yet")
|
|
|
|
color: label.color
|
|
|
|
font.pixelSize: VLCStyle.fontSize_xxlarge
|
|
}
|
|
|
|
T.Label {
|
|
anchors.topMargin: VLCStyle.margin_normal
|
|
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
|
|
text: I18n.qtr("Drag & Drop some content here!")
|
|
|
|
color: label.color
|
|
|
|
font.pixelSize: VLCStyle.fontSize_large
|
|
}
|
|
}
|
|
}
|
|
|
|
PlaylistToolbar {
|
|
id: toolbar
|
|
|
|
Layout.preferredHeight: VLCStyle.heightBar_normal
|
|
Layout.fillHeight: false
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: VLCStyle.margin_normal
|
|
Layout.rightMargin: VLCStyle.margin_normal
|
|
}
|
|
}
|
|
|
|
Keys.priority: Keys.AfterItem
|
|
Keys.forwardTo: listView
|
|
Keys.onPressed: (event) => root.Navigation.defaultKeyAction(event)
|
|
}
|