adding in a video model and editor

This commit is contained in:
Chris Cochrun 2022-03-15 15:08:17 -05:00
parent fab9f86b41
commit c35c0f6550
22 changed files with 972 additions and 160 deletions

View file

@ -2,4 +2,5 @@
;;; For more information see (info "(emacs) Directory Variables") ;;; For more information see (info "(emacs) Directory Variables")
((nil . ((projectile-project-compilation-cmd . "cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1 -B build/ . && make --dir build/") ((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))))

View file

@ -2,9 +2,9 @@
#+AUTHOR: Chris Cochrun #+AUTHOR: Chris Cochrun
* Church Presenter * 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 songs lyrics with image and video backgrounds
- Presents slides - Presents slides
- (Custom slide builder) - (Custom slide builder)

View file

@ -3,8 +3,8 @@ add_executable(presenter)
target_sources(presenter target_sources(presenter
PRIVATE PRIVATE
main.cpp resources.qrc main.cpp resources.qrc
songlistmodel.cpp songlistmodel.h
songsqlmodel.cpp songsqlmodel.h songsqlmodel.cpp songsqlmodel.h
videosqlmodel.cpp videosqlmodel.h
mpv/mpvobject.h mpv/mpvobject.cpp mpv/mpvobject.h mpv/mpvobject.cpp
mpv/qthelper.hpp mpv/mpvhelpers.h mpv/qthelper.hpp mpv/mpvhelpers.h
) )

View file

@ -30,9 +30,9 @@
#include <qsqlquery.h> #include <qsqlquery.h>
#include <qstringliteral.h> #include <qstringliteral.h>
#include "songlistmodel.h"
#include "mpv/mpvobject.h" #include "mpv/mpvobject.h"
#include "songsqlmodel.h" #include "songsqlmodel.h"
#include "videosqlmodel.h"
static void connectToDatabase() { static void connectToDatabase() {
// let's setup our sql database // let's setup our sql database
@ -44,6 +44,7 @@ static void connectToDatabase() {
} }
const QDir writeDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); const QDir writeDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
qDebug() << "dir location " << writeDir.absolutePath();
if (!writeDir.mkpath(".")) { if (!writeDir.mkpath(".")) {
qFatal("Failed to create writable location at %s", qPrintable(writeDir.absolutePath())); qFatal("Failed to create writable location at %s", qPrintable(writeDir.absolutePath()));
@ -79,21 +80,21 @@ int main(int argc, char *argv[])
#endif #endif
QGuiApplication::setWindowIcon(QIcon::fromTheme(QStringLiteral("system-config-display"))); QGuiApplication::setWindowIcon(QIcon::fromTheme(QStringLiteral("system-config-display")));
// apparently mpv needs this class set // apparently mpv needs this class set
// let's register mpv as well
std::setlocale(LC_NUMERIC, "C"); std::setlocale(LC_NUMERIC, "C");
qmlRegisterType<MpvObject>("mpv", 1, 0, "MpvObject"); qmlRegisterType<MpvObject>("mpv", 1, 0, "MpvObject");
//register our song model from sql //register our models
qmlRegisterType<SongSqlModel>("org.presenter", 1, 0, "SongSqlModel"); qmlRegisterType<SongSqlModel>("org.presenter", 1, 0, "SongSqlModel");
qmlRegisterType<VideoSqlModel>("org.presenter", 1, 0, "VideoSqlModel");
SongListModel songListModel;
connectToDatabase(); connectToDatabase();
QQmlApplicationEngine engine; QQmlApplicationEngine engine;
engine.rootContext()->setContextObject(new KLocalizedContext(&engine)); engine.rootContext()->setContextObject(new KLocalizedContext(&engine));
engine.rootContext()->setContextProperty("_songListModel", &songListModel);
engine.load(QUrl(QStringLiteral("qrc:qml/main.qml"))); engine.load(QUrl(QStringLiteral("qrc:qml/main.qml")));
// QQuickView *view = new QQuickView; // QQuickView *view = new QQuickView;

View file

@ -1,6 +1,8 @@
#include "mpvobject.h" #include "mpvobject.h"
// std // std
#include <qdir.h>
#include <qvariant.h>
#include <stdexcept> #include <stdexcept>
#include <clocale> #include <clocale>
@ -19,6 +21,7 @@
#include <QtX11Extras/QX11Info> #include <QtX11Extras/QX11Info>
#include <QDebug> #include <QDebug>
#include <QStandardPaths>
// libmpv // libmpv
#include <mpv/client.h> #include <mpv/client.h>
@ -28,6 +31,7 @@
#include "qthelper.hpp" #include "qthelper.hpp"
const QDir writeDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
//--- MpvRenderer //--- MpvRenderer
void* MpvRenderer::get_proc_address(void *ctx, const char *name) { void* MpvRenderer::get_proc_address(void *ctx, const char *name) {
@ -261,11 +265,13 @@ void MpvObject::doUpdate()
void MpvObject::command(const QVariant& params) void MpvObject::command(const QVariant& params)
{ {
// qDebug() << params;
mpv::qt::command(mpv, params); mpv::qt::command(mpv, params);
} }
void MpvObject::commandAsync(const QVariant& params) void MpvObject::commandAsync(const QVariant& params)
{ {
qDebug() << params;
mpv::qt::command_async(mpv, params); mpv::qt::command_async(mpv, params);
} }
@ -522,6 +528,17 @@ void MpvObject::loadFile(QVariant urls)
command(QVariantList() << "loadfile" << 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) void MpvObject::subAdd(QVariant urls)
{ {
command(QVariantList() << "sub-add" << urls); command(QVariantList() << "sub-add" << urls);

View file

@ -135,6 +135,7 @@ public slots:
void stepForward(); void stepForward();
void seek(double pos); void seek(double pos);
void loadFile(QVariant urls); void loadFile(QVariant urls);
void screenshotToFile(QUrl url);
void subAdd(QVariant urls); void subAdd(QVariant urls);
bool enableAudio() const { return m_enableAudio; } bool enableAudio() const { return m_enableAudio; }

View file

@ -2,7 +2,7 @@
Type=Application Type=Application
Name=present Name=present
GenericName=Church Presentation GenericName=Church Presentation
Comment=A Kirigami base church presenter Comment=A Kirigami based church presenter
Exec=present %U Exec=present %U
TryExec=present TryExec=present
Icon=present Icon=present

View file

@ -26,28 +26,28 @@ Kirigami.ApplicationWindow {
pageStack.initialPage: mainPage pageStack.initialPage: mainPage
header: Presenter.Header {} header: Presenter.Header {}
menuBar: Controls.MenuBar { /* menuBar: Qt.platform.os !== "linux" ? Controls.MenuBar { */
Controls.Menu { /* Controls.Menu { */
title: qsTr("File") /* title: qsTr("File") */
Controls.MenuItem { text: qsTr("New...") } /* Controls.MenuItem { text: qsTr("New...") } */
Controls.MenuItem { text: qsTr("Open...") } /* Controls.MenuItem { text: qsTr("Open...") } */
Controls.MenuItem { text: qsTr("Save") } /* Controls.MenuItem { text: qsTr("Save") } */
Controls.MenuItem { text: qsTr("Save As...") } /* Controls.MenuItem { text: qsTr("Save As...") } */
Controls.MenuSeparator { } /* Controls.MenuSeparator { } */
Controls.MenuItem { text: qsTr("Quit") } /* Controls.MenuItem { text: qsTr("Quit") } */
} /* } */
Controls.Menu { /* Controls.Menu { */
title: qsTr("Settings") /* title: qsTr("Settings") */
Controls.MenuItem { /* Controls.MenuItem { */
text: qsTr("Configure") /* text: qsTr("Configure") */
onTriggered: openSettings() /* onTriggered: openSettings() */
} /* } */
} /* } */
Controls.Menu { /* Controls.Menu { */
title: qsTr("Help") /* title: qsTr("Help") */
Controls.MenuItem { text: qsTr("About") } /* Controls.MenuItem { text: qsTr("About") } */
} /* } */
} /* } : null */
Labs.MenuBar { Labs.MenuBar {
Labs.Menu { Labs.Menu {
@ -81,7 +81,7 @@ Kirigami.ApplicationWindow {
function toggleEditMode() { function toggleEditMode() {
editMode = !editMode; editMode = !editMode;
mainPage.editSwitch(editMode); mainPage.editSwitch();
} }
function toggleLibrary() { function toggleLibrary() {

View file

@ -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)
}
}

View file

@ -109,6 +109,7 @@ ColumnLayout {
showPassiveNotification(serviceItemList.currentIndex); showPassiveNotification(serviceItemList.currentIndex);
changeSlideBackground(background, backgroundType); changeSlideBackground(background, backgroundType);
changeSlideText(text); changeSlideText(text);
changeSlideType(type);
} }
} }

View file

@ -1,14 +1,17 @@
import QtQuick 2.13 import QtQuick 2.13
import QtQuick.Controls 2.0 as Controls import QtQuick.Controls 2.0 as Controls
import QtQuick.Layouts 1.2 import QtQuick.Layouts 1.2
import Qt.labs.platform 1.1 as Labs
import org.kde.kirigami 2.13 as Kirigami import org.kde.kirigami 2.13 as Kirigami
import "./" as Presenter import "./" as Presenter
import org.presenter 1.0 import org.presenter 1.0
import mpv 1.0
Item { Item {
id: root id: root
property string selectedLibrary: "songs" property string selectedLibrary: "songs"
property bool overlay: false
Kirigami.Theme.colorSet: Kirigami.Theme.View Kirigami.Theme.colorSet: Kirigami.Theme.View
@ -292,10 +295,37 @@ Item {
opacity: 1.0 opacity: 1.0
Controls.Label { Controls.Label {
id: videoLabel
anchors.centerIn: parent anchors.centerIn: parent
text: "Videos" 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 { MouseArea {
anchors.fill: parent anchors.fill: parent
onClicked: { onClicked: {
@ -313,6 +343,9 @@ Item {
Layout.preferredHeight: parent.height - 200 Layout.preferredHeight: parent.height - 200
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop
model: videosqlmodel
delegate: videoDelegate
clip: true
state: "deselected" state: "deselected"
states: [ states: [
@ -339,6 +372,109 @@ Item {
duration: 300 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 { 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);
}
}
} }
} }

View file

@ -20,6 +20,8 @@ Controls.Page {
property string songVorder: "" property string songVorder: ""
property int blurRadius: 0 property int blurRadius: 0
/* property var video */
property string dragSongTitle: "" property string dragSongTitle: ""
property bool editing: true 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 { Loader {
id: presentLoader id: presentLoader
active: presenting active: presenting
@ -152,6 +168,19 @@ Controls.Page {
id: songsqlmodel 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) { function changeSlideText(text) {
/* showPassiveNotification("used to be: " + presentation.text); */ /* showPassiveNotification("used to be: " + presentation.text); */
presentation.text = text; presentation.text = text;
@ -193,11 +222,24 @@ Controls.Page {
showPassiveNotification("previous slide please") showPassiveNotification("previous slide please")
} }
function editSwitch(edit) { function editSwitch(editType, item) {
if (edit) if (editMode) {
mainPageArea.push(songEditorComp, Controls.StackView.Immediate) switch (editType) {
else case "song" :
mainPageArea.pop(Controls.StackView.Immediate) 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) { function present(present) {

View file

@ -11,6 +11,7 @@ Item {
id: root id: root
property string text property string text
property string itemType
property url imagebackground property url imagebackground
property url vidbackground property url vidbackground
@ -80,6 +81,7 @@ Item {
Layout.alignment: Qt.AlignCenter Layout.alignment: Qt.AlignCenter
textSize: width / 15 textSize: width / 15
text: root.text text: root.text
itemType: root.itemType
imageSource: imagebackground imageSource: imagebackground
videoSource: vidbackground videoSource: vidbackground
preview: true preview: true

View file

@ -45,10 +45,10 @@ Item {
enableAudio: !preview enableAudio: !preview
Component.onCompleted: mpvLoadingTimer.start() Component.onCompleted: mpvLoadingTimer.start()
onFileLoaded: { onFileLoaded: {
print(videoSource + " has been loaded"); showPassiveNotification(videoSource + " has been loaded");
if (itemType == "song") if (itemType == "song")
mpv.setProperty("loop", "inf"); mpv.setProperty("loop", "inf");
print(mpv.getProperty("loop")); showPassiveNotification(mpv.getProperty("loop"));
} }
MouseArea { MouseArea {

View file

@ -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]); */
}
}
}

View file

@ -9,6 +9,8 @@
<file>qml/presenter/Actions.qml</file> <file>qml/presenter/Actions.qml</file>
<file>qml/presenter/PanelItem.qml</file> <file>qml/presenter/PanelItem.qml</file>
<file>qml/presenter/SongEditor.qml</file> <file>qml/presenter/SongEditor.qml</file>
<file>qml/presenter/VideoEditor.qml</file>
<file>qml/presenter/ImageEditor.qml</file>
<file>qml/presenter/Slide.qml</file> <file>qml/presenter/Slide.qml</file>
<file>qml/presenter/SlideEditor.qml</file> <file>qml/presenter/SlideEditor.qml</file>
<file>qml/presenter/DragHandle.qml</file> <file>qml/presenter/DragHandle.qml</file>

View file

@ -1,70 +0,0 @@
#include "songlistmodel.h"
#include <QTextStream>
#include <QDebug>
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<int, QByteArray> SongListModel::roleNames() const
{
static QHash<int, QByteArray> 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;
}

View file

@ -1,47 +0,0 @@
#ifndef SONGLISTMODEL_H
#define SONGLISTMODEL_H
#include <QAbstractListModel>
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<int, QByteArray> roleNames() const override;
private:
QVector< Data > m_data;
public slots:
void lyricsSlides(QString lyrics);
};
#endif // SONGLISTMODEL_H

View file

@ -92,9 +92,9 @@ void SongSqlModel::newSong() {
if (insertRecord(rows, recorddata)) { if (insertRecord(rows, recorddata)) {
submitAll(); submitAll();
}else { } else {
qDebug() << lastError(); qDebug() << lastError();
} };
} }
void SongSqlModel::deleteSong(const int &row) { void SongSqlModel::deleteSong(const int &row) {

View file

@ -2,7 +2,6 @@
#define SONGSQLMODEL_H #define SONGSQLMODEL_H
#include <QSqlTableModel> #include <QSqlTableModel>
#include <qabstractitemmodel.h>
#include <qobjectdefs.h> #include <qobjectdefs.h>
#include <qqml.h> #include <qqml.h>
#include <qvariant.h> #include <qvariant.h>

167
src/videosqlmodel.cpp Normal file
View file

@ -0,0 +1,167 @@
#include "videosqlmodel.h"
#include <QDateTime>
#include <QDebug>
#include <QSqlError>
#include <QSqlRecord>
#include <QSqlQuery>
#include <QSql>
#include <QSqlDatabase>
#include <QFileInfo>
#include <qabstractitemmodel.h>
#include <qdebug.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qobjectdefs.h>
#include <qsqlrecord.h>
#include <qurl.h>
#include <qvariant.h>
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<int, QByteArray> VideoSqlModel::roleNames() const
{
QHash<int, QByteArray> 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;
}

49
src/videosqlmodel.h Normal file
View file

@ -0,0 +1,49 @@
#ifndef VIDEOSQLMODEL_H
#define VIDEOSQLMODEL_H
#include <QSqlTableModel>
#include <qobject.h>
#include <qobjectdefs.h>
#include <qqml.h>
#include <qurl.h>
#include <qvariant.h>
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<int, QByteArray> roleNames() const override;
signals:
void titleChanged();
void filePathChanged();
private:
int m_id;
QString m_title;
QUrl m_filePath;
};
#endif //SONGSQLMODEL_H