diff --git a/applets/mediaframe/package/contents/ui/main.qml b/applets/mediaframe/package/contents/ui/main.qml index 999481d13..1dc8bcc65 100644 --- a/applets/mediaframe/package/contents/ui/main.qml +++ b/applets/mediaframe/package/contents/ui/main.qml @@ -1,445 +1,445 @@ /* * Copyright 2015 Lars Pontoppidan * * 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 2.010-1301, USA. */ import QtQuick 2.5 import QtQuick.Layouts 1.1 import QtQuick.Dialogs 1.2 import QtQuick.Controls 1.3 import QtQuick.Controls.Styles 1.2 import org.kde.draganddrop 2.0 as DragDrop import org.kde.plasma.plasmoid 2.0 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.kquickcontrolsaddons 2.0 import org.kde.plasma.private.mediaframe 2.0 Item { id: main MediaFrame { id: items random: plasmoid.configuration.randomize } Plasmoid.preferredRepresentation: plasmoid.fullRepresentation Plasmoid.switchWidth: units.gridUnit * 5 Plasmoid.switchHeight: units.gridUnit * 5 Plasmoid.backgroundHints: plasmoid.configuration.useBackground ? PlasmaCore.Types.DefaultBackground : PlasmaCore.Types.NoBackground width: units.gridUnit * 20 height: units.gridUnit * 13 property string activeSource: "" property string transitionSource: "" readonly property bool pause: overlayMouseArea.containsMouse readonly property int itemCount: (items.count + items.futureLength()) readonly property bool hasItems: ((itemCount > 0) || (items.futureLength() > 0)) readonly property bool isTransitioning: faderAnimation.running onActiveSourceChanged: { items.watch(activeSource) } onHasItemsChanged: { if(hasItems) { if(activeSource == "") nextItem() } } function loadPathList() { var list = plasmoid.configuration.pathList items.clear() for(var i in list) { var item = JSON.parse(list[i]) items.add(item.path,true) } } Component.onCompleted: { loadPathList() if (items.random) nextItem() } Connections { target: plasmoid.configuration onPathListChanged: loadPathList() } function addItem(item) { if(items.isAdded(item.path)) { console.info(item.path,"already exists. Skipping...") return } // work-around for QTBUG-67773: // C++ object property of type QVariant(QStringList) is not updated on changes from QML // so explicitly create a deep JSValue copy, modify that and then set it back to overwrite the old var updatedList = plasmoid.configuration.pathList.slice(); updatedList.push(JSON.stringify(item)); plasmoid.configuration.pathList = updatedList; } function nextItem() { if(!hasItems) { console.warn("No items available") return } var active = activeSource // Only record history if we have more than one item if(itemCount > 1) items.pushHistory(active) if(items.futureLength() > 0) { setActiveSource(items.popFuture()) } else { //setLoading() items.get(function(filePath){ setActiveSource(filePath) //unsetLoading() },function(errorMessage){ //unsetLoading() console.error("Error while getting next image",errorMessage) }) } } function previousItem() { var active = activeSource items.pushFuture(active) var filePath = items.popHistory() setActiveSource(filePath) } Connections { target: items onItemChanged: { console.log("item",path,"changed") activeSource = "" setActiveSource(path) } } Timer { id: nextTimer interval: (plasmoid.configuration.interval*1000) repeat: true running: hasItems && !pause onTriggered: nextItem() } Item { id: itemView anchors.fill: parent /* Video { id: video width : 800 height : 600 source: "" onStatusChanged: { if(status == Video.Loaded) video.play() } } */ Item { id: imageView visible: hasItems anchors.fill: parent Image { id: bufferImage anchors.fill: parent fillMode: plasmoid.configuration.fillMode opacity: 0 cache: false source: transitionSource asynchronous: true autoTransform: true } Image { id: frontImage anchors.fill: parent fillMode: plasmoid.configuration.fillMode cache: false source: activeSource asynchronous: true autoTransform: true MouseArea { anchors.fill: parent onClicked: Qt.openUrlExternally(activeSource) enabled: plasmoid.configuration.leftClickOpenImage } } } // BUG TODO fix the rendering of the drop shadow /* DropShadow { id: itemViewDropShadow anchors.fill: parent visible: imageView.visible && !plasmoid.configuration.useBackground radius: 8.0 samples: 16 color: "#80000000" source: frontImage } */ } function setActiveSource(source) { if(itemCount > 1) { // Only do transition if we have more that one item transitionSource = source faderAnimation.restart() } else { transitionSource = source activeSource = source } } SequentialAnimation { id: faderAnimation ParallelAnimation { OpacityAnimator { target: frontImage; from: 1; to: 0; duration: 450 } OpacityAnimator { target: bufferImage; from: 0; to: 1; duration: 450 } } ScriptAction { script: { // Copy the transitionSource var ts = transitionSource activeSource = ts frontImage.opacity = 1 transitionSource = "" bufferImage.opacity = 0 } } } DragDrop.DropArea { id: dropArea anchors.fill: parent onDrop: { var mimeData = event.mimeData if (mimeData.hasUrls) { var urls = mimeData.urls for (var i = 0, j = urls.length; i < j; ++i) { var url = urls[i] var type = items.isDir(url) ? "folder" : "file" var item = { "path":url, "type":type } addItem(item) } } event.accept(Qt.CopyAction) } } Item { id: overlay anchors.fill: parent visible: hasItems opacity: overlayMouseArea.containsMouse ? 1 : 0 Behavior on opacity { NumberAnimation {} } PlasmaComponents.Button { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - enabled: (items.historyLength() > 0) && !isTransitioning + enabled: (items.historyLength > 0) && !isTransitioning iconSource: "arrow-left" onClicked: { nextTimer.stop() previousItem() } } PlasmaComponents.Button { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter enabled: hasItems && !isTransitioning iconSource: "arrow-right" onClicked: { nextTimer.stop() nextItem() } } Row { anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter anchors.bottomMargin: units.smallSpacing /* PlasmaComponents.Button { iconSource: "documentinfo" onClicked: { } } */ PlasmaComponents.Button { //text: activeSource.split("/").pop().slice(-25) iconSource: "document-preview" onClicked: Qt.openUrlExternally(main.activeSource) //tooltip: activeSource } /* PlasmaComponents.Button { iconSource: "trash-empty" onClicked: { } } PlasmaComponents.Button { iconSource: "flag-black" onClicked: { } } */ } // BUG TODO Fix overlay so _all_ mouse events reach lower components MouseArea { id: overlayMouseArea anchors.fill: parent hoverEnabled: true propagateComposedEvents: true //onClicked: mouse.accepted = false; onPressed: mouse.accepted = false; //onReleased: mouse.accepted = false; onDoubleClicked: mouse.accepted = false; //onPositionChanged: mouse.accepted = false; //onPressAndHold: mouse.accepted = false; } } // Visualization of the count down // TODO Makes plasmashell suck CPU until the universe or the computer collapse in on itself /* Rectangle { id: progress visible: plasmoid.configuration.showCountdown && hasItems && itemCount > 1 color: "transparent" implicitWidth: units.gridUnit implicitHeight: implicitWidth Rectangle { anchors.fill: parent opacity: pause ? 0.1 : 0.5 radius: width / 2 color: "gray" Rectangle { id: innerRing anchors.fill: parent scale: 0 radius: width / 2 color: "lightblue" ScaleAnimator on scale { running: nextTimer.running loops: Animation.Infinite from: 0; to: 1; duration: nextTimer.interval } } } PlasmaCore.IconItem { id: pauseIcon visible: pause anchors.fill: parent source: "media-playback-pause" colorGroup: PlasmaCore.ColorScope.colorGroup } } */ PlasmaComponents.Button { anchors.centerIn: parent visible: !hasItems iconSource: "configure" text: i18nc("@action:button", "Configure...") onClicked: { plasmoid.action("configure").trigger(); } } Connections { target: plasmoid onExternalData: { var type = items.isDir(data) ? "folder" : "file"; var item = { "path": data, "type": type }; addItem(item); } } } diff --git a/applets/mediaframe/plugin/mediaframe.cpp b/applets/mediaframe/plugin/mediaframe.cpp index 0ee66100d..d0339ddc3 100644 --- a/applets/mediaframe/plugin/mediaframe.cpp +++ b/applets/mediaframe/plugin/mediaframe.cpp @@ -1,408 +1,419 @@ /* * Copyright 2015 Lars Pontoppidan * * 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 2.010-1301, USA. */ #include "mediaframe.h" #include #include #include #include #include #include #include #include #include #include #include #include MediaFrame::MediaFrame(QObject *parent) : QObject(parent) { qsrand(QTime::currentTime().msec()); const auto imageMimeTypeNames = QImageReader::supportedMimeTypes(); QMimeDatabase mimeDb; for (const auto& imageMimeTypeName : imageMimeTypeNames) { const auto mimeType = mimeDb.mimeTypeForName(QLatin1String(imageMimeTypeName)); m_filters << mimeType.globPatterns(); } qDebug() << "Added" << m_filters.count() << "filters"; //qDebug() << m_filters; m_next = 0; connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &MediaFrame::slotItemChanged); connect(&m_watcher, &QFileSystemWatcher::fileChanged, this, &MediaFrame::slotItemChanged); } MediaFrame::~MediaFrame() = default; int MediaFrame::count() const { return m_allFiles.count(); } bool MediaFrame::random() const { return m_random; } void MediaFrame::setRandom(bool random) { if (random != m_random) { m_random = random; emit randomChanged(); } } int MediaFrame::random(int min, int max) { if (min > max) { int temp = min; min = max; max = temp; } //qDebug() << "random" << min << "<->" << max << "=" << ((qrand()%(max-min+1))+min); return ((qrand() % (max - min + 1) ) + min); } QString MediaFrame::getCacheDirectory() { return QDir::temp().absolutePath(); } QString MediaFrame::hash(const QString &str) { return QString::fromLatin1(QCryptographicHash::hash(str.toUtf8(), QCryptographicHash::Md5).toHex()); } bool MediaFrame::isDir(const QString &path) { return QDir(path).exists(); } bool MediaFrame::isDirEmpty(const QString &path) { return (isDir(path) && QDir(path).entryInfoList(QDir::NoDotAndDotDot|QDir::AllEntries).isEmpty()); } bool MediaFrame::isFile(const QString &path) { // Check if the file exists and is not a directory return (QFileInfo::exists(path) && QFileInfo(path).isFile()); } void MediaFrame::add(const QString &path) { add(path, AddOption::NON_RECURSIVE); } void MediaFrame::add(const QString &path, AddOption option) { if(isAdded(path)) { qWarning() << "Path" << path << "already added"; return; } QUrl url = QUrl(path); QString localPath = url.toString(QUrl::PreferLocalFile); //qDebug() << "Local path" << localPath << "Path" << path; QStringList paths; QString filePath; if(isDir(localPath)) { if(!isDirEmpty(localPath)) { QDirIterator dirIterator(localPath, m_filters, QDir::Files, (option == AddOption::RECURSIVE ? QDirIterator::Subdirectories | QDirIterator::FollowSymlinks : QDirIterator::NoIteratorFlags)); while (dirIterator.hasNext()) { dirIterator.next(); filePath = dirIterator.filePath(); paths.append(filePath); m_allFiles.append(filePath); //qDebug() << "Appended" << filePath; emit countChanged(); } if(paths.count() > 0) { m_pathMap.insert(path, paths); qDebug() << "Added" << paths.count() << "files from" << path; } else { qWarning() << "No images found in directory" << path; } } else { qWarning() << "Not adding empty directory" << path; } // the pictures have to be sorted before adding them to the list, // because the QDirIterator sorts them in a different way than QDir::entryList //paths.sort(); } else if(isFile(localPath)) { paths.append(path); m_pathMap.insert(path, paths); m_allFiles.append(path); qDebug() << "Added" << paths.count() << "files from" << path; emit countChanged(); } else { if (url.isValid() && !url.isLocalFile()) { qDebug() << "Adding" << url.toString() << "as remote file"; paths.append(path); m_pathMap.insert(path, paths); m_allFiles.append(path); emit countChanged(); } else { qWarning() << "Path" << path << "is not a valid file url or directory"; } } } void MediaFrame::clear() { m_pathMap.clear(); m_allFiles.clear(); emit countChanged(); } void MediaFrame::watch(const QString &path) { QUrl url = QUrl(path); QString localPath = url.toString(QUrl::PreferLocalFile); if(isFile(localPath)) { if(!m_watchFile.isEmpty()) { //qDebug() << "Removing" << m_watchFile << "from watch list"; m_watcher.removePath(m_watchFile); } else { qDebug() << "Nothing in watch list"; } //qDebug() << "watching" << localPath << "for changes"; m_watcher.addPath(localPath); m_watchFile = localPath; } else { qWarning() << "Can't watch remote file" << path << "for changes"; } } bool MediaFrame::isAdded(const QString &path) { return (m_pathMap.contains(path)); } void MediaFrame::get(QJSValue successCallback) { get(successCallback, QJSValue::UndefinedValue); } void MediaFrame::get(QJSValue successCallback, QJSValue errorCallback) { int size = m_allFiles.count() - 1; QString path; QString errorMessage; QJSValueList args; if(size < 1) { if(size == 0) { path = m_allFiles.at(0); if(successCallback.isCallable()) { args << QJSValue(path); successCallback.call(args); } return; } else { errorMessage = QStringLiteral("No files available"); qWarning() << errorMessage; args << QJSValue(errorMessage); errorCallback.call(args); return; } } if(m_random) { path = m_allFiles.at(this->random(0, size)); } else { path = m_allFiles.at(m_next); m_next++; if(m_next > size) { qDebug() << "Resetting next count from" << m_next << "due to queue size" << size; m_next = 0; } } QUrl url = QUrl(path); if(url.isValid()) { QString localPath = url.toString(QUrl::PreferLocalFile); if (!isFile(localPath)) { m_filename = path.section(QLatin1Char('/'), -1); QString cachedFile = getCacheDirectory()+QLatin1Char('/')+hash(path)+QLatin1Char('_')+m_filename; if(isFile(cachedFile)) { // File has been cached qDebug() << path << "is cached as" << cachedFile; if(successCallback.isCallable()) { args << QJSValue(cachedFile); successCallback.call(args); } return; } m_successCallback = successCallback; m_errorCallback = errorCallback; m_filename = cachedFile; qDebug() << path << "doesn't exist locally, trying remote."; KIO::StoredTransferJob * job = KIO::storedGet( url, KIO::NoReload, KIO::HideProgressInfo); connect(job, SIGNAL(finished(KJob*)), this, SLOT(slotFinished(KJob*))); } else { if(successCallback.isCallable()) { args << QJSValue(path); successCallback.call(args); } return; } } else { errorMessage = path + QLatin1String(" is not a valid URL"); qCritical() << errorMessage; if(errorCallback.isCallable()) { args << QJSValue(errorMessage); errorCallback.call(args); } return; } } void MediaFrame::pushHistory(const QString &string) { + const int oldCount = m_history.count(); + m_history.prepend(string); // Keep a sane history size - if(m_history.length() > 50) + if (m_history.length() > 50) { m_history.removeLast(); + } + + if (oldCount != m_history.count()) { + emit historyLengthChanged(); + } } QString MediaFrame::popHistory() { - if(m_history.isEmpty()) + if (m_history.isEmpty()) { return QString(); - return m_history.takeFirst(); + } + + const QString item = m_history.takeFirst(); + emit historyLengthChanged(); + return item; } -int MediaFrame::historyLength() +int MediaFrame::historyLength() const { return m_history.length(); } void MediaFrame::pushFuture(const QString &string) { m_future.prepend(string); } QString MediaFrame::popFuture() { if(m_future.isEmpty()) return QString(); return m_future.takeFirst(); } int MediaFrame::futureLength() { return m_future.length(); } void MediaFrame::slotItemChanged(const QString &path) { emit itemChanged(path); } void MediaFrame::slotFinished(KJob *job) { QString errorMessage; QJSValueList args; if (job->error()) { errorMessage = QLatin1String("Error loading image: ") + job->errorString(); qCritical() << errorMessage; if(m_errorCallback.isCallable()) { args << QJSValue(errorMessage); m_errorCallback.call(args); } } else if (KIO::StoredTransferJob *transferJob = qobject_cast(job)) { QImage image; // TODO make proper caching calls QString path = m_filename; qDebug() << "Saving download to" << path; image.loadFromData(transferJob->data()); image.save(path); qDebug() << "Saved to" << path; if(m_successCallback.isCallable()) { args << QJSValue(path); m_successCallback.call(args); } } else { errorMessage = QStringLiteral("Unknown error occurred"); qCritical() << errorMessage; if(m_errorCallback.isCallable()) { args << QJSValue(errorMessage); m_errorCallback.call(args); } } } diff --git a/applets/mediaframe/plugin/mediaframe.h b/applets/mediaframe/plugin/mediaframe.h index 6f7ffaf5c..cff0ab997 100644 --- a/applets/mediaframe/plugin/mediaframe.h +++ b/applets/mediaframe/plugin/mediaframe.h @@ -1,109 +1,112 @@ /* * Copyright 2015 Lars Pontoppidan * * 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 2.010-1301, USA. */ #ifndef MEDIAFRAME_H #define MEDIAFRAME_H #include #include #include #include #include #include #include class MediaFrame : public QObject { Q_OBJECT Q_PROPERTY(int count READ count NOTIFY countChanged) + Q_PROPERTY(int historyLength READ historyLength NOTIFY historyLengthChanged) Q_PROPERTY(bool random READ random WRITE setRandom NOTIFY randomChanged) public: enum AddOption { NON_RECURSIVE, RECURSIVE }; Q_ENUM(AddOption) explicit MediaFrame(QObject *parent = nullptr); ~MediaFrame() override; int count() const; + int historyLength() const; + bool random() const; void setRandom(bool random); Q_INVOKABLE bool isDir(const QString &path); Q_INVOKABLE bool isDirEmpty(const QString &path); Q_INVOKABLE bool isFile(const QString &path); Q_INVOKABLE void add(const QString &path); Q_INVOKABLE void add(const QString &path, AddOption option); Q_INVOKABLE void clear(); Q_INVOKABLE void watch(const QString &path); Q_INVOKABLE bool isAdded(const QString &path); Q_INVOKABLE void get(QJSValue callback); Q_INVOKABLE void get(QJSValue callback, QJSValue error_callback); Q_INVOKABLE void pushHistory(const QString &string); Q_INVOKABLE QString popHistory(); - Q_INVOKABLE int historyLength(); Q_INVOKABLE void pushFuture(const QString &string); Q_INVOKABLE QString popFuture(); Q_INVOKABLE int futureLength(); Q_SIGNALS: void countChanged(); + void historyLengthChanged(); void randomChanged(); void itemChanged(const QString &path); private Q_SLOTS: void slotItemChanged(const QString &path); void slotFinished(KJob *job); private: int random(int min, int max); QString getCacheDirectory(); QString hash(const QString &str); QStringList m_filters; QHash m_pathMap; QStringList m_allFiles; QString m_watchFile; QFileSystemWatcher m_watcher; QStringList m_history; QStringList m_future; QJSValue m_successCallback; QJSValue m_errorCallback; QString m_filename; bool m_random = false; int m_next = 0; }; #endif