// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause pragma ComponentBehavior: Bound import QtQuick import QtQuick.Controls.Fusion import QtQuick.Dialogs import QtQuick.Layouts import QtCore import MediaControls import Config import io.qt.filenameprovider Rectangle { id: root implicitWidth: 380 color: Config.mainColor border.color: "lightgrey" radius: 10 property int currentIndex: -1 property bool isShuffled: false property alias mediaCount: files.count signal playlistUpdated() signal currentFileRemoved() function getSource() { if (isShuffled && mediaCount > 1) { let randomIndex = Math.floor(Math.random() * mediaCount) while (randomIndex == currentIndex) { randomIndex = Math.floor(Math.random() * mediaCount) } currentIndex = randomIndex } return files.get(currentIndex).path } function addFiles(index, selectedFiles) { selectedFiles.forEach(function (file){ const url = new URL(file) files.insert(index, { path: url, isMovie: isMovie(FileNameProvider.getFileName(url.toString())) }) }) playlistUpdated() } function addFile(index, selectedFile) { if (index > mediaCount || index < 0) { index = 0 currentIndex = 0 } files.insert(index, { path: selectedFile, isMovie: isMovie(FileNameProvider.getFileName(selectedFile.toString())) }) } function isMovie(path) { const paths = path.split('.') const extension = paths[paths.length - 1] const musicFormats = ["mp3", "wav", "aac"] for (const format of musicFormats) { if (format === extension) { return false } } return true } MouseArea { anchors.fill: root preventStealing: true } FileDialog { id: folderView title: qsTr("Add files to playlist") currentFolder: StandardPaths.standardLocations(StandardPaths.MoviesLocation)[0] fileMode: FileDialog.OpenFiles onAccepted: { root.addFiles(files.count, folderView.selectedFiles) close() } } ListModel { id: files } Item { id: playlist anchors.fill: root anchors.margins: 30 RowLayout { id: header width: playlist.width Label { font.bold: true font.pixelSize: 20 text: qsTr("Playlist") color: Config.secondaryColor Layout.fillWidth: true } CustomButton { icon.source: ControlImages.iconSource("Add_file") onClicked: folderView.open() } } ListView { id: listView model: files anchors.fill: playlist anchors.topMargin: header.height + 30 spacing: 20 delegate: RowLayout { id: row width: listView.width spacing: 15 required property string path required property int index required property bool isMovie Image { id: mediaIcon states: [ State { name: "activeMovie" when: root.currentIndex === row.index && row.isMovie PropertyChanges { mediaIcon.source: Images.iconSource("Movie_Active", false) } }, State { name: "inactiveMovie" when: root.currentIndex !== row.index && row.isMovie PropertyChanges { mediaIcon.source: Images.iconSource("Movie_Icon") } }, State { name: "activeMusic" when: root.currentIndex === row.index && !row.isMovie PropertyChanges { mediaIcon.source: Images.iconSource("Music_Active", false) } }, State { name: "inactiveMusic" when: root.currentIndex !== row.index && !row.isMovie PropertyChanges { mediaIcon.source: Images.iconSource("Music_Icon") } } ] } Label { Layout.fillWidth: true elide: Text.ElideRight font.bold: root.currentIndex === row.index color: root.currentIndex === row.index ? Config.highlightColor : Config.secondaryColor font.pixelSize: 18 text: { return FileNameProvider.getFileName(row.path) } } CustomButton { icon.source: ControlImages.iconSource("Trash_Icon") onClicked: { const removedIndex = row.index files.remove(row.index) if (root.currentIndex === removedIndex) { root.currentFileRemoved() } else if (root.currentIndex > removedIndex) { --root.currentIndex } } } } remove: Transition { NumberAnimation { property: "opacity" from: 1.0 to: 0.0 duration: 400 } } add: Transition { NumberAnimation { property: "opacity" from: 0.0 to: 1.0 duration: 400 } NumberAnimation { property: "scale" from: 0.5 to: 1.0 duration: 400 } } displaced: Transition { NumberAnimation { properties: "y" duration: 600 easing.type: Easing.OutBounce } } } } }