import QtQuick import QtQuick.Controls as QQC2 import QtQuick.Layouts import org.kde.kirigami as Kirigami import org.kde.discover as Discover Kirigami.Dialog { id: reviewDialog required property Discover.AbstractResource application required property Discover.AbstractReviewsBackend backend readonly property alias rating: ratingInput.value readonly property alias name: nameInput.text readonly property alias summary: titleInput.text readonly property alias review: reviewInput.text property bool appHasAnyProprietaryLicenses: false implicitWidth: Kirigami.Units.gridUnit * 30 topPadding: 0 leftPadding: Kirigami.Units.largeSpacing rightPadding: Kirigami.Units.largeSpacing bottomPadding: Kirigami.Units.largeSpacing title: i18n("Reviewing %1", application.name) standardButtons: Kirigami.Dialog.NoButton customFooterActions: [ Kirigami.Action { id: submitAction text: i18n("Submit review") icon.name: "document-send" enabled: !instructionalLabel.visible onTriggered: reviewDialog.accept(); } ] contentItem: ColumnLayout { Layout.fillWidth: true spacing: Kirigami.Units.smallSpacing QQC2.Label { Layout.fillWidth: true text: i18n("If you love %1, tell people what's great about it! Focus on functionality, usability, and appearance. If the app didn't meet your needs, explain why.", reviewDialog.application.name) wrapMode: Text.Wrap textFormat: Text.PlainText visible: !reviewMightBeCrapWarning.visible } Kirigami.InlineMessage { id: reviewMightBeCrapWarning readonly property bool packagedByDistro: ![ "flatpak-backend", "snap-backend", "kns-backend", "fwupd-backend" ].includes(reviewDialog.application.backend.name); readonly property string appBugReportUrl: reviewDialog.application.bugURL.toString() Layout.fillWidth: true type: Kirigami.MessageType.Warning text: { if (reviewInput.hasDoesntRunKeywords) { if (packagedByDistro) { return xi18nc("@info", "If %1 isn't launching, consider reporting this to its packagers in %2 at %3 instead, as it may be an easily fixable packaging issue rather than a flaw in the app itself.", reviewDialog.application.name, Discover.ResourcesModel.distroName, Discover.ResourcesModel.distroBugReportUrl()); } else { return xi18nc("@info", "If %1 isn't launching, consider reporting this its developers at %2 instead, as it may be an easily fixable packaging issue rather than a flaw in the app itself.", reviewDialog.application.name, appBugReportUrl); } } else if (reviewInput.hasCrashKeywords && appBugReportUrl.length > 0){ return xi18nc("@info", "If the app is crashing a lot, consider reporting this to the developers at %1 instead.", appBugReportUrl); } else if (reviewInput.hasSubjectiveKeywords && appBugReportUrl.length > 0) { return xi18nc("@info", "If you're having a problem with the app, consider reporting it to the developers at %1 instead, or else describe the problem here in greater detail.", appBugReportUrl); } else { return "" } } visible: { // The "improve your review" UI is app-specific and doesn't apply to KNS. if (reviewDialog.application.backend.name === "kns-backend") { return false; } // Let people leave crappy reviews for proprietary apps, since trying to // communicate with their devs to improve them is usually like screaming // into the void. if (appHasAnyProprietaryLicenses) { return false; } return reviewInput.hasAnySuspiciousKeywords && text.length > 0; } onLinkActivated: link => Qt.openUrlExternally(link) } Kirigami.Separator { Layout.topMargin: Kirigami.Units.largeSpacing Layout.fillWidth: true } Kirigami.FormLayout { Layout.fillWidth: true Rating { id: ratingInput Kirigami.FormData.label: i18n("Rating:") readOnly: false } QQC2.TextField { id: nameInput Kirigami.FormData.label: i18n("Name:") visible: page.reviewsBackend !== null && reviewDialog.backend.preferredUserName.length > 0 Layout.fillWidth: true readOnly: !reviewDialog.backend.supportsNameChange text: visible ? reviewDialog.backend.preferredUserName : "" } QQC2.TextField { id: titleInput Kirigami.FormData.label: i18n("Title:") Layout.fillWidth: true validator: RegularExpressionValidator { regularExpression: /.{3,70}/ } } } QQC2.TextArea { id: reviewInput readonly property string lowercaseText: reviewInput.text.toLowerCase() // Intentionally untranslated to cover the case of people writing reviews // in English when their system is in another language. readonly property string doesntRunKeywordsEn: "launch,run,start" readonly property string doesntRunKeywordsI18n: i18nc("Words the user might use in an unhelpful sentence saying an app is not launching. Preserve the commas. If necessary, add more words used for this in your language and/or shorten to common stems contained by multiple forms of the words.", "launch,run,start") readonly property list doesntRunKeywords: doesntRunKeywordsEn.concat(",", doesntRunKeywordsI18n).split(",") readonly property bool hasDoesntRunKeywords: doesntRunKeywords.some(suspiciousWord => lowercaseText.includes(suspiciousWord)) readonly property string crashKeywordsEn: "crash,segfault" readonly property string crashKeywordsI18n: i18nc("Words the user might use in an unhelpful sentence saying an app crashes. Preserve the commas. If necessary, add more words used for this in your language and/or shorten to common stems contained by multiple forms of the words", "crash,segfault") readonly property list crashKeywords: crashKeywordsEn.concat(",", crashKeywordsI18n).split(",") readonly property bool hasCrashKeywords: crashKeywords.some(suspiciousWord => lowercaseText.includes(suspiciousWord)) readonly property string subjectiveKeywordsEn: "doesn't work,dumb,stupid,crap,junk,suck,terrible,hate" readonly property string subjectiveKeywordsI18n: i18nc("Word the user might use in an unhelpful sentence saying an app isn't very good. Preserve the commas. If necessary, add more words used for this in your language and/or shorten to common stems contained by multiple forms of the words,", "doesn't work,dumb,stupid,crap,junk,suck,terrible,hate") readonly property list subjectiveKeywords: subjectiveKeywordsEn.concat(",", subjectiveKeywordsI18n).split(",") readonly property bool hasSubjectiveKeywords: subjectiveKeywords.some(suspiciousWord => lowercaseText.includes(suspiciousWord)) readonly property bool hasAnySuspiciousKeywords: hasDoesntRunKeywords || hasSubjectiveKeywords || hasCrashKeywords readonly property bool acceptableInput: length > 15 && length < 3000 Layout.fillWidth: true Layout.minimumHeight: Kirigami.Units.gridUnit * 8 KeyNavigation.priority: KeyNavigation.BeforeItem wrapMode: TextEdit.Wrap } QQC2.Label { id: instructionalLabel Layout.fillWidth: true text: { if (rating < 2) { return i18n("Enter a rating"); } if (!titleInput.acceptableInput) { return i18n("Write the title"); } if (reviewInput.length === 0) { return i18n("Write the review"); } if (reviewInput.length < 15) { return i18n("Keep writing…"); } if (reviewInput.length > 3000) { return i18n("Too long!"); } if (nameInput.visible && nameInput.length < 1) { return i18nc("@info:usagetip", "Insert a name"); } return ""; } wrapMode: Text.WordWrap opacity: 0.6 visible: text.length > 0 } } onOpened: { appHasAnyProprietaryLicenses = application.licenses.some(license => license.licenseType === "proprietary") ratingInput.forceActiveFocus(Qt.PopupFocusReason); } Component.onCompleted: { const submitButton = customFooterButton(submitAction); if (submitButton) { reviewInput.KeyNavigation.tab = submitButton; submitButton.KeyNavigation.tab = ratingInput; } } }