diff --git a/.gitignore b/.gitignore index 790be03..2ec412e 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ unknownsys_build/* win32_build/* unix_build/* mac_build/* +/src/build +/src/win32_build diff --git a/3rdparty/headers/qmediaplaylist.h b/3rdparty/headers/qmediaplaylist.h index 8342712..1205332 100644 --- a/3rdparty/headers/qmediaplaylist.h +++ b/3rdparty/headers/qmediaplaylist.h @@ -6,7 +6,7 @@ #include -#include +// #include QT_BEGIN_NAMESPACE @@ -89,7 +89,7 @@ public slots: QT_END_NAMESPACE -Q_MEDIA_ENUM_DEBUG(QMediaPlaylist, PlaybackMode) -Q_MEDIA_ENUM_DEBUG(QMediaPlaylist, Error) +// Q_MEDIA_ENUM_DEBUG(QMediaPlaylist, PlaybackMode) +// Q_MEDIA_ENUM_DEBUG(QMediaPlaylist, Error) #endif // QMEDIAPLAYLIST_H diff --git a/3rdparty/sources/qmediaplaylist.cpp b/3rdparty/sources/qmediaplaylist.cpp index 584b4c7..2012967 100644 --- a/3rdparty/sources/qmediaplaylist.cpp +++ b/3rdparty/sources/qmediaplaylist.cpp @@ -1,9 +1,9 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only -#include "../include/qmediaplaylist.h" -#include "../include/qmediaplaylist_p.h" -#include "qplaylistfileparser.h" +#include "../headers/qmediaplaylist.h" +#include "../headers/qmediaplaylist_p.h" +#include "../headers/qplaylistfileparser.h" #include #include diff --git a/3rdparty/sources/qmediaplaylist_p.cpp b/3rdparty/sources/qmediaplaylist_p.cpp index 47765f0..a0c1edd 100644 --- a/3rdparty/sources/qmediaplaylist_p.cpp +++ b/3rdparty/sources/qmediaplaylist_p.cpp @@ -1,7 +1,7 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only -#include "../include/qmediaplaylist_p.h" +#include "../headers/qmediaplaylist_p.h" QT_BEGIN_NAMESPACE diff --git a/3rdparty/sources/qplaylistfileparser.cpp b/3rdparty/sources/qplaylistfileparser.cpp index 0a0be9d..6c3d6ea 100644 --- a/3rdparty/sources/qplaylistfileparser.cpp +++ b/3rdparty/sources/qplaylistfileparser.cpp @@ -3,7 +3,7 @@ #include "qmediametadata.h" #include "qmediaplayer.h" -#include "qplaylistfileparser.h" +#include "../headers/qplaylistfileparser.h" #include #include diff --git a/README.md b/README.md index f14670b..2cb34b9 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,21 @@ ``` .\windeployqt.exe --qmldir "" "" ``` + +#### A version of SoftProjector which uses HTTP pages for bible/song displays instead of Windows forms. +If you've ever had your mouse or a Windows application open up on the projector during live service, and everyone seeing, then you know the downsides of projecting a computer display. This version of softprojector outputs bible & songs as a webpage, which allows you to add them as scene in OBS. From there you can send the OBS' program mix to Decklink Ouput into your video switcher. + +This http implementation opened up different style options of displaying bible text and lyrics. By editing the httpserver.html or running a local file copy, any amount of computers can show and style bible and songs however wanted. Your stream PC may want to show just the primary bible in a lower-thirds style for livestream, and your projection pc output shows parallel. + +Furthermore, this now allowed me to add features such as song verse split, where one screen can show the full verse and another shows 2 lines at a time. Perfect for readability on livestream. + +Lastly, for us multilinguals, I also added parallel song verse translations which works alongside the verse split feature. This is great because only one person/computer is needed to show different lyrics. + +#### To use this, configure your local IP in the settings +Add a browser source in OBS, and then style it using the css setting (example css in :/httpserver.html). Alternatively, copy the .html file to your local machine and edit it as you like. +![tempsnip](https://github.com/SoftProjector/softprojector/assets/23706821/816e8742-cbf1-4967-b50e-5ee511d69a4f) +#### To use the song verse split, check the 'Split Verse Text' checkbox above the song preview list. +When you go live, the split list will show below the full verses. Simply use your keyboard arrows to navigate. +![267142147-35343950-036b-433e-bbba-28600d464c01](https://github.com/user-attachments/assets/8c24ea6c-ea59-4b38-8b7d-36c22f68e7a1) +#### For parallel verses, add the translation to your song using '=' +![vers-tra](https://github.com/user-attachments/assets/5e2d0975-2fb9-4d0a-be0f-17471f30f009) diff --git a/src/headers/editwidget.hpp b/src/headers/editwidget.hpp index 239fb9b..157ae1b 100644 --- a/src/headers/editwidget.hpp +++ b/src/headers/editwidget.hpp @@ -69,6 +69,7 @@ public slots: private slots: void addNewSong(Song song, QString msgNewSongbook, QString msgCaption); void addSongbook(); + bool validateSong(); void on_btnCancel_clicked(); void on_btnSave_clicked(); void resetUiItems(); diff --git a/src/headers/highlight.hpp b/src/headers/highlight.hpp index e1a8773..4af431a 100644 --- a/src/headers/highlight.hpp +++ b/src/headers/highlight.hpp @@ -43,7 +43,7 @@ class Highlight : public QSyntaxHighlighter }; QVector highlightingRules; - QTextCharFormat verseFormat, chorusFormat, vstavkaFormat; + QTextCharFormat verseFormat, chorusFormat, vstavkaFormat, translationFormat; }; // *** Announcement editor highlighter diff --git a/src/headers/httpserver.hpp b/src/headers/httpserver.hpp new file mode 100644 index 0000000..c640111 --- /dev/null +++ b/src/headers/httpserver.hpp @@ -0,0 +1,53 @@ +/*************************************************************************** +// +// softProjector - an open source media projection software +// Copyright (C) 2022 Vladislav Kobzar +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 3 of the License. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +***************************************************************************/ + +#include + +// Simple HTTP server implementation in Qt taken from https://stackoverflow.com/a/32931195 + +#ifndef HTTPSERVER_H +#define HTTPSERVER_H + +class HttpServer : public QObject +{ + Q_OBJECT + +public: + HttpServer(); + void startServer(QString httpServerIPAddress, quint16 httpServerPort, quint16 webSocketServerPort); + void stopServer(); + bool isRunning; + +public slots: + void startConnection(); + void sendData(); + void closingClient(); + +private: + QTcpServer *server = new QTcpServer(); + QString ipAddress; + quint16 httpPort; + quint16 webSocketPort; + +private slots: + +signals: +}; + +#endif diff --git a/src/headers/settings.hpp b/src/headers/settings.hpp index 8e8fa54..1a14667 100644 --- a/src/headers/settings.hpp +++ b/src/headers/settings.hpp @@ -309,6 +309,11 @@ class GeneralSettings bool settingsChangedAll; bool settingsChangedMulti; bool settingsChangedSingle; + bool httpServerEnabled; + QString httpServerIPAddress; + int webSocketServerPort; + int httpServerPort; + bool disableScreens; }; class DisplaySettings diff --git a/src/headers/settingsdialog.hpp b/src/headers/settingsdialog.hpp index 479d86a..d0adcf0 100644 --- a/src/headers/settingsdialog.hpp +++ b/src/headers/settingsdialog.hpp @@ -54,6 +54,7 @@ public slots: BibleVersionSettings& bsets3, BibleVersionSettings& bsets4); void positionsDisplayWindow(); void updateScreen(); + void httpServerState(bool& state, QString& httpServerIPAddress, int& httpServerPort, int& webSocketServerPort); private: Ui::SettingsDialog *ui; @@ -64,6 +65,11 @@ public slots: int currentDisplayScreen4; bool is_always_on_top; + bool currentHTTPEnabled; + QString currentHTTPServerIPAddress; + int currentHTTPPort; + int currentWebSocketPort; + GeneralSettings gsettings; Theme theme; BibleVersionSettings bsettings; diff --git a/src/headers/softprojector.hpp b/src/headers/softprojector.hpp index 3ab64e9..67ccd4d 100644 --- a/src/headers/softprojector.hpp +++ b/src/headers/softprojector.hpp @@ -40,6 +40,8 @@ #include "videoinfo.hpp" #include "slideshoweditor.hpp" #include "schedule.hpp" +#include "httpserver.hpp" +#include "websocketserver.hpp" class QActionGroup; @@ -76,10 +78,16 @@ class SoftProjector : public QMainWindow PictureWidget *pictureWidget; MediaWidget *mediaPlayer; MediaControl *mediaControls; + WebSocketServer *webSocketServer; + HttpServer *httpServer; bool showing; // whether we are currently showing to the projector Song current_song; int current_song_verse; + QStringList currentSongVerses; // List of song verse blocks. + QStringList currentSongTranslatedVerses; // List of translation-only verse blocks. + QStringList currentSongVerseSplitList; // List of the current verse split into lines. + QStringList currentSongTranslatedVerseSplitList; // List of the current translation-only verse split into lines. Verse current_verse; Announcement currentAnnounce; QString version_string; @@ -95,6 +103,7 @@ public slots: void saveSettings(); void positionDisplayWindow(); void updateScreen(); + void httpServerState(bool &state, QString &httpServerIPAddress, int &httpServerPort, int &webSocketServerPort); void setWaitCursor(); void setArrowCursor(); @@ -139,6 +148,7 @@ public slots: QList schedule; QDir appDataDir; + bool songSplitVerse; private slots: void showDisplayScreen(bool show); @@ -250,6 +260,15 @@ private slots: void on_actionCloseDisplay_triggered(); void updateCloseDisplayButtons(bool isOn); + void sendBibleVerseToServer(); + void sendSongVerseToServer(); + void showSongVerseSplit(bool enabled); + void sendSongVerseSplit(); + QStringList splitSongVerse(QString verse); + void setSongVerseSplitList(int currentVerseIndex, QString selectRow = "first"); + void on_songVerseSplitListWidget_itemSelectionChanged(); + void on_songVerseSplitListWidget_itemDoubleClicked(QListWidgetItem *item); + void on_songVerseSplitListWidgetNavigation(Qt::Key); protected: void closeEvent(QCloseEvent *event); void keyPressEvent(QKeyEvent *event); diff --git a/src/headers/song.hpp b/src/headers/song.hpp index 1a85092..1226de0 100644 --- a/src/headers/song.hpp +++ b/src/headers/song.hpp @@ -30,6 +30,7 @@ bool isStanzaAndVerseTitle(QString string); bool isStanzaRefrainTitle(QString string); bool isStanzaAndRefrainTitle(QString string); bool isStanzaSlideTitle(QString string); +QString getStanzaBlock(int &i, QStringList &list, bool includeTranslation = false); class Stanza { @@ -68,7 +69,7 @@ class Song void readData(); void saveUpdate(); void saveNew(); - QStringList getSongTextList(); + QStringList getSongTextList(bool includeTranslation = false); Stanza getStanza(int current); QString getSongbookName(); bool isValid(); @@ -86,6 +87,7 @@ class Song QString musicBy; QString songText; QString notes; + bool useTranslation; // Show verse translations or not. bool usePrivateSettings; int alignmentV; int alignmentH; @@ -101,7 +103,6 @@ class Song private: void setDefaults(); - QString getStanzaBlock(int &i, QStringList &list); void removeLastChorus(QStringList ct, QStringList &list); }; diff --git a/src/headers/songwidget.hpp b/src/headers/songwidget.hpp index 6c68eff..a988ee8 100644 --- a/src/headers/songwidget.hpp +++ b/src/headers/songwidget.hpp @@ -62,6 +62,7 @@ public slots: void setArrowCursor(); void sendSong(Song song, int currentItem); void addToSchedule(Song &song); + void enableSongSplitVerse(bool enabled); private slots: void on_comboBoxCategory_currentIndexChanged(int index); @@ -83,6 +84,8 @@ private slots: void on_pushButtonClearResults_clicked(); void on_comboBoxFilterType_currentIndexChanged(int index); + void on_enableSplitVerseCheckBox_toggled(bool checked); + private: Ui::SongWidget *ui; QString songbook; @@ -99,6 +102,9 @@ private slots: QList cat_ids; QList allSongs; HighlighterDelegate *highlight; + QTimer *searchTimer; + QString searchText; + void processSearch(); }; #endif // SONGWIDGET_HPP diff --git a/src/headers/websocketserver.hpp b/src/headers/websocketserver.hpp new file mode 100644 index 0000000..2877fa9 --- /dev/null +++ b/src/headers/websocketserver.hpp @@ -0,0 +1,74 @@ +/*************************************************************************** +// +// softProjector - an open source media projection software +// Copyright (C) 2022 Vladislav Kobzar +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 3 of the License. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +***************************************************************************/ + +#ifndef WEBSOCKETSERVER_HPP +#define WEBSOCKETSERVER_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "bible.hpp" +#include "song.hpp" +#include "announcement.hpp" + +// WebSocket implementation taken from https://code.qt.io/cgit/qt/qtwebsockets.git/tree/examples/websockets?h=5.15 + +QT_FORWARD_DECLARE_CLASS(QWebSocketServer) +QT_FORWARD_DECLARE_CLASS(QWebSocket) + +class WebSocketServer : public QObject +{ + Q_OBJECT + +public: + WebSocketServer(); + void startServer(QString httpServerIPAddress, quint16 port); + void stopServer(); + void setBibleText(Verse verse); + void setSongText(QString verse, QString verseTranslation = "", QString splitVerse = "", QString splitVerseTranslation = "", bool isLast = false); + void setBlank(); + bool isRunning; + +public slots: + void closingClient(); + +private: + QWebSocketServer *server; + QList clients; + qint64 bytesAvailable() const; + QJsonObject jsonOutput; + void establishConnection(); + bool servingConnection; + void sendToClients(); + void onNewConnection(); + void socketDisconnected(); + +signals: + void closed(); +}; + +#endif diff --git a/src/html/httpserver.html b/src/html/httpserver.html new file mode 100644 index 0000000..64afe8a --- /dev/null +++ b/src/html/httpserver.html @@ -0,0 +1,530 @@ + + + + + + + + + + + + + + +
+
+

    

