diff --git a/TODO.org b/TODO.org index 3ae1053..16b7765 100644 --- a/TODO.org +++ b/TODO.org @@ -4,12 +4,8 @@ :END: * Inbox -** TODO BUG in dropping and then selecting song will duplicate entries :dev: -SCHEDULED: <2022-04-05 Tue> -[[file:~/dev/church-presenter/src/qml/presenter/LeftDock.qml::Layout.fillHeight: true]] - -or at least turns the entry above it into the same as itself while retaining it's title? - +** TODO Make an image sql model +[[file:~/dev/church-presenter/src/videosqlmodel.h::ifndef VIDEOSQLMODEL_H]] ** TODO Build out a slide preview system so we can see each slide in the song or image slideshow [[file:~/dev/church-presenter/src/qml/presenter/SongEditor.qml::Presenter.SlideEditor {]] @@ -53,6 +49,12 @@ or at least turns the entry above it into the same as itself while retaining it' ** DONE Parse Lyrics to create a list of strings for slides SCHEDULED: <2022-03-23 Wed 10:00> +** DONE BUG in dropping and then selecting song will duplicate entries :dev: +SCHEDULED: <2022-04-05 Tue> +[[file:~/dev/church-presenter/src/qml/presenter/LeftDock.qml::Layout.fillHeight: true]] + +or at least turns the entry above it into the same as itself while retaining it's title? + ** DONE Make nextSlideText a nextAction function to incorporate other types of items [[file:~/dev/church-presenter/src/qml/presenter/Presentation.qml::function nextSlideText() {]] diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fe6f05d..ab4398d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -7,6 +7,7 @@ target_sources(presenter serviceitemmodel.cpp serviceitemmodel.h serviceitem.cpp serviceitem.h videosqlmodel.cpp videosqlmodel.h + imagesqlmodel.cpp imagesqlmodel.h mpv/mpvobject.h mpv/mpvobject.cpp mpv/qthelper.hpp mpv/mpvhelpers.h ) diff --git a/src/imagesqlmodel.cpp b/src/imagesqlmodel.cpp new file mode 100644 index 0000000..c4537f0 --- /dev/null +++ b/src/imagesqlmodel.cpp @@ -0,0 +1,169 @@ +#include "imagesqlmodel.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const char *imagesTableName = "images"; + +static void createImageTable() +{ + if(QSqlDatabase::database().tables().contains(imagesTableName)) { + return; + } + + QSqlQuery query; + if (!query.exec("CREATE TABLE IF NOT EXISTS 'images' (" + " '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 images"; + + query.exec("INSERT INTO images (title, filePath) VALUES ('Dec 180', '/home/chris/nextcloud/tfc/openlp/180-dec.png')"); + qDebug() << query.lastQuery(); + query.exec("INSERT INTO images (title, filePath) VALUES ('No TFC', " + "'/home/chris/nextcloud/tfc/openlp/No TFC.png')"); + + query.exec("select * from images"); + qDebug() << query.lastQuery(); +} + +ImageSqlModel::ImageSqlModel(QObject *parent) : QSqlTableModel(parent) { + qDebug() << "creating image table"; + createImageTable(); + setTable(imagesTableName); + setEditStrategy(QSqlTableModel::OnManualSubmit); + // make sure to call select else the model won't fill + select(); +} + +QVariant ImageSqlModel::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 ImageSqlModel::roleNames() const +{ + QHash names; + names[Qt::UserRole] = "id"; + names[Qt::UserRole + 1] = "title"; + names[Qt::UserRole + 2] = "filePath"; + return names; +} + +void ImageSqlModel::newImage(const QUrl &filePath) { + qDebug() << "adding new image"; + 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 ImageSqlModel::deleteImage(const int &row) { + QSqlRecord recordData = record(row); + if (recordData.isEmpty()) + return; + + removeRow(row); + submitAll(); +} + +int ImageSqlModel::id() const { + return m_id; +} + +QString ImageSqlModel::title() const { + return m_title; +} + +void ImageSqlModel::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 ImageSqlModel::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 ImageSqlModel::filePath() const { + return m_filePath; +} + +void ImageSqlModel::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 ImageSqlModel::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(); +} + +QUrl ImageSqlModel::getImage(const int &row) { + qDebug() << "Row we are getting is " << row; + QUrl image; + QSqlRecord rec = record(row); + qDebug() << rec.value("filePath").toUrl(); + // image.append(rec.value("title")); + // image.append(rec.value("filePath")); + image = rec.value("filePath").toUrl(); + return image; +} diff --git a/src/imagesqlmodel.h b/src/imagesqlmodel.h new file mode 100644 index 0000000..461ba8a --- /dev/null +++ b/src/imagesqlmodel.h @@ -0,0 +1,49 @@ +#ifndef IMAGESQLMODEL_H +#define IMAGESQLMODEL_H + +#include +#include +#include +#include +#include +#include + +class ImageSqlModel : 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: + ImageSqlModel(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 newImage(const QUrl &filePath); + Q_INVOKABLE void deleteImage(const int &row); + Q_INVOKABLE QUrl getImage(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 //IMAGESQLMODEL_H diff --git a/src/main.cpp b/src/main.cpp index 06a8365..845fdc3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -37,6 +37,7 @@ #include "serviceitemmodel.h" #include "songsqlmodel.h" #include "videosqlmodel.h" +#include "imagesqlmodel.h" static void connectToDatabase() { // let's setup our sql database @@ -98,6 +99,7 @@ int main(int argc, char *argv[]) //register our models qmlRegisterType("org.presenter", 1, 0, "SongSqlModel"); qmlRegisterType("org.presenter", 1, 0, "VideoSqlModel"); + qmlRegisterType("org.presenter", 1, 0, "ImageSqlModel"); qmlRegisterType("org.presenter", 1, 0, "ServiceItemModel"); connectToDatabase(); diff --git a/src/qml/presenter/ImageEditor.qml b/src/qml/presenter/ImageEditor.qml index 4dea612..8da66af 100644 --- a/src/qml/presenter/ImageEditor.qml +++ b/src/qml/presenter/ImageEditor.qml @@ -8,6 +8,9 @@ import "./" as Presenter Item { id: root + property string type: "image" + property var image + GridLayout { id: mainLayout anchors.fill: parent @@ -83,15 +86,15 @@ Item { border.color: Kirigami.Theme.activeBackgroundColor border.width: 2 } - closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent + closePolicy: Controls.Popup.CloseOnEscape | Controls.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() + text: "Image" + icon.name: "emblem-images-symbolic" + onClicked: imageFileDialog.open() & backgroundType.close() } Controls.ToolButton { Layout.fillWidth: true @@ -121,11 +124,11 @@ Item { ColumnLayout { Controls.SplitView.fillHeight: true - Controls.SplitView.preferredWidth: 500 - Controls.SplitView.minimumWidth: 500 + Controls.SplitView.preferredWidth: 300 + Controls.SplitView.minimumWidth: 100 Controls.TextField { - id: songTitleField + id: imageTitleField Layout.preferredWidth: 300 Layout.fillWidth: true @@ -133,98 +136,46 @@ Item { Layout.rightMargin: 20 placeholderText: "Song Title..." - text: songTitle + text: "idk" 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); + /* onEditingFinished: updateTitle(text); */ } - Controls.ScrollView { - id: songLyricsField - - Layout.preferredHeight: 3000 - Layout.fillWidth: true + Item { + id: empty 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 + spacing: 5 - Rectangle { - id: slideBar - color: Kirigami.Theme.highlightColor - - Layout.preferredWidth: 500 - Layout.preferredHeight: songTitleField.height - Layout.rightMargin: 20 - Layout.leftMargin: 20 + Item { + id: topEmpty + Layout.fillHeight: true } - 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 + Image { + id: imagePreview + Layout.preferredWidth: 600 + Layout.preferredHeight: Layout.preferredWidth / 16 * 9 + Layout.alignment: Qt.AlignCenter + fillMode: Image.PreserveAspectFit + source: "file://" + image.toString() + } + Item { + id: botEmpty + Layout.fillHeight: true } } + } } + function changeImage(image) { + root.image = image; + print(image.toString()); } - Timer { - id: editorTimer - interval: 1000 - repeat: true - running: false - onTriggered: updateLyrics(lyricsEditor.text) - } } diff --git a/src/qml/presenter/Library.qml b/src/qml/presenter/Library.qml index 1607b08..4414e26 100644 --- a/src/qml/presenter/Library.qml +++ b/src/qml/presenter/Library.qml @@ -485,12 +485,40 @@ Item { Layout.fillWidth: true Layout.alignment: Qt.AlignTop color: Kirigami.Theme.backgroundColor + opacity: 1.0 Controls.Label { + id: imageLabel anchors.centerIn: parent text: "Images" } + Controls.Label { + id: imageCount + anchors {left: imageLabel.right + verticalCenter: imageLabel.verticalCenter + leftMargin: 15} + text: imagesqlmodel.rowCount() + font.pixelSize: 15 + color: Kirigami.Theme.disabledTextColor + } + + Kirigami.Icon { + id: imageDrawerArrow + anchors {right: parent.right + verticalCenter: imageCount.verticalCenter + rightMargin: 10} + source: "arrow-down" + rotation: selectedLibrary == "images" ? 0 : 180 + + Behavior on rotation { + NumberAnimation { + easing.type: Easing.OutCubic + duration: 300 + } + } + } + MouseArea { anchors.fill: parent onClicked: { @@ -508,6 +536,9 @@ Item { Layout.preferredHeight: parent.height - 200 Layout.fillWidth: true Layout.alignment: Qt.AlignTop + model: imagesqlmodel + delegate: imageDelegate + clip: true state: "deselected" states: [ @@ -534,6 +565,118 @@ Item { duration: 300 } } + + Component { + id: imageDelegate + Item{ + implicitWidth: ListView.view.width + height: selectedLibrary == "images" ? 50 : 0 + Kirigami.BasicListItem { + id: imageListItem + + property bool rightMenu: false + + implicitWidth: imageLibraryList.width + height: selectedLibrary == "images" ? 50 : 0 + clip: true + label: title + /* subtitle: author */ + supportsMouseEvents: false + backgroundColor: { + if (parent.ListView.isCurrentItem) { + Kirigami.Theme.highlightColor; + } else if (imageDragHandler.containsMouse){ + Kirigami.Theme.highlightColor; + } else { + Kirigami.Theme.backgroundColor; + } + } + textColor: { + if (parent.ListView.isCurrentItem || imageDragHandler.containsMouse) + activeTextColor; + else + Kirigami.Theme.textColor; + } + + Behavior on height { + NumberAnimation { + easing.type: Easing.OutCubic + duration: 300 + } + } + Drag.active: imageDragHandler.drag.active + Drag.hotSpot.x: width / 2 + Drag.hotSpot.y: height / 2 + Drag.keys: [ "library" ] + + states: State { + name: "dragged" + when: imageListItem.Drag.active + PropertyChanges { + target: imageListItem + x: x + y: y + width: width + height: height + } + ParentChange { + target: imageListItem + parent: rootApp.overlay + } + } + + } + + MouseArea { + id: imageDragHandler + anchors.fill: parent + hoverEnabled: true + drag { + target: imageListItem + onActiveChanged: { + if (imageDragHandler.drag.active) { + dragItemTitle = title; + dragItemType = "image"; + dragItemText = ""; + dragItemBackgroundType = "image"; + dragItemBackground = filePath; + } else { + imageListItem.Drag.drop() + } + } + filterChildren: true + threshold: 10 + } + MouseArea { + id: imageClickHandler + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + onClicked: { + if(mouse.button == Qt.RightButton) + rightClickImageMenu.popup() + else{ + imageLibraryList.currentIndex = index + const image = imagesqlmodel.getImage(imageLibraryList.currentIndex); + if (!editMode) + editMode = true; + editType = "image"; + editSwitch(image); + } + } + + } + } + Controls.Menu { + id: rightClickImageMenu + x: imageClickHandler.mouseX + y: imageClickHandler.mouseY + 10 + Kirigami.Action { + text: "delete" + onTriggered: imagesqlmodel.deleteImage(index) + } + } + } + } } Rectangle { diff --git a/src/qml/presenter/MainWindow.qml b/src/qml/presenter/MainWindow.qml index 1605785..2124e91 100644 --- a/src/qml/presenter/MainWindow.qml +++ b/src/qml/presenter/MainWindow.qml @@ -66,17 +66,24 @@ Controls.Page { id: presentation anchors.fill: parent } + Presenter.SongEditor { id: songEditor visible: false anchors.fill: parent } - + Presenter.VideoEditor { id: videoEditor visible: false anchors.fill: parent } + + Presenter.ImageEditor { + id: imageEditor + visible: false + anchors.fill: parent + } } Presenter.Library { @@ -102,6 +109,10 @@ Controls.Page { id: videosqlmodel } + ImageSqlModel { + id: imagesqlmodel + } + ServiceItemModel { id: serviceItemModel } @@ -141,19 +152,24 @@ Controls.Page { presentation.visible = false; videoEditor.visible = false; videoEditor.stop(); + imageEditor.visible = false; songEditor.visible = true; songEditor.changeSong(item); break; case "video" : presentation.visible = false; songEditor.visible = false; + imageEditor.visible = false; videoEditor.visible = true; videoEditor.changeVideo(item); break; case "image" : - mainPageArea.pop(Controls.StackView.Immediate); - mainPageArea.push(imageEditorComp, Controls.StackView.Immediate); + presentation.visible = false; + videoEditor.visible = false; videoEditor.stop(); + songEditor.visible = false; + imageEditor.visible = true; + imageEditor.changeImage(item); break; default: videoEditor.visible = false; diff --git a/src/serviceitemmodel.cpp b/src/serviceitemmodel.cpp index 050e3ca..1bcebe9 100644 --- a/src/serviceitemmodel.cpp +++ b/src/serviceitemmodel.cpp @@ -116,15 +116,12 @@ Qt::ItemFlags ServiceItemModel::flags(const QModelIndex &index) const { void ServiceItemModel::addItem(ServiceItem *item) { const int index = m_items.size(); - qDebug() << index; + // qDebug() << index; // foreach (item, m_items) { // qDebug() << item; // } beginInsertRows(QModelIndex(), index, index); m_items.append(item); - foreach (item, m_items) { - qDebug() << item; - } endInsertRows(); } diff --git a/src/songsqlmodel.cpp b/src/songsqlmodel.cpp index 0d105af..8087077 100644 --- a/src/songsqlmodel.cpp +++ b/src/songsqlmodel.cpp @@ -39,14 +39,14 @@ static void createTable() qFatal("Failed to query database: %s", qPrintable(query.lastError().text())); } - qDebug() << query.lastQuery(); - qDebug() << "inserting into songs"; + // qDebug() << query.lastQuery(); + // qDebug() << "inserting into songs"; query.exec( "INSERT INTO songs (title, lyrics, author, ccli, audio, vorder, " "background, backgroundType, textAlignment) VALUES ('10,000 Reasons', '10,000 reasons " "for my heart to sing', 'Matt Redman', '13470183', '', '', '', '', 'center')"); - qDebug() << query.lastQuery(); + // qDebug() << query.lastQuery(); query.exec("INSERT INTO songs (title, lyrics, author, ccli, audio, vorder, " "background, backgroundType, textAlignment) VALUES ('River', 'Im going down to " "the river', 'Jordan Feliz', '13470183', '', '', '', '', 'center')"); @@ -55,15 +55,15 @@ static void createTable() "background, backgroundType, textAlignment) VALUES ('Marvelous Light', 'Into marvelous " "light Im running', 'Chris Tomlin', '13470183', '', '', '', '', 'center')"); - qDebug() << query.lastQuery(); + // qDebug() << query.lastQuery(); query.exec("select * from songs"); - qDebug() << query.lastQuery(); + // qDebug() << query.lastQuery(); } SongSqlModel::SongSqlModel(QObject *parent) : QSqlTableModel(parent) { - qDebug() << "creating table"; + // qDebug() << "creating table"; createTable(); setTable(songsTableName); setEditStrategy(QSqlTableModel::OnManualSubmit); diff --git a/src/videosqlmodel.h b/src/videosqlmodel.h index be269a7..f36911f 100644 --- a/src/videosqlmodel.h +++ b/src/videosqlmodel.h @@ -46,4 +46,4 @@ private: QUrl m_filePath; }; -#endif //SONGSQLMODEL_H +#endif //VIDEOSQLMODEL_H