From c35c0f655089bcd32d39894276b0f8a445eec2fd Mon Sep 17 00:00:00 2001 From: Chris Cochrun Date: Tue, 15 Mar 2022 15:08:17 -0500 Subject: [PATCH] adding in a video model and editor --- .dir-locals.el | 3 +- README.org | 4 +- src/CMakeLists.txt | 2 +- src/main.cpp | 11 +- src/mpv/mpvobject.cpp | 17 +++ src/mpv/mpvobject.h | 1 + src/present.desktop | 2 +- src/qml/main.qml | 46 +++--- src/qml/presenter/ImageEditor.qml | 230 +++++++++++++++++++++++++++++ src/qml/presenter/LeftDock.qml | 1 + src/qml/presenter/Library.qml | 190 ++++++++++++++++++++++++ src/qml/presenter/MainWindow.qml | 52 ++++++- src/qml/presenter/Presentation.qml | 2 + src/qml/presenter/Slide.qml | 4 +- src/qml/presenter/VideoEditor.qml | 227 ++++++++++++++++++++++++++++ src/resources.qrc | 2 + src/songlistmodel.cpp | 70 --------- src/songlistmodel.h | 47 ------ src/songsqlmodel.cpp | 4 +- src/songsqlmodel.h | 1 - src/videosqlmodel.cpp | 167 +++++++++++++++++++++ src/videosqlmodel.h | 49 ++++++ 22 files changed, 972 insertions(+), 160 deletions(-) create mode 100644 src/qml/presenter/ImageEditor.qml create mode 100644 src/qml/presenter/VideoEditor.qml delete mode 100644 src/songlistmodel.cpp delete mode 100644 src/songlistmodel.h create mode 100644 src/videosqlmodel.cpp create mode 100644 src/videosqlmodel.h diff --git a/.dir-locals.el b/.dir-locals.el index 2fba23d..c19bc28 100644 --- a/.dir-locals.el +++ b/.dir-locals.el @@ -2,4 +2,5 @@ ;;; For more information see (info "(emacs) Directory Variables") ((nil . ((projectile-project-compilation-cmd . "cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1 -B build/ . && make --dir build/") - (projectile-project-run-cmd . "./build/bin/presenter")))) + (projectile-project-run-cmd . "./build/bin/presenter"))) + (c++-mode . ((aggressive-indent-mode . nil)))) diff --git a/README.org b/README.org index d3b8e13..d8ee0da 100644 --- a/README.org +++ b/README.org @@ -2,9 +2,9 @@ #+AUTHOR: Chris Cochrun * Church Presenter -This is an attempt at building a church presentation application in Qt/QML. QML provides a very powerful and easy declarative way of creating a UI so it should also be a very simple method of creating on screen slides and presentations. This experiment is to see how difficult it is to rebuild these applications in QML as opposed to other more complicated systems. After digging through the source code of OpenLP, I discovered they are essentially created a web server and rendering a webpage onto the screen to show slides. This felt like a waste of resources and added complexity when something so simple and useful as QML exists. +This is an attempt at building a church presentation application in Qt/QML. QML provides a very powerful and easy declarative way of creating a UI so it should also be a very simple method of creating on screen slides and presentations. This experiment is to see how difficult it is to rebuild these applications in QML as opposed to other more complicated systems. After digging through the source code of OpenLP, I discovered they are essentially creating a web server and rendering a webpage onto the screen to show slides. This felt like a waste of resources and added complexity when something so simple and useful as QML exists. -** Features (planned are in brackets) +** Features (planned are in parentheses) - Presents songs lyrics with image and video backgrounds - Presents slides - (Custom slide builder) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0048af4..a5d35c0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -3,8 +3,8 @@ add_executable(presenter) target_sources(presenter PRIVATE main.cpp resources.qrc - songlistmodel.cpp songlistmodel.h songsqlmodel.cpp songsqlmodel.h + videosqlmodel.cpp videosqlmodel.h mpv/mpvobject.h mpv/mpvobject.cpp mpv/qthelper.hpp mpv/mpvhelpers.h ) diff --git a/src/main.cpp b/src/main.cpp index d60cbd3..ed2f64c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -30,9 +30,9 @@ #include #include -#include "songlistmodel.h" #include "mpv/mpvobject.h" #include "songsqlmodel.h" +#include "videosqlmodel.h" static void connectToDatabase() { // let's setup our sql database @@ -44,6 +44,7 @@ static void connectToDatabase() { } const QDir writeDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + qDebug() << "dir location " << writeDir.absolutePath(); if (!writeDir.mkpath(".")) { qFatal("Failed to create writable location at %s", qPrintable(writeDir.absolutePath())); @@ -79,21 +80,21 @@ int main(int argc, char *argv[]) #endif QGuiApplication::setWindowIcon(QIcon::fromTheme(QStringLiteral("system-config-display"))); + // apparently mpv needs this class set + // let's register mpv as well std::setlocale(LC_NUMERIC, "C"); qmlRegisterType("mpv", 1, 0, "MpvObject"); - //register our song model from sql + //register our models qmlRegisterType("org.presenter", 1, 0, "SongSqlModel"); - - SongListModel songListModel; + qmlRegisterType("org.presenter", 1, 0, "VideoSqlModel"); connectToDatabase(); QQmlApplicationEngine engine; engine.rootContext()->setContextObject(new KLocalizedContext(&engine)); - engine.rootContext()->setContextProperty("_songListModel", &songListModel); engine.load(QUrl(QStringLiteral("qrc:qml/main.qml"))); // QQuickView *view = new QQuickView; diff --git a/src/mpv/mpvobject.cpp b/src/mpv/mpvobject.cpp index 0fc4ae3..53f5fe0 100644 --- a/src/mpv/mpvobject.cpp +++ b/src/mpv/mpvobject.cpp @@ -1,6 +1,8 @@ #include "mpvobject.h" // std +#include +#include #include #include @@ -19,6 +21,7 @@ #include #include +#include // libmpv #include @@ -28,6 +31,7 @@ #include "qthelper.hpp" +const QDir writeDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); //--- MpvRenderer void* MpvRenderer::get_proc_address(void *ctx, const char *name) { @@ -261,11 +265,13 @@ void MpvObject::doUpdate() void MpvObject::command(const QVariant& params) { + // qDebug() << params; mpv::qt::command(mpv, params); } void MpvObject::commandAsync(const QVariant& params) { + qDebug() << params; mpv::qt::command_async(mpv, params); } @@ -522,6 +528,17 @@ void MpvObject::loadFile(QVariant urls) command(QVariantList() << "loadfile" << urls); } +void MpvObject::screenshotToFile(QUrl url) { + qDebug() << "Url of screenshot to be taken: " << url; + QDir dir = writeDir.absolutePath() + "/presenter/Church Presenter/thumbnails"; + qDebug() << "thumbnails dir: " << dir; + QDir absDir = writeDir.absolutePath() + "/presenter/Church Presenter"; + if (!dir.exists()) + absDir.mkdir("thumbnails"); + QString file = url.path() + ".jpg"; + commandAsync(QVariantList() << "screenshot-to-file" << file << "video"); +} + void MpvObject::subAdd(QVariant urls) { command(QVariantList() << "sub-add" << urls); diff --git a/src/mpv/mpvobject.h b/src/mpv/mpvobject.h index f410b77..da94f96 100644 --- a/src/mpv/mpvobject.h +++ b/src/mpv/mpvobject.h @@ -135,6 +135,7 @@ public slots: void stepForward(); void seek(double pos); void loadFile(QVariant urls); + void screenshotToFile(QUrl url); void subAdd(QVariant urls); bool enableAudio() const { return m_enableAudio; } diff --git a/src/present.desktop b/src/present.desktop index f7f4a0d..0076611 100644 --- a/src/present.desktop +++ b/src/present.desktop @@ -2,7 +2,7 @@ Type=Application Name=present GenericName=Church Presentation -Comment=A Kirigami base church presenter +Comment=A Kirigami based church presenter Exec=present %U TryExec=present Icon=present diff --git a/src/qml/main.qml b/src/qml/main.qml index 35694e3..5dbc468 100644 --- a/src/qml/main.qml +++ b/src/qml/main.qml @@ -26,28 +26,28 @@ Kirigami.ApplicationWindow { pageStack.initialPage: mainPage header: Presenter.Header {} - menuBar: Controls.MenuBar { - Controls.Menu { - title: qsTr("File") - Controls.MenuItem { text: qsTr("New...") } - Controls.MenuItem { text: qsTr("Open...") } - Controls.MenuItem { text: qsTr("Save") } - Controls.MenuItem { text: qsTr("Save As...") } - Controls.MenuSeparator { } - Controls.MenuItem { text: qsTr("Quit") } - } - Controls.Menu { - title: qsTr("Settings") - Controls.MenuItem { - text: qsTr("Configure") - onTriggered: openSettings() - } - } - Controls.Menu { - title: qsTr("Help") - Controls.MenuItem { text: qsTr("About") } - } - } + /* menuBar: Qt.platform.os !== "linux" ? Controls.MenuBar { */ + /* Controls.Menu { */ + /* title: qsTr("File") */ + /* Controls.MenuItem { text: qsTr("New...") } */ + /* Controls.MenuItem { text: qsTr("Open...") } */ + /* Controls.MenuItem { text: qsTr("Save") } */ + /* Controls.MenuItem { text: qsTr("Save As...") } */ + /* Controls.MenuSeparator { } */ + /* Controls.MenuItem { text: qsTr("Quit") } */ + /* } */ + /* Controls.Menu { */ + /* title: qsTr("Settings") */ + /* Controls.MenuItem { */ + /* text: qsTr("Configure") */ + /* onTriggered: openSettings() */ + /* } */ + /* } */ + /* Controls.Menu { */ + /* title: qsTr("Help") */ + /* Controls.MenuItem { text: qsTr("About") } */ + /* } */ + /* } : null */ Labs.MenuBar { Labs.Menu { @@ -81,7 +81,7 @@ Kirigami.ApplicationWindow { function toggleEditMode() { editMode = !editMode; - mainPage.editSwitch(editMode); + mainPage.editSwitch(); } function toggleLibrary() { diff --git a/src/qml/presenter/ImageEditor.qml b/src/qml/presenter/ImageEditor.qml new file mode 100644 index 0000000..4dea612 --- /dev/null +++ b/src/qml/presenter/ImageEditor.qml @@ -0,0 +1,230 @@ +import QtQuick 2.13 +import QtQuick.Controls 2.15 as Controls +import QtQuick.Dialogs 1.3 +import QtQuick.Layouts 1.2 +import org.kde.kirigami 2.13 as Kirigami +import "./" as Presenter + +Item { + id: root + + GridLayout { + id: mainLayout + anchors.fill: parent + columns: 2 + rowSpacing: 5 + columnSpacing: 0 + + Controls.ToolBar { + Layout.fillWidth: true + Layout.columnSpan: 2 + id: toolbar + RowLayout { + anchors.fill: parent + + Controls.ComboBox { + model: Qt.fontFamilies() + implicitWidth: 300 + editable: true + hoverEnabled: true + onCurrentTextChanged: showPassiveNotification(currentText) + } + Controls.SpinBox { + editable: true + from: 5 + to: 72 + hoverEnabled: true + } + Controls.ComboBox { + model: ["IMAGES", "Center", "Right", "Justify"] + implicitWidth: 100 + hoverEnabled: true + } + Controls.ToolButton { + text: "B" + hoverEnabled: true + } + Controls.ToolButton { + text: "I" + hoverEnabled: true + } + Controls.ToolButton { + text: "U" + hoverEnabled: true + } + Controls.ToolSeparator {} + Item { Layout.fillWidth: true } + Controls.ToolSeparator {} + Controls.ToolButton { + text: "Effects" + icon.name: "image-auto-adjust" + hoverEnabled: true + onClicked: {} + } + Controls.ToolButton { + id: backgroundButton + text: "Background" + icon.name: "fileopen" + hoverEnabled: true + onClicked: backgroundType.open() + } + + Controls.Popup { + id: backgroundType + x: backgroundButton.x + y: backgroundButton.y + backgroundButton.height + 20 + modal: true + focus: true + dim: false + background: Rectangle { + Kirigami.Theme.colorSet: Kirigami.Theme.Tooltip + color: Kirigami.Theme.backgroundColor + radius: 10 + border.color: Kirigami.Theme.activeBackgroundColor + border.width: 2 + } + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent + ColumnLayout { + anchors.fill: parent + Controls.ToolButton { + Layout.fillHeight: true + Layout.fillWidth: true + text: "Video" + icon.name: "emblem-videos-symbolic" + onClicked: videoFileDialog.open() & backgroundType.close() + } + Controls.ToolButton { + Layout.fillWidth: true + Layout.fillHeight: true + text: "Image" + icon.name: "folder-pictures-symbolic" + onClicked: imageFileDialog.open() & backgroundType.close() + } + } + } + } + } + + Controls.SplitView { + Layout.fillHeight: true + Layout.fillWidth: true + Layout.columnSpan: 2 + handle: Item{ + implicitWidth: 6 + Rectangle { + height: parent.height + anchors.horizontalCenter: parent.horizontalCenter + width: 1 + color: Controls.SplitHandle.hovered ? Kirigami.Theme.hoverColor : Kirigami.Theme.backgroundColor + } + } + + ColumnLayout { + Controls.SplitView.fillHeight: true + Controls.SplitView.preferredWidth: 500 + Controls.SplitView.minimumWidth: 500 + + Controls.TextField { + id: songTitleField + + Layout.preferredWidth: 300 + Layout.fillWidth: true + Layout.leftMargin: 20 + Layout.rightMargin: 20 + + placeholderText: "Song Title..." + text: songTitle + padding: 10 + onEditingFinished: updateTitle(text); + } + Controls.TextField { + id: songVorderField + + Layout.preferredWidth: 300 + Layout.fillWidth: true + Layout.leftMargin: 20 + Layout.rightMargin: 20 + + placeholderText: "verse order..." + text: songVorder + padding: 10 + onEditingFinished: updateVerseOrder(text); + } + + Controls.ScrollView { + id: songLyricsField + + Layout.preferredHeight: 3000 + Layout.fillWidth: true + Layout.fillHeight: true + Layout.leftMargin: 20 + + rightPadding: 20 + + Controls.TextArea { + id: lyricsEditor + width: parent.width + placeholderText: "Put lyrics here..." + persistentSelection: true + text: songLyrics + textFormat: TextEdit.MarkdownText + padding: 10 + onEditingFinished: { + updateLyrics(text); + editorTimer.running = false; + } + onPressed: editorTimer.running = true + } + } + Controls.TextField { + id: songAuthorField + + Layout.fillWidth: true + Layout.preferredWidth: 300 + Layout.leftMargin: 20 + Layout.rightMargin: 20 + + placeholderText: "Author..." + text: songAuthor + padding: 10 + onEditingFinished: updateAuthor(text) + } + + } + ColumnLayout { + Controls.SplitView.fillHeight: true + Controls.SplitView.preferredWidth: 700 + Controls.SplitView.minimumWidth: 300 + + Rectangle { + id: slideBar + color: Kirigami.Theme.highlightColor + + Layout.preferredWidth: 500 + Layout.preferredHeight: songTitleField.height + Layout.rightMargin: 20 + Layout.leftMargin: 20 + } + + Presenter.SlideEditor { + id: slideEditor + Layout.preferredWidth: 500 + Layout.fillWidth: true + Layout.preferredHeight: slideEditor.width / 16 * 9 + Layout.bottomMargin: 30 + Layout.rightMargin: 20 + Layout.leftMargin: 20 + } + + } + } + + } + Timer { + id: editorTimer + interval: 1000 + repeat: true + running: false + onTriggered: updateLyrics(lyricsEditor.text) + } +} diff --git a/src/qml/presenter/LeftDock.qml b/src/qml/presenter/LeftDock.qml index 6c6d5e1..776227c 100644 --- a/src/qml/presenter/LeftDock.qml +++ b/src/qml/presenter/LeftDock.qml @@ -109,6 +109,7 @@ ColumnLayout { showPassiveNotification(serviceItemList.currentIndex); changeSlideBackground(background, backgroundType); changeSlideText(text); + changeSlideType(type); } } diff --git a/src/qml/presenter/Library.qml b/src/qml/presenter/Library.qml index 9e00935..62762c6 100644 --- a/src/qml/presenter/Library.qml +++ b/src/qml/presenter/Library.qml @@ -1,14 +1,17 @@ import QtQuick 2.13 import QtQuick.Controls 2.0 as Controls import QtQuick.Layouts 1.2 +import Qt.labs.platform 1.1 as Labs import org.kde.kirigami 2.13 as Kirigami import "./" as Presenter import org.presenter 1.0 +import mpv 1.0 Item { id: root property string selectedLibrary: "songs" + property bool overlay: false Kirigami.Theme.colorSet: Kirigami.Theme.View @@ -292,10 +295,37 @@ Item { opacity: 1.0 Controls.Label { + id: videoLabel anchors.centerIn: parent text: "Videos" } + Controls.Label { + id: videoCount + anchors {left: videoLabel.right + verticalCenter: videoLabel.verticalCenter + leftMargin: 15} + text: videosqlmodel.rowCount() + font.pixelSize: 15 + color: Kirigami.Theme.disabledTextColor + } + + Kirigami.Icon { + id: videoDrawerArrow + anchors {right: parent.right + verticalCenter: videoCount.verticalCenter + rightMargin: 10} + source: "arrow-down" + rotation: selectedLibrary == "videos" ? 0 : 180 + + Behavior on rotation { + NumberAnimation { + easing.type: Easing.OutCubic + duration: 300 + } + } + } + MouseArea { anchors.fill: parent onClicked: { @@ -313,6 +343,9 @@ Item { Layout.preferredHeight: parent.height - 200 Layout.fillWidth: true Layout.alignment: Qt.AlignTop + model: videosqlmodel + delegate: videoDelegate + clip: true state: "deselected" states: [ @@ -339,6 +372,109 @@ Item { duration: 300 } } + + Component { + id: videoDelegate + Item{ + implicitWidth: ListView.view.width + height: selectedLibrary == "videos" ? 50 : 0 + Kirigami.BasicListItem { + id: videoListItem + + property bool rightMenu: false + + implicitWidth: videoLibraryList.width + height: selectedLibrary == "videos" ? 50 : 0 + clip: true + label: title + /* subtitle: author */ + supportsMouseEvents: false + backgroundColor: { + if (parent.ListView.isCurrentItem) { + Kirigami.Theme.highlightColor; + } else if (videoDragHandler.containsMouse){ + Kirigami.Theme.highlightColor; + } else { + Kirigami.Theme.backgroundColor; + } + } + textColor: { + if (parent.ListView.isCurrentItem || videoDragHandler.containsMouse) + activeTextColor; + else + Kirigami.Theme.textColor; + } + + Behavior on height { + NumberAnimation { + easing.type: Easing.OutCubic + duration: 300 + } + } + Drag.active: videoDragHandler.drag.active + Drag.hotSpot.x: width / 2 + Drag.hotSpot.y: height / 2 + Drag.keys: [ "library" ] + + states: State { + name: "dragged" + when: videoListItem.Drag.active + PropertyChanges { + target: videoListItem + x: x + y: y + } + } + + } + + MouseArea { + id: videoDragHandler + anchors.fill: parent + hoverEnabled: true + drag { + target: videoListItem + onActiveChanged: { + if (videoDragHandler.drag.active) { + dragVideoTitle = title + showPassiveNotification(dragVideoTitle) + } else { + videoListItem.Drag.drop() + } + } + filterChildren: true + threshold: 10 + } + MouseArea { + id: videoClickHandler + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + onClicked: { + if(mouse.button == Qt.RightButton) + rightClickVideoMenu.popup() + else{ + videoLibraryList.currentIndex = index + const video = videosqlmodel.getVideo(videoLibraryList.currentIndex); + /* showPassiveNotification("selected video: " + video); */ + if (!editMode) + editMode = true; + editSwitch("video", video); + } + } + + } + } + Controls.Menu { + id: rightClickVideoMenu + x: videoClickHandler.mouseX + y: videoClickHandler.mouseY + 10 + Kirigami.Action { + text: "delete" + onTriggered: videosqlmodel.deleteVideo(index) + } + } + } + } } Rectangle { @@ -513,5 +649,59 @@ Item { } } + DropArea { + id: fileDropArea + anchors.fill: parent + onDropped: drop => { + overlay = false; + showPassiveNotification("dropped"); + print(drop.urls); + /* thumbnailer.loadFile(drop.urls[0]); */ + addVideo(drop.urls[0]); + } + onEntered: overlay = true + onExited: overlay = false + + function addVideo(url) { + videosqlmodel.newVideo(url); + selectedLibrary = "videos"; + videoLibraryList.currentIndex = videosqlmodel.rowCount(); + print(videosqlmodel.getVideo(videoLibraryList.currentIndex)); + const video = videosqlmodel.getVideo(videoLibraryList.currentIndex); + showPassiveNotification("newest video: " + video); + if (!editMode) + editMode = true; + editSwitch("video", video); + } + } + + Rectangle { + id: fileDropOverlay + color: overlay ? Kirigami.Theme.highlightColor : "#00000000" + anchors.fill: parent + border.width: 8 + border.color: overlay ? Kirigami.Theme.hoverColor : "#00000000" + } + + MpvObject { + id: thumbnailer + useHwdec: true + enableAudio: false + width: 0 + height: 0 + Component.onCompleted: print("ready") + onFileLoaded: { + thumbnailer.pause(); + print("FILE: " + thumbnailer.mediaTitle); + thumbnailer.screenshotToFile(thumbnailFile(thumbnailer.mediaTitle)); + showPassiveNotification("Screenshot Taken to: " + thumbnailFile(thumbnailer.mediaTitle)); + thumbnailer.stop(); + } + function thumbnailFile(title) { + const thumbnailFolder = Labs.StandardPaths.writableLocation(Labs.StandardPaths.AppDataLocation) + "/thumbnails/"; + return Qt.resolvedUrl(thumbnailFolder + title); + } + } + } } diff --git a/src/qml/presenter/MainWindow.qml b/src/qml/presenter/MainWindow.qml index 3fcf9e6..851568f 100644 --- a/src/qml/presenter/MainWindow.qml +++ b/src/qml/presenter/MainWindow.qml @@ -20,6 +20,8 @@ Controls.Page { property string songVorder: "" property int blurRadius: 0 + /* property var video */ + property string dragSongTitle: "" property bool editing: true @@ -78,6 +80,20 @@ Controls.Page { } } + Component { + id: videoEditorComp + Presenter.VideoEditor { + id: videoEditor + } + } + + Component { + id: imageEditorComp + Presenter.ImageEditor { + id: imageEditor + } + } + Loader { id: presentLoader active: presenting @@ -152,6 +168,19 @@ Controls.Page { id: songsqlmodel } + VideoSqlModel { + id: videosqlmodel + } + + function changeSlideType(type) { + /* showPassiveNotification("used to be: " + presentation.text); */ + presentation.itemType = type; + /* showPassiveNotification("next"); */ + if (slideItem) + slideItem.itemType = type; + /* showPassiveNotification("last"); */ + } + function changeSlideText(text) { /* showPassiveNotification("used to be: " + presentation.text); */ presentation.text = text; @@ -193,11 +222,24 @@ Controls.Page { showPassiveNotification("previous slide please") } - function editSwitch(edit) { - if (edit) - mainPageArea.push(songEditorComp, Controls.StackView.Immediate) - else - mainPageArea.pop(Controls.StackView.Immediate) + function editSwitch(editType, item) { + if (editMode) { + switch (editType) { + case "song" : + mainPageArea.push(songEditorComp, Controls.StackView.Immediate); + break; + case "video" : + mainPageArea.push(videoEditorComp, {"video": item}, Controls.StackView.Immediate); + break; + case "image" : + mainPageArea.push(imageEditorComp, Controls.StackView.Immediate); + break; + default: + mainPageArea.pop(Controls.StackView.Immediate); + editMode = false; + } + } else + mainPageArea.pop(Controls.StackView.Immediate); } function present(present) { diff --git a/src/qml/presenter/Presentation.qml b/src/qml/presenter/Presentation.qml index 1c46a59..42062c7 100644 --- a/src/qml/presenter/Presentation.qml +++ b/src/qml/presenter/Presentation.qml @@ -11,6 +11,7 @@ Item { id: root property string text + property string itemType property url imagebackground property url vidbackground @@ -80,6 +81,7 @@ Item { Layout.alignment: Qt.AlignCenter textSize: width / 15 text: root.text + itemType: root.itemType imageSource: imagebackground videoSource: vidbackground preview: true diff --git a/src/qml/presenter/Slide.qml b/src/qml/presenter/Slide.qml index 4677948..099ee58 100644 --- a/src/qml/presenter/Slide.qml +++ b/src/qml/presenter/Slide.qml @@ -45,10 +45,10 @@ Item { enableAudio: !preview Component.onCompleted: mpvLoadingTimer.start() onFileLoaded: { - print(videoSource + " has been loaded"); + showPassiveNotification(videoSource + " has been loaded"); if (itemType == "song") mpv.setProperty("loop", "inf"); - print(mpv.getProperty("loop")); + showPassiveNotification(mpv.getProperty("loop")); } MouseArea { diff --git a/src/qml/presenter/VideoEditor.qml b/src/qml/presenter/VideoEditor.qml new file mode 100644 index 0000000..a5a6fe1 --- /dev/null +++ b/src/qml/presenter/VideoEditor.qml @@ -0,0 +1,227 @@ +import QtQuick 2.13 +import QtQuick.Controls 2.15 as Controls +import QtQuick.Dialogs 1.3 +import QtQuick.Layouts 1.2 +import org.kde.kirigami 2.13 as Kirigami +import "./" as Presenter +import mpv 1.0 + +Item { + id: root + + property var video + property bool audioOn: true + + GridLayout { + id: mainLayout + anchors.fill: parent + columns: 2 + rowSpacing: 5 + columnSpacing: 0 + + Controls.ToolBar { + Layout.fillWidth: true + Layout.columnSpan: 2 + id: toolbar + RowLayout { + anchors.fill: parent + + Controls.ComboBox { + model: Qt.fontFamilies() + implicitWidth: 300 + editable: true + hoverEnabled: true + onCurrentTextChanged: showPassiveNotification(currentText) + } + Controls.SpinBox { + editable: true + from: 5 + to: 72 + hoverEnabled: true + } + Controls.ComboBox { + model: ["VIDEOS", "Center", "Right", "Justify"] + implicitWidth: 100 + hoverEnabled: true + } + Controls.ToolButton { + text: "B" + hoverEnabled: true + } + Controls.ToolButton { + text: "I" + hoverEnabled: true + } + Controls.ToolButton { + text: "U" + hoverEnabled: true + } + Controls.ToolSeparator {} + Item { Layout.fillWidth: true } + Controls.ToolSeparator {} + Controls.ToolButton { + text: "Effects" + icon.name: "image-auto-adjust" + hoverEnabled: true + onClicked: {} + } + Controls.ToolButton { + id: backgroundButton + text: "Background" + icon.name: "fileopen" + hoverEnabled: true + onClicked: backgroundType.open() + } + + Controls.Popup { + id: backgroundType + x: backgroundButton.x + y: backgroundButton.y + backgroundButton.height + 20 + modal: true + focus: true + dim: false + background: Rectangle { + Kirigami.Theme.colorSet: Kirigami.Theme.Tooltip + color: Kirigami.Theme.backgroundColor + radius: 10 + border.color: Kirigami.Theme.activeBackgroundColor + border.width: 2 + } + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent + ColumnLayout { + anchors.fill: parent + Controls.ToolButton { + Layout.fillHeight: true + Layout.fillWidth: true + text: "Video" + icon.name: "emblem-videos-symbolic" + onClicked: videoFileDialog.open() & backgroundType.close() + } + Controls.ToolButton { + Layout.fillWidth: true + Layout.fillHeight: true + text: "Image" + icon.name: "folder-pictures-symbolic" + onClicked: imageFileDialog.open() & backgroundType.close() + } + } + } + } + } + + Controls.SplitView { + Layout.fillHeight: true + Layout.fillWidth: true + Layout.columnSpan: 2 + handle: Item{ + implicitWidth: 6 + Rectangle { + height: parent.height + anchors.horizontalCenter: parent.horizontalCenter + width: 1 + color: Controls.SplitHandle.hovered ? Kirigami.Theme.hoverColor : Kirigami.Theme.backgroundColor + } + } + + ColumnLayout { + Controls.SplitView.fillHeight: true + Controls.SplitView.preferredWidth: 300 + Controls.SplitView.minimumWidth: 100 + + Controls.TextField { + id: videoTitleField + + Layout.preferredWidth: 300 + Layout.fillWidth: true + Layout.leftMargin: 20 + Layout.rightMargin: 20 + + placeholderText: "Song Title..." + text: video[0] + padding: 10 + /* onEditingFinished: updateTitle(text); */ + } + + Item { + id: empty + Layout.fillHeight: true + } + } + ColumnLayout { + Controls.SplitView.fillHeight: true + Controls.SplitView.preferredWidth: 700 + Controls.SplitView.minimumWidth: 300 + spacing: 5 + + Item { + id: topEmpty + Layout.fillHeight: true + } + + MpvObject { + id: videoPreview + objectName: "mpv" + Layout.preferredWidth: 600 + Layout.preferredHeight: Layout.preferredWidth / 16 * 9 + Layout.alignment: Qt.AlignCenter + useHwdec: true + enableAudio: audioOn + Component.onCompleted: mpvLoadingTimer.start() + onPositionChanged: videoSlider.value = position + onFileLoaded: { + showPassiveNotification(video.title + " has been loaded"); + videoPreview.pause(); + /* showPassiveNotification(mpv.getProperty("loop")); */ + } + } + Rectangle { + id: videoBg + color: Kirigami.Theme.alternateBackgroundColor + + Layout.preferredWidth: videoPreview.Layout.preferredWidth + Layout.preferredHeight: videoTitleField.height + Layout.alignment: Qt.AlignHCenter + + RowLayout { + anchors.fill: parent + spacing: 2 + Kirigami.Icon { + source: videoPreview.isPlaying ? "media-pause" : "media-play" + Layout.preferredWidth: 25 + Layout.preferredHeight: 25 + MouseArea { + anchors.fill: parent + onPressed: videoPreview.playPause() + cursorShape: Qt.PointingHandCursor + } + } + Controls.Slider { + id: videoSlider + Layout.fillWidth: true + Layout.preferredHeight: 25 + from: 0 + to: videoPreview.duration + /* value: videoPreview.postion */ + live: false + onMoved: videoPreview.seek(value); + } + } + } + + Item { + id: botEmpty + Layout.fillHeight: true + } + + } + } + } + Timer { + id: mpvLoadingTimer + interval: 100 + onTriggered: { + videoPreview.loadFile(video[1].toString()); + /* showPassiveNotification(video[0]); */ + } + } +} diff --git a/src/resources.qrc b/src/resources.qrc index b1f420b..5fc74df 100644 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -9,6 +9,8 @@ qml/presenter/Actions.qml qml/presenter/PanelItem.qml qml/presenter/SongEditor.qml + qml/presenter/VideoEditor.qml + qml/presenter/ImageEditor.qml qml/presenter/Slide.qml qml/presenter/SlideEditor.qml qml/presenter/DragHandle.qml diff --git a/src/songlistmodel.cpp b/src/songlistmodel.cpp deleted file mode 100644 index a0caef3..0000000 --- a/src/songlistmodel.cpp +++ /dev/null @@ -1,70 +0,0 @@ -#include "songlistmodel.h" -#include -#include - -SongListModel::SongListModel(QObject *parent) - : QAbstractListModel(parent) -{ - m_data - << Data("10,000 Reasons", "10,000 reasons for my heart to sing", "Matt Redman", "13470183", "") - << Data("Marvelous Light", "Into marvelous light I'm running", "Chris Tomlin", "13470183", "") - << Data("10,000 Reasons", "10,000 reasons for my heart to sing", "Matt Redman", "13470183", "") - << Data("Marvelous Light", "Into marvelous light I'm running", "Chris Tomlin", "13470183", "") - << Data("River", "I'm going down to the river", "Jordan Feliz", "13470183", "") - << Data("Marvelous Light", "Into marvelous light I'm running", "Chris Tomlin", "13470183", "") - << Data("10,000 Reasons", "10,000 reasons for my heart to sing", "Matt Redman", "13470183", "") - << Data("Marvelous Light", "Into marvelous light I'm running", "Chris Tomlin", "13470183", "") - << Data("Marvelous Light", "Into marvelous light I'm running", "Chris Tomlin", "13470183", "") - << Data("10,000 Reasons", "10,000 reasons for my heart to sing", "Matt Redman", "13470183", "") - << Data("Marvelous Light", "Into marvelous light I'm running", "Chris Tomlin", "13470183", "") - << Data("10,000 Reasons", "10,000 reasons for my heart to sing", "Matt Redman", "13470183", "") - << Data("Marvelous Light", "Into marvelous light I'm running", "Chris Tomlin", "13470183", ""); -} - -int SongListModel::rowCount(const QModelIndex &parent) const -{ - if (parent.isValid()) - return 0; - return m_data.count(); -} - -QVariant SongListModel::data(const QModelIndex &index, int role) const -{ - if (!index.isValid()) - return QVariant(); - - // this is the returning of song data - const Data &data = m_data.at(index.row()); - if ( role == TitleRole ) - return data.title; - else if (role == LyricsRole) - return data.lyrics; - else if (role == AuthorRole) - return data.author; - else if (role == CCLINumRole) - return data.ccli; - else if (role == AudioRole) - return data.audio; - else - return QVariant(); -} - -QHash SongListModel::roleNames() const -{ - static QHash mapping { - {TitleRole, "title"}, - {LyricsRole, "lyrics"}, - {AuthorRole, "author"}, - {CCLINumRole, "ccli"}, - {AudioRole, "audio"} - }; - - return mapping; -} - -void SongListModel::lyricsSlides(QString lyrics) -{ - QTextStream stream(&lyrics); - QString line = stream.readLine(); - qDebug() << line; -} diff --git a/src/songlistmodel.h b/src/songlistmodel.h deleted file mode 100644 index 94e3286..0000000 --- a/src/songlistmodel.h +++ /dev/null @@ -1,47 +0,0 @@ -#ifndef SONGLISTMODEL_H -#define SONGLISTMODEL_H - -#include - -struct Data { - Data () {} - Data ( const QString& title, const QString& lyrics, const QString& author, - const QString& ccli, const QString& audio) - : title(title), lyrics(lyrics), author(author), ccli(ccli), audio(audio) {} - QString title; - QString lyrics; - QString author; - QString ccli; - QString audio; -}; - -class SongListModel : public QAbstractListModel -{ - Q_OBJECT - -public: - enum Roles { - TitleRole = Qt::UserRole, - LyricsRole, - AuthorRole, - CCLINumRole, - AudioRole, - }; - explicit SongListModel(QObject *parent = nullptr); - - // Basic functionality: - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - - QHash roleNames() const override; - - -private: - QVector< Data > m_data; - -public slots: - void lyricsSlides(QString lyrics); -}; - -#endif // SONGLISTMODEL_H diff --git a/src/songsqlmodel.cpp b/src/songsqlmodel.cpp index cc29018..1097aaa 100644 --- a/src/songsqlmodel.cpp +++ b/src/songsqlmodel.cpp @@ -92,9 +92,9 @@ void SongSqlModel::newSong() { if (insertRecord(rows, recorddata)) { submitAll(); - }else { + } else { qDebug() << lastError(); - } + }; } void SongSqlModel::deleteSong(const int &row) { diff --git a/src/songsqlmodel.h b/src/songsqlmodel.h index a264194..112dba6 100644 --- a/src/songsqlmodel.h +++ b/src/songsqlmodel.h @@ -2,7 +2,6 @@ #define SONGSQLMODEL_H #include -#include #include #include #include diff --git a/src/videosqlmodel.cpp b/src/videosqlmodel.cpp new file mode 100644 index 0000000..20c247b --- /dev/null +++ b/src/videosqlmodel.cpp @@ -0,0 +1,167 @@ +#include "videosqlmodel.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const char *videosTableName = "videos"; + +static void createVideoTable() +{ + if(QSqlDatabase::database().tables().contains(videosTableName)) { + return; + } + + QSqlQuery query; + if (!query.exec("CREATE TABLE IF NOT EXISTS 'videos' (" + " 'id' INTEGER NOT NULL," + " 'title' TEXT NOT NULL," + " 'filePath' TEXT NOT NULL," + " PRIMARY KEY(id))")) { + qFatal("Failed to query database: %s", + qPrintable(query.lastError().text())); + } + qDebug() << query.lastQuery(); + qDebug() << "inserting into videos"; + + query.exec("INSERT INTO videos (title, filePath) VALUES ('The Test', '/home/chris/nextcloud/tfc/openlp/videos/test.mp4')"); + qDebug() << query.lastQuery(); + query.exec("INSERT INTO videos (title, filePath) VALUES ('Sabbath', '/home/chris/nextcloud/tfc/openlp/videos/Sabbath.mp4')"); + + query.exec("select * from videos"); + qDebug() << query.lastQuery(); +} + +VideoSqlModel::VideoSqlModel(QObject *parent) : QSqlTableModel(parent) { + qDebug() << "creating video table"; + createVideoTable(); + setTable(videosTableName); + setEditStrategy(QSqlTableModel::OnManualSubmit); + // make sure to call select else the model won't fill + select(); +} + +QVariant VideoSqlModel::data(const QModelIndex &index, int role) const { + if (role < Qt::UserRole) { + return QSqlTableModel::data(index, role); + } + + // qDebug() << role; + const QSqlRecord sqlRecord = record(index.row()); + return sqlRecord.value(role - Qt::UserRole); +} + +QHash VideoSqlModel::roleNames() const +{ + QHash names; + names[Qt::UserRole] = "id"; + names[Qt::UserRole + 1] = "title"; + names[Qt::UserRole + 2] = "filePath"; + return names; +} + +void VideoSqlModel::newVideo(const QUrl &filePath) { + qDebug() << "adding new video"; + int rows = rowCount(); + + qDebug() << rows; + QSqlRecord recordData = record(); + QFileInfo fileInfo = filePath.toString(); + QString title = fileInfo.baseName(); + recordData.setValue("title", title); + recordData.setValue("filePath", filePath); + + if (insertRecord(rows, recordData)) { + submitAll(); + } else { + qDebug() << lastError(); + }; +} + +void VideoSqlModel::deleteVideo(const int &row) { + QSqlRecord recordData = record(row); + if (recordData.isEmpty()) + return; + + removeRow(row); + submitAll(); +} + +int VideoSqlModel::id() const { + return m_id; +} + +QString VideoSqlModel::title() const { + return m_title; +} + +void VideoSqlModel::setTitle(const QString &title) { + if (title == m_title) + return; + + m_title = title; + + select(); + emit titleChanged(); +} + +// This function is for updating the title from outside the delegate +void VideoSqlModel::updateTitle(const int &row, const QString &title) { + qDebug() << "Row is " << row; + QSqlRecord rowdata = record(row); + qDebug() << rowdata; + rowdata.setValue("title", title); + setRecord(row, rowdata); + qDebug() << rowdata; + submitAll(); + emit titleChanged(); +} + +QUrl VideoSqlModel::filePath() const { + return m_filePath; +} + +void VideoSqlModel::setFilePath(const QUrl &filePath) { + if (filePath == m_filePath) + return; + + m_filePath = filePath; + + select(); + emit filePathChanged(); +} + +// This function is for updating the filepath from outside the delegate +void VideoSqlModel::updateFilePath(const int &row, const QUrl &filePath) { + qDebug() << "Row is " << row; + QSqlRecord rowdata = record(row); + qDebug() << rowdata; + rowdata.setValue("filePath", filePath); + setRecord(row, rowdata); + qDebug() << rowdata; + submitAll(); + emit filePathChanged(); +} + +QVariantList VideoSqlModel::getVideo(const int &row) { + qDebug() << "Row we are getting is " << row; + QVariantList video; + QSqlRecord rec = record(row - 1); + qDebug() << rec.value("title"); + video.append(rec.value("title")); + video.append(rec.value("filePath")); + return video; +} diff --git a/src/videosqlmodel.h b/src/videosqlmodel.h new file mode 100644 index 0000000..be269a7 --- /dev/null +++ b/src/videosqlmodel.h @@ -0,0 +1,49 @@ +#ifndef VIDEOSQLMODEL_H +#define VIDEOSQLMODEL_H + +#include +#include +#include +#include +#include +#include + +class VideoSqlModel : public QSqlTableModel +{ + Q_OBJECT + Q_PROPERTY(int id READ id) + Q_PROPERTY(QString title READ title WRITE setTitle NOTIFY titleChanged) + Q_PROPERTY(QUrl filePath READ filePath WRITE setFilePath NOTIFY filePathChanged) + QML_ELEMENT + +public: + VideoSqlModel(QObject *parent = 0); + + int id() const; + QString title() const; + QUrl filePath() const; + + void setTitle(const QString &title); + void setFilePath(const QUrl &filePath); + + Q_INVOKABLE void updateTitle(const int &row, const QString &title); + Q_INVOKABLE void updateFilePath(const int &row, const QUrl &filePath); + + Q_INVOKABLE void newVideo(const QUrl &filePath); + Q_INVOKABLE void deleteVideo(const int &row); + Q_INVOKABLE QVariantList getVideo(const int &row); + + QVariant data(const QModelIndex &index, int role) const override; + QHash roleNames() const override; + +signals: + void titleChanged(); + void filePathChanged(); + +private: + int m_id; + QString m_title; + QUrl m_filePath; +}; + +#endif //SONGSQLMODEL_H