+

  

+
+
+

    

+

  

+
+
+
+

+ +
+
+
+

+
+
+

+
+ + + + + + + diff --git a/src/qml/DisplayArea.qml b/src/qml/DisplayArea.qml index 5529392..bc327a1 100644 --- a/src/qml/DisplayArea.qml +++ b/src/qml/DisplayArea.qml @@ -19,6 +19,7 @@ import QtQuick import QtMultimedia +import QtWebSockets Rectangle { id: dispArea diff --git a/src/softProjector.pro b/src/softProjector.pro index dc6a26f..163f5ac 100644 --- a/src/softProjector.pro +++ b/src/softProjector.pro @@ -27,7 +27,8 @@ QT += core \ quick \ printsupport \ multimedia \ - multimediawidgets + multimediawidgets \ + websockets TARGET = SoftProjector TEMPLATE = app @@ -46,6 +47,9 @@ RCC_DIR = $${RES_DIR}/rcc OUT_PWD = $${RES_DIR}/bin SOURCES += sources/main.cpp \ + ../3rdparty/sources/qmediaplaylist.cpp \ + ../3rdparty/sources/qmediaplaylist_p.cpp \ + ../3rdparty/sources/qplaylistfileparser.cpp \ sources/softprojector.cpp \ sources/songwidget.cpp \ sources/biblewidget.cpp \ @@ -88,8 +92,13 @@ SOURCES += sources/main.cpp \ sources/projectordisplayscreen.cpp \ sources/imagegenerator.cpp \ sources/spimageprovider.cpp \ - sources/mediacontrol.cpp + sources/mediacontrol.cpp \ + sources/httpserver.cpp \ + sources/websocketserver.cpp HEADERS += headers/softprojector.hpp \ + ../3rdparty/headers/qmediaplaylist.h \ + ../3rdparty/headers/qmediaplaylist_p.h \ + ../3rdparty/headers/qplaylistfileparser.h \ headers/songwidget.hpp \ headers/biblewidget.hpp \ headers/editwidget.hpp \ @@ -131,7 +140,9 @@ HEADERS += headers/softprojector.hpp \ headers/projectordisplayscreen.hpp \ headers/imagegenerator.hpp \ headers/spimageprovider.hpp \ - headers/mediacontrol.hpp + headers/mediacontrol.hpp \ + headers/httpserver.hpp \ + headers/websocketserver.hpp FORMS += ui/softprojector.ui \ ui/songwidget.ui \ ui/biblewidget.ui \ diff --git a/src/softprojector.qrc b/src/softprojector.qrc index 20077a9..ab67d15 100644 --- a/src/softprojector.qrc +++ b/src/softprojector.qrc @@ -1,4 +1,4 @@ - + icons/effectsBlurredShadow.png icons/effectsNone.png @@ -103,4 +103,7 @@ qml/DisplayArea.qml + + html/httpserver.html + diff --git a/src/sources/biblewidget.cpp b/src/sources/biblewidget.cpp index 8858a37..6554e19 100644 --- a/src/sources/biblewidget.cpp +++ b/src/sources/biblewidget.cpp @@ -195,6 +195,11 @@ void BibleWidget::sendToProjector(bool add_to_history) void BibleWidget::on_lineEditBook_textChanged(QString text) { + if (text == "0") { + ui->lineEditBook->clear(); + return; + } + // Called when the bible book filter field is modified. QStringList all_books = bible.getBooks(); @@ -376,7 +381,7 @@ void BibleWidget::on_search_button_clicked() if (!search_results.isEmpty()) // If have results, then show them { - if( not ui->result_label->isVisible() ) + if( ! ui->result_label->isVisible() ) { ui->lineEditBook->clear(); hidden_splitter_state = ui->results_splitter->saveState(); diff --git a/src/sources/editwidget.cpp b/src/sources/editwidget.cpp index ce24db9..4f4b3a2 100644 --- a/src/sources/editwidget.cpp +++ b/src/sources/editwidget.cpp @@ -56,7 +56,7 @@ void EditWidget::changeEvent(QEvent *e) } } -void EditWidget::on_btnSave_clicked() +bool EditWidget::validateSong() { // Check if song title exists. A song title MUST exits QString song_title = ui->lineEditTitle->text(); @@ -70,10 +70,50 @@ void EditWidget::on_btnSave_clicked() mb.setIcon(QMessageBox::Warning); mb.exec(); ui->lineEditTitle->setFocus(); + return false; + } + + // Validate translation lyrics line count matches original. + // Must be same for showing split verse. + QStringList lyricLines = ui->textEditSong->toPlainText().trimmed().split("\n"); + QStringList lyrics; // Create the verse blocks. + for (int i = 0; i < lyricLines.length(); i++) { + if (isStanzaTitle(lyricLines[i])) { + lyrics.append(getStanzaBlock(i, lyricLines, true)); + } + } + + for (const QString &verse : qAsConst(lyrics)) { + if (!verse.contains("=")) { continue; } + + QStringList split = verse.split("="); + if (split.length() == 2) { + // Validate amount of lines. + QStringList original = split[0].split("\n"); + original.removeFirst(); // Remove the "Verse/Chorus" titles. + QStringList translation = split[1].split("\n"); + if (original.count() == translation.count()) { + continue; + } + } + QMessageBox mb(this); + mb.setText(tr("The translated text must have the same amount of lines as the original. Must have same amount of lines between '='")); + mb.setWindowTitle(tr("Validation error")); + mb.setIcon(QMessageBox::Warning); + mb.exec(); + return false; + } + + return true; +} + +void EditWidget::on_btnSave_clicked() +{ + if (!validateSong()) { return; } - setSave(); + setSave(); setWaitCursor(); if (is_new) { @@ -150,6 +190,7 @@ void EditWidget::resetUiItems() ui->lineEditKey->setText(ss.tune); ui->comboBoxCategory->setCurrentIndex(cat_ids.indexOf(ss.category)); ui->checkBoxSongSettings->setChecked(ss.usePrivateSettings); + ui->checkBoxUseTranslation->setChecked(ss.useTranslation); ui->groupBoxSettings->setVisible(ss.usePrivateSettings); ui->comboBoxVAlignment->setCurrentIndex(ss.alignmentV); ui->comboBoxHAlignment->setCurrentIndex(ss.alignmentH); @@ -175,6 +216,7 @@ void EditWidget::setUiItems() ui->lineEditKey->setText(editSong.tune); ui->comboBoxCategory->setCurrentIndex(cat_ids.indexOf(editSong.category)); setSongbook(editSong.songID); + ui->checkBoxUseTranslation->setChecked(editSong.useTranslation); ui->checkBoxSongSettings->setChecked(editSong.usePrivateSettings); ui->groupBoxSettings->setVisible(editSong.usePrivateSettings); ui->comboBoxVAlignment->setCurrentIndex(editSong.alignmentV); @@ -201,10 +243,10 @@ void EditWidget::setSave(){ newSong.tune = ui->lineEditKey->text(); newSong.wordsBy = ui->lineEditWordsBy->text(); newSong.musicBy = ui->lineEditMusicBy->text(); -// newSong.songText = resetLyric(ui->textEditSong->toPlainText()); newSong.songText = ui->textEditSong->toPlainText().trimmed(); newSong.alignmentV = ui->comboBoxVAlignment->currentIndex(); newSong.alignmentH = ui->comboBoxHAlignment->currentIndex(); + newSong.useTranslation = ui->checkBoxUseTranslation->isChecked(); newSong.usePrivateSettings = ui->checkBoxSongSettings->isChecked(); newSong.useBackground = ui->checkBoxUseBackground->isChecked(); newSong.backgroundName = ui->lineEditBackgroundPath->text(); @@ -417,34 +459,13 @@ void EditWidget::loadCategories(bool ui_update) int EditWidget::isInDatabase(Song *song) { - QString s_title(""), s_id("0"), sb_id("0"); QSqlQuery sq; - - // check if song is part of songbook - sq.exec("SELECT id FROM Songbooks WHERE name = '" + song->songbook_name + "'"); - while(sq.next()) - sb_id = sq.value(0).toString().trimmed(); - sq.clear(); - if(sb_id == "0") - return 0; // no such songbook in database - - // get song id - sq.exec("SELECT id, title from Songs WHERE songbook_id = '" + sb_id +"' AND number = '" + QString::number(song->number) +"'"); - while(sq.next()) - { - s_id = sq.value(0).toString().trimmed(); - s_title = sq.value(1).toString().trimmed(); + sq.exec("SELECT id from Songs WHERE id = " + QString::number(song->songID)); + if (sq.first()) { + int s_id = sq.value(0).toInt(); + return s_id; } - sq.clear(); - if(s_id == "0") - return 0; // no matching song - song->songID = s_id.toInt(); - - // get song title - if(s_title!=song->title.trimmed()) - return 0; - else - return s_id.toInt(); + return 0; } void EditWidget::on_checkBoxSongSettings_toggled(bool checked) diff --git a/src/sources/generalsettingwidget.cpp b/src/sources/generalsettingwidget.cpp index 4fbd65e..eeaab80 100644 --- a/src/sources/generalsettingwidget.cpp +++ b/src/sources/generalsettingwidget.cpp @@ -157,6 +157,13 @@ void GeneralSettingWidget::loadSettings() ui->comboBoxControlsAlignV->setCurrentIndex(mySettings.displayControls.alignmentV); ui->comboBoxControlsAlignH->setCurrentIndex(mySettings.displayControls.alignmentH); ui->horizontalSliderOpacity->setValue(mySettings.displayControls.opacity*100); + + // HTTP server + ui->checkBoxEnableHttpServer->setChecked(mySettings.httpServerEnabled); + ui->lineEditHTTPServerIPAddress->setText(mySettings.httpServerIPAddress); + ui->spinBoxHttpPort->setValue(mySettings.httpServerPort); + ui->spinBoxWebSocketPort->setValue(mySettings.webSocketServerPort); + ui->checkBoxDisableScreens->setChecked(mySettings.disableScreens); } void GeneralSettingWidget::loadThemes() @@ -198,6 +205,12 @@ GeneralSettings GeneralSettingWidget::getSettings() r = r/100; mySettings.displayControls.opacity = r; + mySettings.httpServerEnabled = ui->checkBoxEnableHttpServer->isChecked(); + mySettings.httpServerIPAddress = ui->lineEditHTTPServerIPAddress->text(); + mySettings.httpServerPort = ui->spinBoxHttpPort->value(); + mySettings.webSocketServerPort = ui->spinBoxWebSocketPort->value(); + mySettings.disableScreens = ui->checkBoxDisableScreens->isChecked(); + return mySettings; } diff --git a/src/sources/highlight.cpp b/src/sources/highlight.cpp index 6904a1d..fc85ea8 100644 --- a/src/sources/highlight.cpp +++ b/src/sources/highlight.cpp @@ -78,6 +78,13 @@ Highlight::Highlight(QTextDocument *parent) rule.format = vstavkaFormat; highlightingRules.append(rule); } + + // Translation formating + translationFormat.setForeground(Qt::black); + translationFormat.setBackground(Qt::green); + rule.pattern = QRegularExpression("^=[^\n]*"); + rule.format = translationFormat; + highlightingRules.append(rule); } void Highlight::highlightBlock(const QString &text) diff --git a/src/sources/httpserver.cpp b/src/sources/httpserver.cpp new file mode 100644 index 0000000..079cab2 --- /dev/null +++ b/src/sources/httpserver.cpp @@ -0,0 +1,92 @@ +/*************************************************************************** +// +// softProjector - an open source media projection software +// Copyright (C) 2022 Vladislav Kobzar +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 3 of the License. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +***************************************************************************/ + +#include "../headers/httpserver.hpp" + +#include +#include +#include + +HttpServer::HttpServer() { } + +void HttpServer::startServer(QString httpServerIPAddress, quint16 httpServerPort, quint16 webSocketServerPort) +{ + ipAddress = httpServerIPAddress; + httpPort = httpServerPort; + webSocketPort = webSocketServerPort; + server = new QTcpServer(); + + // waiting for the web brower to make contact, this will emit signal + connect(server, SIGNAL(newConnection()), this, SLOT(startConnection())); + + if(server->listen(QHostAddress(ipAddress), httpPort)) { + isRunning = true; + qDebug() << "HTTP server listening on " << ipAddress << ":" << httpPort; + } else { + qDebug() << "HTTP server failed listening on " << ipAddress << ":" << httpPort; + } +} + +void HttpServer::startConnection() +{ + QTcpSocket *socket = server->nextPendingConnection(); + connect(socket, SIGNAL(readyRead()), this, SLOT(sendData())); + connect(socket, SIGNAL(disconnected()), this, SLOT(closingClient())); +} + +void HttpServer::closingClient() +{ + QTcpSocket* socket = (QTcpSocket*)sender(); + socket->deleteLater(); +} + +void HttpServer::sendData() +{ + QTcpSocket* socket = (QTcpSocket*)sender(); + + QFile file(":/html/html/httpserver.html"); + if(!file.open(QFile::ReadOnly | + QFile::Text)) + { + qDebug() << "Could not open httpserver.html for reading"; + return; + } + + QTextStream in(&file); + QString html = in.readAll(); + file.close(); + + html.replace("${ipAddress}", ipAddress); + html.replace("${webSocketPort}", QString::number(webSocketPort)); + + socket->write("HTTP/1.1 200 OK\r\n"); // \r needs to be before \n + socket->write("Content-Type: text/html\r\n"); + socket->write("Connection: close\r\n"); // Require two \r\n. + socket->write("\r\n"); + + socket->write(html.toUtf8()); + + socket->disconnectFromHost(); +} + +void HttpServer::stopServer() +{ + server->close(); + isRunning = false; +} diff --git a/src/sources/main.cpp b/src/sources/main.cpp index ae782fb..a130792 100644 --- a/src/sources/main.cpp +++ b/src/sources/main.cpp @@ -77,7 +77,7 @@ bool connect(QString database_file) sq.exec("CREATE TABLE 'Songbooks' ('id' INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL , 'name' TEXT, 'info' TEXT)"); sq.exec("CREATE TABLE 'Songs' ('id' INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL , " "'songbook_id' INTEGER, 'number' INTEGER, 'title' TEXT, 'category' INTEGER DEFAULT 0, " - "'tune' TEXT, 'words' TEXT, 'music' TEXT, 'song_text' TEXT, 'notes' TEXT, " + "'tune' TEXT, 'words' TEXT, 'music' TEXT, 'song_text' TEXT, 'notes' TEXT, 'use_translation' BOOL, " "'use_private' BOOL, 'alignment_v' INTEGER, 'alignment_h' INTEGER, 'color' INTEGER, 'font' TEXT, " "'info_color' INTEGER, 'info_font' TEXT, 'ending_color' INTEGER, 'ending_font' TEXT, " "'use_background' BOOL, 'background_name' TEXT, 'background' BLOB, 'count' INTEGER DEFAULT 0, 'date' TEXT)"); diff --git a/src/sources/managedatadialog.cpp b/src/sources/managedatadialog.cpp index 15c8067..6b9d7ae 100644 --- a/src/sources/managedatadialog.cpp +++ b/src/sources/managedatadialog.cpp @@ -359,7 +359,7 @@ void ManageDataDialog::importSongbook(QString path) // Prepare to import Songs QSqlDatabase::database().transaction(); sq.prepare("INSERT INTO Songs (songbook_id, number, title, category, tune, words, music, " - "song_text, notes, use_private, alignment_v, alignment_h, color, font, " + "song_text, notes, use_translation, use_private, alignment_v, alignment_h, color, font, " "background_name, count, date)" "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); @@ -403,7 +403,7 @@ void ManageDataDialog::importSongbook(QString path) } else if (xml.StartElement && xml.name() == "Song"_L1) { - QString xnum,xtitle,xcat,xtune,xwords,xmusic,xtext,xnotes, + QString xnum,xtitle,xcat,xtune,xwords,xmusic,xtext,xnotes,xusetranslation, xuse,xalign,xcolor,xfont,xback,xcount,xdate; // Read song data xnum = xml.attributes().value("number").toString(); @@ -446,6 +446,11 @@ void ManageDataDialog::importSongbook(QString path) xnotes = xml.readElementText(); xml.readNext(); } + else if(xml.StartElement && xml.name() == "use_translation"_L1) + { + xusetranslation = xml.readElementText(); + xml.readNext(); + } else if(xml.StartElement && xml.name() == "use_private"_L1) { xuse = xml.readElementText(); @@ -571,10 +576,10 @@ void ManageDataDialog::importSongbook(QString path) // Get and insert songs q.exec("SELECT * FROM Songs"); - sq.prepare("INSERT INTO Songs (songbook_id,number,title,category,tune,words,music,song_text,notes," + sq.prepare("INSERT INTO Songs (songbook_id,number,title,category,tune,words,music,song_text,notes,use_translation," "use_private,alignment_v,alignment_h,color,font,info_color,info_font,ending_color," "ending_font,use_background,background_name,background,count,date) " - "VALUES(:id, :num, :ti, :ca, :tu, :wo, :mu, :st, :no, :up, :av, :ah, :tc, :tf, " + "VALUES(:id, :num, :ti, :ca, :tu, :wo, :mu, :st, :no, :ut, :up, :av, :ah, :tc, :tf, " ":ic, :if, :ec, :ef, :ub, :bn, :b, :ct, :d)"); while(q.next()) { @@ -590,6 +595,7 @@ void ManageDataDialog::importSongbook(QString path) st = cleanSongLines(st); sq.bindValue(":st",st); sq.bindValue(":no",q.record().value("notes")); + sq.bindValue(":ut",q.record().value("use_translation")); sq.bindValue(":up",q.record().value("use_private")); sq.bindValue(":av",q.record().value("alignment_v")); sq.bindValue(":ah",q.record().value("alignment_h")); @@ -723,7 +729,7 @@ void ManageDataDialog::exportSongbook(QString path) q.exec("PRAGMA user_version = 2"); q.exec("CREATE TABLE 'SongBook' ('title' TEXT, 'info' TEXT)"); q.exec("CREATE TABLE 'Songs' ('number' INTEGER, 'title' TEXT, 'category' INTEGER DEFAULT 0, " - "'tune' TEXT, 'words' TEXT, 'music' TEXT, 'song_text' TEXT, 'notes' TEXT, " + "'tune' TEXT, 'words' TEXT, 'music' TEXT, 'song_text' TEXT, 'notes' TEXT, 'use_translation' BOOL, " "'use_private' BOOL, 'alignment_v' INTEGER, 'alignment_h' INTEGER, 'color' INTEGER, 'font' TEXT, " "'info_color' INTEGER, 'info_font' TEXT, 'ending_color' INTEGER, 'ending_font' TEXT, " "'use_background' BOOL, 'background_name' TEXT, 'background' BLOB, 'count' INTEGER DEFAULT 0, 'date' TEXT)"); @@ -742,10 +748,10 @@ void ManageDataDialog::exportSongbook(QString path) // Write Songs sq.exec("SELECT * FROM Songs WHERE songbook_id = " + songbook_id); - q.prepare("INSERT INTO Songs (number,title,category,tune,words,music,song_text,notes," + q.prepare("INSERT INTO Songs (number,title,category,tune,words,music,song_text,notes,use_translation," "use_private,alignment_v,alignment_h,color,font,info_color,info_font,ending_color,ending_font," "use_background,background_name,background,count,date) " - "VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); + "VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); while(sq.next()) { q.addBindValue(sq.record().value("number")); @@ -756,6 +762,7 @@ void ManageDataDialog::exportSongbook(QString path) q.addBindValue(sq.record().value("music")); q.addBindValue(sq.record().value("song_text")); q.addBindValue(sq.record().value("notes")); + q.addBindValue(sq.record().value("use_translation")); q.addBindValue(sq.record().value("use_private")); q.addBindValue(sq.record().value("alignment_v")); q.addBindValue(sq.record().value("alignment_h")); diff --git a/src/sources/settings.cpp b/src/sources/settings.cpp index eba3953..7f044e5 100644 --- a/src/sources/settings.cpp +++ b/src/sources/settings.cpp @@ -832,6 +832,16 @@ void Settings::loadSettings() } else if (n == "dcOpacity") general.displayControls.opacity = v.toDouble(); + else if (n == "httpEnabled") + general.httpServerEnabled = (v=="true"); + else if (n == "httpServerIPAddress") + general.httpServerIPAddress = v; + else if (n == "httpPort") + general.httpServerPort = v.toInt(); + else if (n == "webSocketPort") + general.webSocketServerPort = v.toInt(); + else if (n == "disableScreens") + general.disableScreens = (v=="true"); } } else if(t == "spMain") @@ -984,6 +994,17 @@ void Settings::saveSettings() gset += "\ndcIconSize = " + QString::number(general.displayControls.buttonSize); gset += QString("\ndcAlignment = %1,%2").arg(general.displayControls.alignmentV).arg(general.displayControls.alignmentH); gset += "\ndcOpacity = " + QString::number(general.displayControls.opacity); + if(general.httpServerEnabled) + gset += "\nhttpEnabled = true"; + else + gset += "\nhttpEnabled = false"; + gset += "\nhttpServerIPAddress = " + general.httpServerIPAddress; + gset += "\nhttpPort = " + QString::number(general.httpServerPort); + gset += "\nwebSocketPort = " + QString::number(general.webSocketServerPort); + if(general.disableScreens) + gset += "\ndisableScreens = true"; + else + gset += "\ndisableScreens = false"; // **** prepare softProjector main settings spset += "spSplitter = " + spMain.spSplitter.toHex(); diff --git a/src/sources/settingsdialog.cpp b/src/sources/settingsdialog.cpp index 5d1b429..ad54a37 100644 --- a/src/sources/settingsdialog.cpp +++ b/src/sources/settingsdialog.cpp @@ -79,6 +79,10 @@ void SettingsDialog::loadSettings(GeneralSettings &sets, Theme &thm, SlideShowSe currentDisplayScreen2 = gsettings.displayScreen2; currentDisplayScreen3 = gsettings.displayScreen3; currentDisplayScreen4 = gsettings.displayScreen4; + currentHTTPEnabled = gsettings.httpServerEnabled; + currentHTTPServerIPAddress = gsettings.httpServerIPAddress; + currentHTTPPort = gsettings.httpServerPort; + currentWebSocketPort = gsettings.webSocketServerPort; // Set individual items generalSettingswidget->setSettings(gsettings); @@ -179,6 +183,15 @@ void SettingsDialog::applySettings() // Redraw the screen: emit updateScreen(); + // Update HTTP server state + if(currentHTTPServerIPAddress != gsettings.httpServerIPAddress + || currentHTTPPort != gsettings.httpServerPort + || currentWebSocketPort != gsettings.webSocketServerPort + || currentHTTPEnabled != gsettings.httpServerEnabled) + { + emit httpServerState(gsettings.httpServerEnabled,gsettings.httpServerIPAddress,gsettings.httpServerPort,gsettings.webSocketServerPort); + } + // Save Settings theme.saveThemeUpdate(); @@ -188,6 +201,10 @@ void SettingsDialog::applySettings() currentDisplayScreen2 = gsettings.displayScreen2; currentDisplayScreen3 = gsettings.displayScreen3; currentDisplayScreen4 = gsettings.displayScreen4; + currentHTTPEnabled = gsettings.httpServerEnabled; + currentHTTPServerIPAddress = gsettings.httpServerIPAddress; + currentHTTPPort = gsettings.httpServerPort; + currentWebSocketPort = gsettings.webSocketServerPort; } void SettingsDialog::getThemes() diff --git a/src/sources/softprojector.cpp b/src/sources/softprojector.cpp index a2f33fc..0a8a245 100644 --- a/src/sources/softprojector.cpp +++ b/src/sources/softprojector.cpp @@ -60,6 +60,13 @@ SoftProjector::SoftProjector(QWidget *parent) mediaPlayer = new MediaWidget; mediaControls = new MediaControl(this); + if (mySettings.general.httpServerEnabled) { + webSocketServer = new WebSocketServer(); + webSocketServer->startServer(mySettings.general.httpServerIPAddress, mySettings.general.webSocketServerPort); + httpServer = new HttpServer(); + httpServer->startServer(mySettings.general.httpServerIPAddress, mySettings.general.httpServerPort, mySettings.general.webSocketServerPort); + } + ui->setupUi(this); // Create action group for language slections @@ -75,7 +82,9 @@ SoftProjector::SoftProjector(QWidget *parent) // Apply Settings applySetting(mySettings.general, theme, mySettings.slideSets, mySettings.bibleSets, mySettings.bibleSets2, mySettings.bibleSets3, mySettings.bibleSets4); - positionDisplayWindow(); + if (!mySettings.general.disableScreens) { + positionDisplayWindow(); + } showing = false; @@ -87,7 +96,6 @@ SoftProjector::SoftProjector(QWidget *parent) ui->projectTab->addTab(announceWidget,QIcon(":/icons/icons/announce.png"), tr("Announcements (F8)")); ui->projectTab->setCurrentIndex(0); - connect(bibleWidget, SIGNAL(goLive(QStringList, QString, QItemSelection)), this, SLOT(setChapterList(QStringList, QString, QItemSelection))); connect(bibleWidget, SIGNAL(setArrowCursor()), this, SLOT(setArrowCursor())); @@ -116,6 +124,7 @@ SoftProjector::SoftProjector(QWidget *parent) BibleVersionSettings&,BibleVersionSettings&))); connect(settingsDialog,SIGNAL(positionsDisplayWindow()),this,SLOT(positionDisplayWindow())); connect(settingsDialog,SIGNAL(updateScreen()),this,SLOT(updateScreen())); + connect(settingsDialog,SIGNAL(httpServerState(bool&,QString&,int&,int&)),this,SLOT(httpServerState(bool&,QString&,int&,int&))); connect(songWidget,SIGNAL(addToSchedule(Song&)),this,SLOT(addToShcedule(Song&))); connect(announceWidget,SIGNAL(addToSchedule(Announcement&)),this,SLOT(addToShcedule(Announcement&))); @@ -147,9 +156,11 @@ SoftProjector::SoftProjector(QWidget *parent) ui->toolBarEdit->addAction(ui->action_Help); ui->toolBarShow->addAction(ui->actionShow); - ui->toolBarShow->addAction(ui->actionClear); ui->toolBarShow->addAction(ui->actionHide); - ui->toolBarShow->addAction(ui->actionCloseDisplay); + if (!mySettings.general.disableScreens) { + ui->toolBarShow->addAction(ui->actionCloseDisplay); + ui->toolBarShow->addAction(ui->actionClear); + } ui->actionShow->setEnabled(false); ui->actionHide->setEnabled(false); @@ -188,6 +199,10 @@ SoftProjector::SoftProjector(QWidget *parent) version_string = "2.2"; this->setWindowTitle("SoftProjector " + version_string); + + // Song verse split hidden by default. + ui->songVerseSplitLayoutWidget->hide(); + connect(songWidget,SIGNAL(enableSongSplitVerse(bool)),this,SLOT(showSongVerseSplit(bool))); } SoftProjector::~SoftProjector() @@ -211,6 +226,8 @@ SoftProjector::~SoftProjector() delete shSart2; delete helpDialog; delete ui; + delete webSocketServer; + delete httpServer; } void SoftProjector::positionDisplayWindow() @@ -354,6 +371,9 @@ void SoftProjector::positionDisplayWindow() void SoftProjector::showDisplayScreen(bool show) { + if (mySettings.general.disableScreens) + return; + if(show) { pds1->showFullScreen(); @@ -460,6 +480,20 @@ void SoftProjector::applySetting(GeneralSettings &g, Theme &t, SlideShowSettings retranslateUis(); } +void SoftProjector::httpServerState(bool &state, QString &httpServerIPAddress, int &httpServerPort, int &webSocketServerPort) +{ + if (httpServer->isRunning) + httpServer->stopServer(); + + if (webSocketServer->isRunning) + webSocketServer->stopServer(); + + if (state) { + httpServer->startServer(httpServerIPAddress, httpServerPort, webSocketServerPort); + webSocketServer->startServer(httpServerIPAddress, webSocketServerPort); + } +} + void SoftProjector::closeEvent(QCloseEvent *event) { if(is_schedule_saved || schedule_file_path.isEmpty()) @@ -514,6 +548,9 @@ void SoftProjector::keyPressEvent(QKeyEvent *event) { ui->projectTab->setCurrentWidget(songWidget); songWidget->setSearchActive(); + if (songSplitVerse) { + ui->songVerseSplitLayoutWidget->setVisible(false); // re-show the split list ui. + } } else if(key == Qt::Key_F8) ui->projectTab->setCurrentWidget(announceWidget); @@ -534,6 +571,8 @@ void SoftProjector::keyPressEvent(QKeyEvent *event) nextSlide(); else if(key == Qt::Key_Enter) nextSlide(); + else if(key == Qt::Key_Down || key == Qt::Key_Up) + on_songVerseSplitListWidgetNavigation(Qt::Key(key)); else QMainWindow::keyPressEvent(event); } @@ -565,20 +604,41 @@ void SoftProjector::setAnnounceText(Announcement announce, int row) updateScreen(); } +/** + * @brief SoftProjector::setSongList called on go live, from songwidget preview list or schedule. + * @param row the selected verse index of the song preview list. + */ void SoftProjector::setSongList(Song song, int row) { - QStringList song_list = song.getSongTextList(); current_song = song; - current_song_verse = row; + QStringList songTextList = song.getSongTextList(true); // The complete song text with verse titles and translations. + + // Create a list for the rightmost ui list (without translations), + // for the http-server a list without verse titles, and a list with just the translation. + QStringList song_list; + currentSongVerses.clear(); + currentSongTranslatedVerses.clear(); + + foreach (QString verseText, songTextList) { + QStringList split = verseText.split("="); + song_list.append(split[0].trimmed()); // Add the original language (with verse title) to the ui list. + if (song.useTranslation && split.length() == 2) { + currentSongTranslatedVerses.append(split[1].trimmed()); // Add the translation to the translation list. + } else { + currentSongTranslatedVerses.append(""); // Empty translation because not every verse may be translated. + } + // Remove verse title and add to currentSongVerses. + QStringList splitLines = split[0].split("\n"); + splitLines.removeFirst(); + currentSongVerses.append(splitLines.join("\n").trimmed()); + } // Display the specified song text in the right-most column of softProjector pType = SONG; + ui->widgetMultiVerse->setVisible(false); ui->rbMultiVerse->setChecked(false); mediaControls->setVisible(false); - showing = true; - new_list = true; - ui->listShow->clear(); ui->labelIcon->setPixmap(QPixmap(":/icons/icons/song_tab.png").scaled(16,16,Qt::IgnoreAspectRatio,Qt::SmoothTransformation)); ui->labelShow->setText(song.title); if(song.notes.isEmpty()) @@ -588,13 +648,26 @@ void SoftProjector::setSongList(Song song, int row) ui->labelSongNotes->setText(QString("%1\n%2").arg(tr("Notes:","Notes to songs")).arg(song.notes)); ui->labelSongNotes->setVisible(true); } + + ui->listShow->blockSignals(true); // So itemSelectionChanged on clear doesnt call updateScreen again, like the old new_list flag. + ui->listShow->clear(); ui->listShow->setSpacing(5); ui->listShow->setWordWrap(false); ui->listShow->addItems(song_list); ui->listShow->setCurrentRow(row); ui->listShow->setFocus(); - new_list = false; - updateScreen(); + ui->listShow->blockSignals(false); + + showing = true; + + if (mySettings.general.httpServerEnabled) + { + sendSongVerseToServer(); + ui->actionShow->setEnabled(false); + ui->actionHide->setEnabled(true); + } else { + updateScreen(); + } } void SoftProjector::setChapterList(QStringList chapter_list, QString caption, QItemSelection selectedItems) @@ -604,10 +677,10 @@ void SoftProjector::setChapterList(QStringList chapter_list, QString caption, QI ui->widgetMultiVerse->setVisible(true); mediaControls->setVisible(false); showing = true; - new_list = true; ui->labelIcon->setPixmap(QPixmap(":/icons/icons/book.png").scaled(16,16,Qt::IgnoreAspectRatio,Qt::SmoothTransformation)); ui->labelShow->setText(caption); ui->labelSongNotes->setVisible(false); + ui->listShow->blockSignals(true); ui->listShow->clear(); ui->listShow->setSpacing(2); ui->listShow->setWordWrap(true); @@ -620,8 +693,16 @@ void SoftProjector::setChapterList(QStringList chapter_list, QString caption, QI ui->listShow->setCurrentRow(selectedItems.first().top()); ui->listShow->selectionModel()->select(selectedItems,QItemSelectionModel::Select); ui->listShow->setFocus(); - new_list = false; - updateScreen(); + ui->listShow->blockSignals(false); + + if (mySettings.general.httpServerEnabled) + { + sendBibleVerseToServer(); + ui->actionShow->setEnabled(false); + ui->actionHide->setEnabled(true); + } else { + updateScreen(); + } } void SoftProjector::setPictureList(QList &image_list,int row,QString name) @@ -761,10 +842,22 @@ void SoftProjector::on_listShow_itemSelectionChanged() // Called when the user selects a different row in the show (right-most) list. // First check if ratio button "Multi Verse" is check. If so, make button "Show" // enable and update screen only after show_botton is clicked. - if(ui->rbMultiVerse->isChecked()) + if (ui->rbMultiVerse->isChecked()) { ui->actionShow->setEnabled(true); - else - updateScreen(); + return; + } + if (mySettings.general.httpServerEnabled) + { + if (showing) { + if (pType == BIBLE) { + sendBibleVerseToServer(); + } else if (pType == SONG) { + sendSongVerseToServer(); + } + } + return; + } + updateScreen(); } void SoftProjector::updateScreen() @@ -841,6 +934,7 @@ void SoftProjector::updateScreen() ui->actionShow->setEnabled(false); ui->actionHide->setEnabled(true); + switch (pType) { case BIBLE: @@ -938,6 +1032,9 @@ void SoftProjector::showBible() } } +/* + * @param currentRow the selected verse index of the song preview or rightmost lists. + */ void SoftProjector::showSong(int currentRow) { // Get Song Settings @@ -1074,12 +1171,38 @@ void SoftProjector::showVideo() void SoftProjector::on_actionShow_triggered() { showing = true; + if (mySettings.general.httpServerEnabled) + { + if (pType == BIBLE) { + sendBibleVerseToServer(); + } else if (pType == SONG) { + if (ui->songVerseSplitListWidget->hasFocus()) { + sendSongVerseSplit(); // Resume song verse split display. + } else { + sendSongVerseToServer(); + } + } + + ui->actionShow->setEnabled(false); + ui->actionHide->setEnabled(true); + return; + } + updateScreen(); } void SoftProjector::on_actionHide_triggered() { showing = false; + if (mySettings.general.httpServerEnabled) + { + webSocketServer->setBlank(); + + ui->actionShow->setEnabled(true); + ui->actionHide->setEnabled(false); + return; + } + updateScreen(); } @@ -1100,12 +1223,11 @@ void SoftProjector::on_actionClear_triggered() } ui->actionClear->setEnabled(false); ui->actionShow->setEnabled(true); -// ui->actionHide->setEnabled(false); } void SoftProjector::on_actionCloseDisplay_triggered() { - if(ui->actionCloseDisplay->isChecked()) + if(ui->actionCloseDisplay->isChecked() && !mySettings.general.disableScreens) { pds1->showFullScreen(); if(hasDisplayScreen2) @@ -1158,8 +1280,21 @@ void SoftProjector::on_actionSettings_triggered() void SoftProjector::on_listShow_doubleClicked(QModelIndex index) { - // Called when the user double clicks on a row in the preview table. + // Called when the user double clicks on a row in the rightmost table. showing = true; + if (mySettings.general.httpServerEnabled) + { + if (pType == BIBLE) { + sendBibleVerseToServer(); + } else if (pType == SONG) { + sendSongVerseToServer(); + } + + ui->actionShow->setEnabled(false); + ui->actionHide->setEnabled(true); + return; + } + updateScreen(); } @@ -2319,7 +2454,7 @@ void SoftProjector::saveSchedule(bool overWrite) sq.exec("CREATE TABLE IF NOT EXISTS 'bible' ('scid' INTEGER, 'verseIds' TEXT, 'caption' TEXT, 'captionLong' TEXT)"); sq.exec("CREATE TABLE IF NOT EXISTS 'song' ('scid' INTEGER, 'songid' INTEGER, 'sbid' INTEGER, 'sbName' TEXT, " "'number' INTEGER, 'title' TEXT, 'category' INTEGER, 'tune' TEXT, 'wordsBy' TEXT, 'musicBy' TEXT, " - "'songText' TEXT, 'notes' TEXT, 'usePrivate' BOOL, 'alignV' INTEGER, 'alignH' INTEGER, 'color' INTEGER, " + "'songText' TEXT, 'notes' TEXT, 'useTranslation' BOOL, 'usePrivate' BOOL, 'alignV' INTEGER, 'alignH' INTEGER, 'color' INTEGER, " "'font' TEXT, 'infoColor' INTEGER, 'infoFont' TEXT, 'endingColor' INTEGER, 'endingFont' TEXT, " "'useBack' BOOL, 'backImage' BLOB, 'backName' TEXT)"); sq.exec("CREATE TABLE IF NOT EXISTS 'slideshow' ('scid' INTEGER, 'ssid' INTEGER, 'name' TEXT, 'info' TEXT)"); @@ -2378,9 +2513,9 @@ void SoftProjector::saveScheduleItemNew(QSqlQuery &q, int scid, const BibleHisto void SoftProjector::saveScheduleItemNew(QSqlQuery &q, int scid, const Song &s) { q.prepare("INSERT INTO song (scid,songid,sbid,sbName,number,title,category,tune,wordsBy,musicBy," - "songText,notes,usePrivate,alignV,alignH,color,font,infoColor,infoFont,endingColor," + "songText,notes,useTranslation,usePrivate,alignV,alignH,color,font,infoColor,infoFont,endingColor," "endingFont,useBack,backImage,backName) " - "VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); + "VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); q.addBindValue(scid); q.addBindValue(s.songID); q.addBindValue(s.songbook_id); @@ -2393,6 +2528,7 @@ void SoftProjector::saveScheduleItemNew(QSqlQuery &q, int scid, const Song &s) q.addBindValue(s.musicBy); q.addBindValue(s.songText); q.addBindValue(s.notes); + q.addBindValue(s.useTranslation); q.addBindValue(s.usePrivateSettings); q.addBindValue(s.alignmentV); q.addBindValue(s.alignmentH); @@ -2678,6 +2814,7 @@ void SoftProjector::openScheduleItem(QSqlQuery &q, const int scid, Song &s) s.musicBy = r.field("musicBy").value().toString(); s.songText = r.field("songText").value().toString(); s.notes = r.field("notes").value().toString(); + s.useTranslation = r.field("useTranslation").value().toBool(); s.usePrivateSettings = r.field("usePrivate").value().toBool(); s.alignmentV = r.field("alignV").value().toInt(); s.alignmentH = r.field("alignH").value().toInt(); @@ -2744,3 +2881,156 @@ void SoftProjector::openScheduleItem(QSqlQuery &q, const int scid, Announcement a.alignmentV = q.value(12).toInt(); a.alignmentH = q.value(13).toInt(); } + + +void SoftProjector::sendBibleVerseToServer() +{ + ui->songVerseSplitLayoutWidget->setVisible(false); // Hide the song split list if visible. + + int srows(ui->listShow->count()); + QList currentRows; + for(int i(0); ilistShow->item(i)->isSelected()) + currentRows.append(i); + } + webSocketServer->setBibleText(bibleWidget->bible.getCurrentVerseAndCaption(currentRows,theme.bible, mySettings.bibleSets)); +} + +/* + * Called on go live with http server enabled. + */ +void SoftProjector::sendSongVerseToServer() +{ + int currentVerseIndex = ui->listShow->currentRow(); + + if(!songSplitVerse) + { // Sends the full verse. + webSocketServer->setSongText( + currentSongVerses[currentVerseIndex], + currentSongTranslatedVerses[currentVerseIndex], + "", "", (currentSongVerses.count() == currentVerseIndex + 1) + ); + } else { + setSongVerseSplitList(currentVerseIndex); + } +} + +void SoftProjector::showSongVerseSplit(bool enabled) +{ + songSplitVerse = enabled; + if (!enabled) { + // Hide the split list. Split shows only when live. + ui->songVerseSplitLayoutWidget->setVisible(false); + } +} + +QStringList SoftProjector::splitSongVerse(QString verse) +{ // Group into 2's. If verse has odd amount of lines, last group will include 3. + QStringList splitList; + QStringList oneLines = verse.split("\n"); + int splitListCount = oneLines.count(); + int evenGroups = splitListCount / 2; + int remaining = splitListCount % 2; + + if (evenGroups == 0 && remaining != 0) { + // One line. + splitList.append(oneLines.at(0)); + } + + int line = 0; + for (int i = 0; i < evenGroups; i++) { + // Appends two lines together and adds to output splitList. + + QString group = oneLines.at(line) + "\n" + oneLines.at(line + 1); + + if (i == evenGroups - 1 && remaining != 0) { + // Add remainder to this last group. + group += "\n" + oneLines.at(line + 2); + } + + splitList.append(group); + + line = line + 2; + } + + return splitList; +} + +/** + * @param currentVerseIndex the selected verse index in ui->listShow + * @param selectRow "last" to select last split lines (when navigating back). + */ +void SoftProjector::setSongVerseSplitList(int currentVerseIndex, QString selectRow) +{ + currentSongVerseSplitList = splitSongVerse(currentSongVerses[currentVerseIndex]); + QString translatedVerse = currentSongTranslatedVerses[currentVerseIndex]; + if (translatedVerse == "") { + // Not every verse is translated, fill with empty strings. + currentSongTranslatedVerseSplitList = QVector(currentSongVerseSplitList.count(), "").toList(); + } else { + currentSongTranslatedVerseSplitList = splitSongVerse(currentSongTranslatedVerses[currentVerseIndex]); + } + + ui->songVerseSplitLayoutWidget->setVisible(true); + ui->songVerseSplitListWidget->blockSignals(true); + ui->songVerseSplitListWidget->clear(); + ui->songVerseSplitListWidget->blockSignals(false); + ui->songVerseSplitListWidget->setSpacing(5); + ui->songVerseSplitListWidget->setWordWrap(false); + ui->songVerseSplitListWidget->addItems(currentSongVerseSplitList); // signals itemSelectionChanged() to send split to server. + ui->songVerseSplitListWidget->setCurrentRow(selectRow == "last" ? (currentSongVerseSplitList.count() - 1) : 0); + ui->songVerseSplitListWidget->setFocus(); +} + +/* + * Send the current song verse lines to the server. +*/ +void SoftProjector::sendSongVerseSplit() { + int currentVerseIndex = ui->listShow->currentRow(); + int currentVerseSplitIndex = ui->songVerseSplitListWidget->currentRow(); + webSocketServer->setSongText( + currentSongVerses[currentVerseIndex], + currentSongTranslatedVerses[currentVerseIndex], + currentSongVerseSplitList[currentVerseSplitIndex], + currentSongTranslatedVerseSplitList[currentVerseSplitIndex], + (currentSongVerses.count() == currentVerseIndex + 1) + ); +} + +void SoftProjector::on_songVerseSplitListWidget_itemSelectionChanged() +{ + if (!showing) { + return; + } + sendSongVerseSplit(); +} + +void SoftProjector::on_songVerseSplitListWidget_itemDoubleClicked(QListWidgetItem *item) +{ + showing = true; + ui->actionShow->setEnabled(false); + ui->actionHide->setEnabled(true); + sendSongVerseSplit(); // Resume song verse split display. +} + +/* + * Go to next or previous verse on up/down key of the first/last split. + * Going to the previous verse shows the last line split. + */ +void SoftProjector::on_songVerseSplitListWidgetNavigation(Qt::Key key) { + if (!showing || !ui->songVerseSplitListWidget->hasFocus()) { + return; + } + if (key == Qt::Key_Up && (ui->songVerseSplitListWidget->currentRow() == 0) && ui->listShow->currentRow() != 0) { + // Go to the previous verse, and show the last split line. + ui->listShow->blockSignals(true); // Block itemSelectionChanged(), which would show the first split line. + ui->listShow->setCurrentRow(ui->listShow->currentRow() - 1); + ui->listShow->blockSignals(false); + setSongVerseSplitList(ui->listShow->currentRow(), "last"); + } else if (key == Qt::Key_Down && (ui->songVerseSplitListWidget->currentRow() == (ui->songVerseSplitListWidget->count() - 1))) { + // Go to the next verse. + nextSlide(); // Same as pressing the Right key. + } +} + diff --git a/src/sources/song.cpp b/src/sources/song.cpp index 87d66ec..01fb4bc 100644 --- a/src/sources/song.cpp +++ b/src/sources/song.cpp @@ -28,7 +28,11 @@ QString clean(QString str) { //Removes all none alphanumeric characters from the string - str.replace(QRegularExpression("[\\W*]")," "); + + // Matches anything that is NOT a Letter (L) or a Number (N). Supports Cyrillic. + QRegularExpression re("[^\\p{L}\\p{N}]+"); + + str.replace(re, " "); str = str.simplified(); return str; } @@ -161,6 +165,30 @@ bool isStanzaSlideTitle(QString string) return false; } +QString getStanzaBlock(int &i, QStringList &list, bool includeTranslation) +{ + QString line,block; + int j(i); + + while(i < list.count()) + { + line = list.at(i); + if(includeTranslation == false && line.contains(QRegularExpression("^="))) { break; } // Doesn't include translations in the ui lists. + if(line.contains(QRegularExpression("^&"))) + line.remove("&"); + + if(isStanzaTitle(line) && (i!=j)) + { + i--; + break; + } + block += line + "\n"; + ++i; + } + + return block.trimmed(); +} + Song::Song() { // initialize songId to be zero at start; @@ -205,6 +233,7 @@ void Song::setDefaults() backgroundName = ""; background = QPixmap(); notes = ""; + useTranslation = false; } void Song::readData() @@ -212,10 +241,10 @@ void Song::readData() QSqlQuery sq; // 0 1 2 3 4 5 6 7 8 // 9 10 11 12 13 14 15 16 17 - // 18 19 20 + // 18 19 20 21 sq.exec("SELECT songbook_id, number, title, category, tune, words, music, song_text, notes, " "use_private, alignment_v, alignment_h, color, font, info_color, info_font, ending_color, ending_font, " - "use_background, background_name, background FROM Songs WHERE id = " + QString::number(songID)); + "use_background, background_name, background, use_translation FROM Songs WHERE id = " + QString::number(songID)); sq.first(); songbook_id = sq.value(0).toString(); number = sq.value(1).toInt(); @@ -226,6 +255,7 @@ void Song::readData() musicBy = sq.value(6).toString(); songText = sq.value(7).toString(); notes = sq.value(8).toString(); + useTranslation = sq.value(21).toBool(); usePrivateSettings = sq.value(9).toBool(); if(!sq.value(10).isNull()) alignmentV = sq.value(10).toInt(); @@ -248,7 +278,7 @@ void Song::readData() background.loadFromData(sq.value(20).toByteArray()); } -QStringList Song::getSongTextList() +QStringList Song::getSongTextList(bool includeTranslation) { // This function prepares a song list that will be shown in the song preview and show list. // It will it will automatically prepare correct sining order of verses and choruses. @@ -270,7 +300,7 @@ QStringList Song::getSongTextList() if(isStanzaVerseTitle(line)) { // Fill Verse - text = getStanzaBlock(pnum,songlist); + text = getStanzaBlock(pnum,songlist,includeTranslation); formatedSong.append(text); if (has_chorus)// add Chorus stansa to the formated list if it exists @@ -282,7 +312,7 @@ QStringList Song::getSongTextList() { // Fill Additional parts of the verse - text = getStanzaBlock(pnum,songlist); + text = getStanzaBlock(pnum,songlist,includeTranslation); // it chorus esits, this means that it was added to the formated list // and needs to be removed before adding addintion Veres stansas to formated list if(has_chorus) @@ -298,7 +328,7 @@ QStringList Song::getSongTextList() else if (isStanzaSlideTitle(line)) { // Fill Insert - text = getStanzaBlock(pnum,songlist); + text = getStanzaBlock(pnum,songlist,includeTranslation); formatedSong.append(text); // Chorus is not added to Insert, if one is needed, @@ -309,7 +339,7 @@ QStringList Song::getSongTextList() else if (isStanzaRefrainTitle(line)) { // Fill Chorus - text = getStanzaBlock(pnum,songlist); + text = getStanzaBlock(pnum,songlist,includeTranslation); QStringList chorusold = chorus; chorus.clear(); chorus.append(text); @@ -338,7 +368,7 @@ QStringList Song::getSongTextList() else if(isStanzaAndRefrainTitle(line)) { // Fill other chorus parts to Chorus block - text = getStanzaBlock(pnum,songlist); + text = getStanzaBlock(pnum,songlist,includeTranslation); removeLastChorus(chorus,formatedSong); chorus.append(text); @@ -350,39 +380,19 @@ QStringList Song::getSongTextList() return formatedSong; } -QString Song::getStanzaBlock(int &i, QStringList &list) -{ - QString line,block; - int j(i); - - while(i < list.count()) - { - line = list.at(i); - if(line.contains(QRegularExpression("^&"))) - line.remove("&"); - - if(isStanzaTitle(line) && (i!=j)) - { - i--; - break; - } - block += line + "\n"; - ++i; - } - - return block.trimmed(); -} - void Song::removeLastChorus(QStringList ct, QStringList &list) { for(int i(0);iendFilterChange(); } void SongProxyModel::setSongbookFilter(QString new_songbook) @@ -652,7 +663,7 @@ void Song::saveUpdate() // Update song information QSqlQuery sq; sq.prepare("UPDATE Songs SET songbook_id = ?, number = ?, title = ?, category = ?, tune = ?, words = ?, music = ?, " - "song_text = ?, notes = ?, use_private = ?, alignment_v = ?, alignment_h = ?, color = ?, font = ?, " + "song_text = ?, notes = ?, use_translation = ?, use_private = ?, alignment_v = ?, alignment_h = ?, color = ?, font = ?, " "info_color = ?, info_font = ?, ending_color = ?, ending_font = ?, use_background = ?, " "background_name = ?, background = ? WHERE id = ?"); sq.addBindValue(songbook_id); @@ -664,6 +675,7 @@ void Song::saveUpdate() sq.addBindValue(musicBy); sq.addBindValue(songText); sq.addBindValue(notes); + sq.addBindValue(useTranslation); sq.addBindValue(usePrivateSettings); sq.addBindValue(alignmentV); sq.addBindValue(alignmentH); @@ -684,10 +696,10 @@ void Song::saveNew() { // Add a new song QSqlQuery sq; - sq.prepare("INSERT INTO Songs (songbook_id,number,title,category,tune,words,music,song_text,notes," + sq.prepare("INSERT INTO Songs (songbook_id,number,title,category,tune,words,music,song_text,notes,use_translation," "use_private,alignment_v,alignment_h,color,font,info_color,info_font,ending_color," "ending_font,use_background,background_name,background) " - "VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); + "VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); sq.addBindValue(songbook_id); sq.addBindValue(number); sq.addBindValue(title); @@ -697,6 +709,7 @@ void Song::saveNew() sq.addBindValue(musicBy); sq.addBindValue(songText); sq.addBindValue(notes); + sq.addBindValue(useTranslation); sq.addBindValue(usePrivateSettings); sq.addBindValue(alignmentV); sq.addBindValue(alignmentH); @@ -738,10 +751,10 @@ QList SongDatabase::getSongs() // get songs // 0 1 2 3 4 5 6 7 8 // 9 10 11 12 13 14 15 16 17 - // 18 19 20 + // 18 19 20 22 sq.exec("SELECT id, songbook_id, number, title, category, tune, words, music, song_text, notes, " "use_private, alignment_v, alignment_h, color, font, info_color, info_font, ending_color, ending_font, " - "use_background, background_name, background FROM Songs"); + "use_background, background_name, background, use_translation FROM Songs"); while(sq.next()) { Song song; @@ -755,6 +768,7 @@ QList SongDatabase::getSongs() song.musicBy = sq.value(7).toString(); song.songText = sq.value(8).toString(); song.notes = sq.value(9).toString(); + song.useTranslation = sq.value(22).toBool(); song.usePrivateSettings = sq.value(10).toBool(); if(!sq.value(11).isNull()) song.alignmentV = sq.value(11).toInt(); diff --git a/src/sources/songwidget.cpp b/src/sources/songwidget.cpp index d9c5dbe..2589d2a 100644 --- a/src/sources/songwidget.cpp +++ b/src/sources/songwidget.cpp @@ -43,7 +43,7 @@ SongWidget::SongWidget(QWidget *parent) : ui->songs_view->setColumnHidden(0, true); // Hide category ui->songs_view->resizeColumnToContents(1); // Song Number ui->songs_view->setColumnWidth(2, 150);//Song Title - ui->songs_view->resizeColumnToContents(3); // Songbook + ui->songs_view->horizontalHeader()->setSectionResizeMode(3, QHeaderView::ResizeToContents); // Songbook ui->songs_view->setColumnWidth(4, 50);//Tune proxy_model->setSongbookFilter("ALL"); @@ -56,6 +56,11 @@ SongWidget::SongWidget(QWidget *parent) : // set highligher highlight = new HighlighterDelegate(ui->listPreview); ui->listWidgetDummy->setVisible(false); + + searchTimer = new QTimer(this); + searchTimer->setSingleShot(true); + connect(searchTimer, &QTimer::timeout, this, &SongWidget::processSearch); + } SongWidget::~SongWidget() @@ -182,6 +187,8 @@ void SongWidget::sendToPreview(Song song) ui->label_notes->setVisible(true); } preview_song = song; + ui->btnLive->setEnabled(true); + ui->enableSplitVerseCheckBox->setEnabled(true); } void SongWidget::sendToPreviewFromSchedule(Song &song) @@ -193,6 +200,8 @@ void SongWidget::sendToPreviewFromSchedule(Song &song) void SongWidget::sendToProjector(Song song, int row) { + emit enableSongSplitVerse(ui->enableSplitVerseCheckBox->isChecked()); + // Display the specified song text in the right-most column of softProjector: emit sendSong(song, row); @@ -302,6 +311,13 @@ void SongWidget::on_btnLive_clicked() } void SongWidget::on_lineEditSearch_textEdited(QString text) +{ + searchText = text; + // The search only actually runs when the user stops typing for 0.2 seconds. + searchTimer->start(200); +} + +void SongWidget::processSearch() { // Check if full-text search is in progress // If no full-text search is in progress, then filter @@ -310,7 +326,7 @@ void SongWidget::on_lineEditSearch_textEdited(QString text) // If search text is numeric, sort by the number, else sort by title bool ok; - text.toInt(&ok); + searchText.toInt(&ok); if(ok) { proxy_model->sort(1); @@ -324,9 +340,7 @@ void SongWidget::on_lineEditSearch_textEdited(QString text) bool match_beginning = (ui->comboBoxFilterType->currentIndex() == 1); bool exact_match = (ui->comboBoxFilterType->currentIndex() == 2); - songs_model->emitLayoutAboutToBeChanged(); // prepares view to be redrawn - proxy_model->setFilterString(text, match_beginning, exact_match); - songs_model->emitLayoutChanged(); // forces the view to redraw + proxy_model->setFilterString(searchText, match_beginning, exact_match); // Select the first row that matches the new filter: ui->songs_view->selectRow(0); @@ -430,7 +444,7 @@ void SongWidget::deleteSong() } void SongWidget::addNewSong(Song song, int initial_sid) -{ +{ // TODO: fix bug, crashes if adding 2 songs with same title in a row. songs_model->addSong(song); allSongs.append(song); @@ -555,7 +569,7 @@ void SongWidget::on_pushButtonSearch_clicked() int type = ui->comboBoxSearchType->currentIndex(); // Make sure that there is some text to do a search for, if none, then return - if(search_text.count()<1) + if(search_text.length()<1) { ui->lineEditSearch->clear(); ui->lineEditSearch->setPlaceholderText(tr("Please enter search text")); @@ -671,3 +685,12 @@ void SongWidget::setSearchActive() ui->lineEditSearch->setFocus(); ui->lineEditSearch->selectAll(); } + +/* + * Show/Hide the song verse split list. + */ +void SongWidget::on_enableSplitVerseCheckBox_toggled(bool checked) +{ + emit enableSongSplitVerse(checked); +} + diff --git a/src/sources/websocketserver.cpp b/src/sources/websocketserver.cpp new file mode 100644 index 0000000..64813fd --- /dev/null +++ b/src/sources/websocketserver.cpp @@ -0,0 +1,129 @@ +/*************************************************************************** +// +// softProjector - an open source media projection software +// Copyright (C) 2023 Vladislav Kobzar +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 3 of the License. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +***************************************************************************/ + +#include "QtWebSockets/qwebsocketserver.h" +#include "QtWebSockets/qwebsocket.h" + +#include "../headers/websocketserver.hpp" + +#include + +WebSocketServer::WebSocketServer() { } + +void WebSocketServer::startServer(QString httpServerIPAddress, quint16 port) { + server = new QWebSocketServer(QStringLiteral("WebSocket Server"), QWebSocketServer::NonSecureMode, this); + if (server->listen(QHostAddress(httpServerIPAddress), port)) { + qDebug() << "Websocket server listening on " << httpServerIPAddress << ":" << port; + connect(server, &QWebSocketServer::newConnection, this, &WebSocketServer::onNewConnection); + connect(server, &QWebSocketServer::closed, this, &WebSocketServer::closed); + isRunning = true; + } +} + +void WebSocketServer::onNewConnection() +{ + QWebSocket *pSocket = server->nextPendingConnection(); + + connect(pSocket, &QWebSocket::disconnected, this, &WebSocketServer::socketDisconnected); + + clients << pSocket; + + sendToClients(); +} + +void WebSocketServer::sendToClients() +{ + QString output = QJsonDocument(jsonOutput).toJson(QJsonDocument::Compact); + for (int i = 0; i < clients.size(); i++) { + clients[i]->sendTextMessage(output); + } +} + +void WebSocketServer::socketDisconnected() +{ + QWebSocket *pClient = qobject_cast(sender()); + qDebug() << "socketDisconnected:" << pClient; + if (pClient) { + clients.removeAll(pClient); + pClient->deleteLater(); + } +} + +void WebSocketServer::closingClient() +{ + QTcpSocket* socket = (QTcpSocket*)sender(); + socket->deleteLater(); +} + +void WebSocketServer::stopServer() +{ + server->close(); + qDeleteAll(clients.begin(), clients.end()); + isRunning = false; +} + +void WebSocketServer::setBibleText(Verse verse) +{ + jsonOutput = QJsonObject(); + + QJsonObject bible; + + QJsonObject primary; + primary["verse"] = verse.primary_text; + primary["reference"] = (verse.primary_caption == ":" ? "" : verse.primary_caption); + bible["primary"] = primary; + + QJsonObject secondary; + secondary["verse"] = verse.secondary_text; + secondary["reference"] =(verse.secondary_caption == ":" ? "" : verse.secondary_caption); + bible["secondary"] = secondary; + + QJsonObject trinary; + trinary["verse"] = verse.trinary_text; + trinary["reference"] = (verse.trinary_caption == ":" ? "" : verse.trinary_caption); + bible["trinary"] = trinary; + + jsonOutput["bible"] = bible; + + sendToClients(); +} + +void WebSocketServer::setSongText(QString verse, QString verseTranslation, QString splitVerse, QString splitVerseTranslation, bool isLast) +{ + jsonOutput = QJsonObject(); + + QJsonObject jsonSong; + + jsonSong["verse"] = verse; + jsonSong["verseTranslation"] = verseTranslation; + jsonSong["split"] = splitVerse; + jsonSong["splitTranslation"] = splitVerseTranslation; + jsonSong["isLast"] = isLast; + + jsonOutput["song"] = jsonSong; + + sendToClients(); +} + +void WebSocketServer::setBlank() +{ + jsonOutput = QJsonObject(); + + sendToClients(); +} diff --git a/src/ui/editwidget.ui b/src/ui/editwidget.ui index fd04644..2bee91c 100644 --- a/src/ui/editwidget.ui +++ b/src/ui/editwidget.ui @@ -169,6 +169,30 @@ + + + + + + Use Translation + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + @@ -202,7 +226,16 @@ true - + + 0 + + + 0 + + + 0 + + 0 diff --git a/src/ui/generalsettingwidget.ui b/src/ui/generalsettingwidget.ui index b3b79aa..1be94cc 100644 --- a/src/ui/generalsettingwidget.ui +++ b/src/ui/generalsettingwidget.ui @@ -6,8 +6,8 @@ 0 0 - 412 - 440 + 371 + 519 @@ -92,18 +92,28 @@ - - - - Qt::Horizontal + + + + + 0 + 0 + - - - 162 - 20 - + + Select onto which screen to display - + + + + + + Secondary Display Screen: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + @@ -233,6 +243,16 @@ Primary Display Screen Controls + + + + NOTE: Display screen controls will be visible on the primary display screen only when one monitor is avaliable. + + + true + + + @@ -371,16 +391,155 @@ - - + + + + + + + HTTP Server (changes require restart) + + + + 9 + + + 9 + + + 9 + + + 9 + + + + + + 60 + 0 + + + + true + + + QAbstractSpinBox::NoButtons + + + 9999 + + + 8089 + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 40 + 20 + + + + + + + + true + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777213 + 16777215 + + - NOTE: Display screen controls will be visible on the primary display screen only when one monitor is avaliable. + HTTP Port: - + + + + + + Disable display forms for HTTP-only use (restart required). + + + Disable Screens + + + + + + + WebSocket Port: + + + + + + + + 0 + 0 + + + + + 60 + 0 + + + true + + QAbstractSpinBox::NoButtons + + + 9999 + + + 8087 + + + + + + + Enable HTTP Server + + + + + IP Address: + + + + + + @@ -392,7 +551,7 @@ 20 - 14 + 40 diff --git a/src/ui/softprojector.ui b/src/ui/softprojector.ui index a29b06c..2c5f121 100644 --- a/src/ui/softprojector.ui +++ b/src/ui/softprojector.ui @@ -76,6 +76,9 @@ + + 6 + @@ -193,17 +196,70 @@ + + true + 0 0 + + false + false + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Song Verse Split + + + 0 + + + + + + + + 0 + 0 + + + + + + + diff --git a/src/ui/songwidget.ui b/src/ui/songwidget.ui index e076e4b..9956a7a 100644 --- a/src/ui/songwidget.ui +++ b/src/ui/songwidget.ui @@ -320,6 +320,19 @@ + + + + false + + + Qt::RightToLeft + + + Split Verse Text + + +