From c00997234c7e52d7540f173bc56b77e0a39915cf Mon Sep 17 00:00:00 2001 From: Prashanth Udupa Date: Mon, 27 Jun 2022 17:07:17 +0530 Subject: [PATCH] Help tips. Quite a few users have posted questions on our Discord channel or have sent emails about things we already have a video explainer for. Going forward, we will identify places in Scrite where users may need to know about such videos the most and suggest them to watch it. --- qml/HelpTipNotification.qml | 57 ++++++++++++++ qml/NotebookView.qml | 4 + qml/NotificationsView.qml | 117 ++++++++++++++++++----------- qml/ScriteDocumentView.qml | 62 ++++++++++++++- qml/StructureGroupsMenu.qml | 8 ++ qml/UserLogin.qml | 2 +- scrite_ui.qrc | 1 + src/core/application.h | 2 + src/network/jsonhttprequest.cpp | 1 + src/network/user.cpp | 39 ++++++++++ src/network/user.h | 8 ++ src/quick/objects/notification.cpp | 18 +++++ src/quick/objects/notification.h | 14 ++++ 13 files changed, 288 insertions(+), 45 deletions(-) create mode 100644 qml/HelpTipNotification.qml diff --git a/qml/HelpTipNotification.qml b/qml/HelpTipNotification.qml new file mode 100644 index 00000000..d302622c --- /dev/null +++ b/qml/HelpTipNotification.qml @@ -0,0 +1,57 @@ +/**************************************************************************** +** +** Copyright (C) TERIFLIX Entertainment Spaces Pvt. Ltd. Bengaluru +** Author: Prashanth N Udupa (prashanth.udupa@teriflix.com) +** +** This code is distributed under GPL v3. Complete text of the license +** can be found here: https://www.gnu.org/licenses/gpl-3.0.txt +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +****************************************************************************/ + +import QtQuick 2.15 +import io.scrite.components 1.0 + +QtObject { + property string tipName + property var helpTip: Scrite.user.helpTips[tipName] + property bool tipShown: helpNotificationSettings.isTipShown(tipName) + property bool enabled: true + + Notification.title: helpTip ? helpTip.title : "" + Notification.image: helpTip ? helpTip.image.url : "" + Notification.active: enabled && helpTip && !tipShown + Notification.text: helpTip ? helpTip.text : "" + Notification.autoClose: false + Notification.buttons: { + var ret = [] + if(helpTip) + helpTip.buttons.forEach( (item) => { + ret.push(item.text) + }) + return ret + } + Notification.onImageClicked: { + if(helpTip) { + if(helpTip.image.action !== "$dismiss") + Qt.openUrlExternally(helpTip.image.action) + markTipAsShown() + } + } + + Notification.onButtonClicked: (buttonIndex) => { + if(helpTip) { + const button = helpTip.buttons[buttonIndex] + if(button.action !== "$dismiss") + Qt.openUrlExternally(button.action) + } + + markTipAsShown() + } + + function markTipAsShown() { + helpNotificationSettings.markTipAsShown(tipName) + } +} diff --git a/qml/NotebookView.qml b/qml/NotebookView.qml index 0675ba36..d27c7247 100644 --- a/qml/NotebookView.qml +++ b/qml/NotebookView.qml @@ -3134,4 +3134,8 @@ Rectangle { Component.onCompleted: Qt.callLater(generateStatsReport) } } + + HelpTipNotification { + tipName: "notebook" + } } diff --git a/qml/NotificationsView.qml b/qml/NotificationsView.qml index d9692f4c..34ae5da0 100644 --- a/qml/NotificationsView.qml +++ b/qml/NotificationsView.qml @@ -12,6 +12,7 @@ ****************************************************************************/ import QtQuick 2.15 +import QtQuick.Layouts 1.15 import QtQuick.Controls 2.15 import io.scrite.components 1.0 @@ -32,51 +33,84 @@ Flickable { model: Scrite.notifications.count Rectangle { + required property int index + property Notification notification: Scrite.notifications.notificationAt(index) + width: notificationsView.width-1 - height: Math.max(100, ntextLayout.implicitHeight+20) + height: Math.max(100, nLayout.implicitHeight+44) color: notification.color border { width: 1; color: primaryColors.borderColor } - property Notification notification: Scrite.notifications.notificationAt(index) - Column { - id: ntextLayout - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.right: notification.autoClose ? parent.right : dismissButton.left - anchors.margins: 20 - spacing: 10 - - Text { - width: parent.width - text: notification.title - wrapMode: Text.WordWrap - font.pixelSize: 20 - font.bold: true - visible: text !== "" - color: notification.textColor - } + RowLayout { + id: nLayout + width: parent.width-44 + anchors.centerIn: parent + spacing: 30 + + Rectangle { + visible: notification.hasImage + Layout.preferredWidth: parent.width*0.25 + Layout.preferredHeight: { + if(nimage.status === Image.Ready) + return nimage.sourceSize.height * (Layout.preferredWidth/nimage.sourceSize.width) + return Layout.preferredWidth*9/16 + } + border.width: 1 + border.color: primaryColors.borderColor + + Image { + id: nimage + source: notification.image + fillMode: Image.PreserveAspectFit + anchors.fill: parent + anchors.margins: 1 + mipmap: true + + MouseArea { + anchors.fill: parent + onClicked: notification.notifyImageClick() + } + } - Text { - width: parent.width - text: notification.text - wrapMode: Text.WordWrap - font.pixelSize: 16 - color: notification.textColor + BusyIndicator { + anchors.centerIn: parent + running: nimage.status !== Image.Ready + } } - Row { - spacing: parent.spacing * 3 - anchors.left: parent.left - anchors.leftMargin: 40 + ColumnLayout { + Layout.fillWidth: true + spacing: 20 - Repeater { - model: notification.buttons + Label { + Layout.fillWidth: true + text: notification.title + wrapMode: Text.WordWrap + font.pointSize: Scrite.app.idealFontPointSize + 4 + font.bold: true + visible: text !== "" + color: notification.textColor + } + + Label { + Layout.fillWidth: true + font.pointSize: Scrite.app.idealFontPointSize + text: notification.text + wrapMode: Text.WordWrap + color: notification.textColor + } - Item { - width: button.width - height: button.height * 2 + RowLayout { + Layout.fillWidth: true + spacing: 20 + + Repeater { + model: notification.buttons Button2 { + required property string modelData + required property int index + id: button anchors.verticalCenter: parent.verticalCenter width: Math.max(75, implicitWidth) @@ -86,16 +120,13 @@ Flickable { } } } - } - Button2 { - id: dismissButton - visible: notification.autoClose === false - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - anchors.rightMargin: 20 - text: "Dismiss" - onClicked: Scrite.notifications.dismissNotification(index) + Button2 { + id: dismissButton + visible: !notification.autoClose && !notification.hasButtons + text: "Dismiss" + onClicked: Scrite.notifications.dismissNotification(index) + } } } } diff --git a/qml/ScriteDocumentView.qml b/qml/ScriteDocumentView.qml index 8b15a6f0..0eff2c37 100644 --- a/qml/ScriteDocumentView.qml +++ b/qml/ScriteDocumentView.qml @@ -162,6 +162,32 @@ Item { property bool showAllFormQuestions: true } + Settings { + id: helpNotificationSettings + fileName: Scrite.app.settingsFilePath + category: "Help" + + property string dayZero + function daysSinceZero() { + const today = new Date() + const dzero = dayZero === "" ? today : new Date(dayZero + "Z") + const days = Math.floor((today.getTime() - dzero.getTime()) / (24*60*60*1000)) + return days + } + + property string tipsShown: "" + function isTipShown(val) { + const ts = tipsShown.split(",") + return ts.indexOf(val) >= 0 + } + function markTipAsShown(val) { + var ts = tipsShown.length > 0 ? tipsShown.split(",") : [] + if(ts.indexOf(val) < 0) + ts.push(val) + tipsShown = ts.join(",") + } + } + Shortcut { context: Qt.ApplicationShortcut sequence: "Ctrl+P" @@ -1215,6 +1241,11 @@ Item { ShortcutsModelItem.shortcut: "F10" } } + + HelpTipNotification { + tipName: Scrite.app.isWindowsPlatform ? "language_windows" : (Scrite.app.isMacOSPlatform ? "language_macos" : "language_linux") + enabled: Scrite.app.transliterationEngine.language !== TransliterationEngine.English + } } ToolButton3 { @@ -1931,6 +1962,10 @@ Item { id: screenplayEditorComponent ScreenplayEditor { + HelpTipNotification { + tipName: "screenplay" + } + // zoomLevelModifier: mainTabBar.currentIndex > 0 ? -3 : 0 Component.onCompleted: { const evalZoomLevelModifierFn = () => { @@ -2132,7 +2167,12 @@ Item { anchors.bottom: parent.bottom visible: !showNotebookInStructure || structureEditorTabs.currentTabIndex === 0 active: structureAppFeature.enabled - sourceComponent: StructureView { } + sourceComponent: StructureView { + HelpTipNotification { + tipName: "structure" + enabled: structureViewLoader.visible + } + } DisabledFeatureNotice { anchors.fill: parent @@ -2766,4 +2806,24 @@ Item { visible: refCount > 0 property int refCount: 0 } + + HelpTipNotification { + id: htNotification + enabled: tipName !== "" + + Component.onCompleted: { + Qt.callLater( () => { + if(helpNotificationSettings.dayZero === "") + helpNotificationSettings.dayZero = new Date() + + const days = helpNotificationSettings.daysSinceZero() + if(days >= 2) { + if(!helpNotificationSettings.isTipShown("discord")) + htNotification.tipName = "discord" + else if(!helpNotificationSettings.isTipShown("subscription") && days >= 5) + htNotification.tipName = "subscription" + } + }) + } + } } diff --git a/qml/StructureGroupsMenu.qml b/qml/StructureGroupsMenu.qml index 960720ab..86826bc2 100644 --- a/qml/StructureGroupsMenu.qml +++ b/qml/StructureGroupsMenu.qml @@ -21,6 +21,8 @@ Menu2 { property SceneGroup sceneGroup: null signal toggled(int row, string name) + closePolicy: htn.Notification.active ? Popup.NoAutoClose : Popup.CloseOnEscape|Popup.CloseOnPressOutside + enabled: false title: "Tag Groups" property string innerTitle: "" @@ -28,6 +30,12 @@ Menu2 { width: 450 height: 500 + HelpTipNotification { + id: htn + tipName: "story_beat_tagging" + enabled: structureGroupsMenu.opened + } + MenuItem2 { width: structureGroupsMenu.width height: structureGroupsMenu.height diff --git a/qml/UserLogin.qml b/qml/UserLogin.qml index 85c4b6bc..7380245b 100644 --- a/qml/UserLogin.qml +++ b/qml/UserLogin.qml @@ -541,7 +541,7 @@ Item { TabSequenceItem.sequence: 1 maximumLength: 128 onTextEdited: allowHighlightSaveAnimation = true - completionStrings: ["Novice", "Learning", "Written Few, None Made", "Hobby Writer", "Working Writer", "Actively Pursuing a Writing Career", "Have Produced Credits", "Experienced"] + completionStrings: ["Hobby Writer", "Actively Pursuing a Writing Career", "Working Writer", "Have Produced Credits"] minimumCompletionPrefixLength: 0 maxCompletionItems: -1 maxVisibleItems: 6 diff --git a/scrite_ui.qrc b/scrite_ui.qrc index 0ba6e92d..9f26f2d8 100644 --- a/scrite_ui.qrc +++ b/scrite_ui.qrc @@ -88,5 +88,6 @@ qml/DocumentVault.qml qml/SceneFeaturedImage.qml qml/RichTextEdit.qml + qml/HelpTipNotification.qml diff --git a/src/core/application.h b/src/core/application.h index 1489a738..f1938018 100644 --- a/src/core/application.h +++ b/src/core/application.h @@ -54,6 +54,8 @@ class Application : public QtApplicationClass QString installationId() const; QDateTime installationTimestamp() const; + + Q_PROPERTY(int launchCounter READ launchCounter CONSTANT) int launchCounter() const; Q_PROPERTY(QString buildTimestamp READ buildTimestamp CONSTANT) diff --git a/src/network/jsonhttprequest.cpp b/src/network/jsonhttprequest.cpp index e47d6d52..0061f407 100644 --- a/src/network/jsonhttprequest.cpp +++ b/src/network/jsonhttprequest.cpp @@ -358,6 +358,7 @@ bool JsonHttpRequest::call() + JsonHttpRequest::platformType() + space + JsonHttpRequest::clientId(); return ret; }(); + if (!userAgentString.isEmpty()) req.setHeader(QNetworkRequest::UserAgentHeader, userAgentString); diff --git a/src/network/user.cpp b/src/network/user.cpp index 6bbb7366..ef68f5b0 100644 --- a/src/network/user.cpp +++ b/src/network/user.cpp @@ -297,12 +297,51 @@ void User::setInstallations(const QJsonArray &val) emit installationsChanged(); } +void User::setHelpTips(const QJsonObject &val) +{ + if (m_helpTips == val) + return; + + m_helpTips = val; + emit helpTipsChanged(); + + const QByteArray json = QJsonDocument(val).toJson(); + JsonHttpRequest::store(QStringLiteral("helpTips"), json.toBase64()); +} + +void User::loadStoredHelpTips() +{ + const QByteArray base64 = JsonHttpRequest::fetch(QStringLiteral("helpTips")).toByteArray(); + const QByteArray json = QByteArray::fromBase64(base64); + + m_helpTips = QJsonDocument::fromJson(json).object(); + emit helpTipsChanged(); +} + void User::firstReload() { this->loadStoredUserInformation(); + this->fetchHelpTips(); this->reload(); } +void User::fetchHelpTips() +{ + JsonHttpRequest *call = new JsonHttpRequest(this); + call->setAutoDelete(true); + + call->setApi(QStringLiteral("user/helpTips")); + call->setType(JsonHttpRequest::GET); + connect(call, &JsonHttpRequest::finished, this, [=]() { + if (call->hasError() || !call->hasResponse()) { + this->loadStoredHelpTips(); + return; // Use stored credentials + } + this->setHelpTips(call->responseData()); + }); + call->call(); +} + void User::reset() { ScriteDocument *document = ScriteDocument::instance(); diff --git a/src/network/user.h b/src/network/user.h index c9f28bff..abaa106a 100644 --- a/src/network/user.h +++ b/src/network/user.h @@ -68,6 +68,10 @@ class User : public QObject QJsonArray installations() const { return m_installations; } Q_SIGNAL void installationsChanged(); + Q_PROPERTY(QJsonObject helpTips READ helpTips NOTIFY helpTipsChanged) + QJsonObject helpTips() const { return m_helpTips; } + Q_SIGNAL void helpTipsChanged(); + Q_PROPERTY(QStringList locations READ locations CONSTANT) Q_INVOKABLE static QStringList locations(); @@ -103,9 +107,12 @@ class User : public QObject User(QObject *parent = nullptr); void setInfo(const QJsonObject &val); void setInstallations(const QJsonArray &val); + void setHelpTips(const QJsonObject &val); + void loadStoredHelpTips(); Q_SLOT void firstReload(); + void fetchHelpTips(); void reset(); void activateCallDone(); void userInfoCallDone(); @@ -124,6 +131,7 @@ class User : public QObject private: bool m_busy = false; QJsonObject m_info; + QJsonObject m_helpTips; QTimer m_touchLogTimer; QList m_enabledFeatures; QJsonArray m_installations; diff --git a/src/quick/objects/notification.cpp b/src/quick/objects/notification.cpp index 4033e6e1..832d94e2 100644 --- a/src/quick/objects/notification.cpp +++ b/src/quick/objects/notification.cpp @@ -69,6 +69,15 @@ void Notification::setTextColor(const QColor &val) emit textColorChanged(); } +void Notification::setImage(const QUrl &val) +{ + if (m_image == val) + return; + + m_image = val; + emit imageChanged(); +} + void Notification::setActive(bool val) { if (m_active == val) @@ -136,6 +145,15 @@ void Notification::notifyButtonClick(int index) this->setActive(false); } +void Notification::notifyImageClick() +{ + if (!m_image.isEmpty()) { + emit imageClicked(); + + this->setActive(false); + } +} + void Notification::doAutoClose() { if (m_active && m_autoClose) diff --git a/src/quick/objects/notification.h b/src/quick/objects/notification.h index cf1aa7de..05b90469 100644 --- a/src/quick/objects/notification.h +++ b/src/quick/objects/notification.h @@ -53,6 +53,14 @@ class Notification : public QObject QColor textColor() const { return m_textColor; } Q_SIGNAL void textColorChanged(); + Q_PROPERTY(QUrl image READ image WRITE setImage NOTIFY imageChanged) + void setImage(const QUrl &val); + QUrl image() const { return m_image; } + Q_SIGNAL void imageChanged(); + + Q_PROPERTY(bool hasImage READ hasImage NOTIFY imageChanged) + bool hasImage() const { return !m_image.isEmpty(); } + Q_PROPERTY(bool active READ active WRITE setActive NOTIFY activeChanged) void setActive(bool val); bool active() const { return m_active; } @@ -73,11 +81,16 @@ class Notification : public QObject QStringList buttons() const { return m_buttons; } Q_SIGNAL void buttonsChanged(); + Q_PROPERTY(bool hasButtons READ hasButtons NOTIFY buttonsChanged) + bool hasButtons() const { return !m_buttons.isEmpty(); } + Q_INVOKABLE void notifyButtonClick(int index); + Q_INVOKABLE void notifyImageClick(); signals: void dismissed(); void buttonClicked(int index); + void imageClicked(); private: void doAutoClose(); @@ -85,6 +98,7 @@ class Notification : public QObject private: bool m_active = false; + QUrl m_image; QColor m_color = QColor(Qt::white); QString m_text; QString m_title;