From 86b84ea17e166f21c023b4fc99672e7457c998cf Mon Sep 17 00:00:00 2001 From: Nicolas Vengeon <nicolas.vengeon@savoirfairelinux.com> Date: Tue, 10 Jan 2023 16:47:18 -0500 Subject: [PATCH] feature: save participant's view Change-Id: I790f10542aed306a7416a4ce79f2eaf7a770135a Gitlab: #698 --- src/app/appsettingsmanager.h | 1 + src/app/calladapter.cpp | 23 +++++- src/app/calladapter.h | 2 + .../contextmenu/GeneralMenuItem.qml | 1 + src/app/constant/JamiStrings.qml | 10 ++- src/app/constant/JamiTheme.qml | 5 ++ src/app/mainapplication.cpp | 2 + src/app/mainview/components/CallOverlay.qml | 17 ++++- .../components/CallViewContextMenu.qml | 32 +++++++++ .../KeyboardShortcutKeyDelegate.qml | 4 +- .../components/KeyboardShortcutTable.qml | 34 +++++++++ .../mainview/components/OngoingCallPage.qml | 22 +++++- .../components/ParticipantOverlay.qml | 39 +++++++++- .../mainview/components/ParticipantsLayer.qml | 17 ++++- src/app/mainview/components/Toast.qml | 72 +++++++++++++++++++ src/app/mainview/components/ToastManager.qml | 28 ++++++++ .../components/RecordingSettings.qml | 68 ++++++++++++++++-- src/app/utilsadapter.cpp | 23 ++++++ src/app/utilsadapter.h | 2 + src/app/videoprovider.cpp | 9 ++- src/app/videoprovider.h | 1 + src/libclient/api/accountmodel.h | 1 + 22 files changed, 395 insertions(+), 18 deletions(-) create mode 100644 src/app/mainview/components/Toast.qml create mode 100644 src/app/mainview/components/ToastManager.qml diff --git a/src/app/appsettingsmanager.h b/src/app/appsettingsmanager.h index 30a0573ac..b71bb598b 100644 --- a/src/app/appsettingsmanager.h +++ b/src/app/appsettingsmanager.h @@ -36,6 +36,7 @@ extern const QString defaultDownloadPath; #define KEYS \ X(MinimizeOnClose, true) \ X(DownloadPath, defaultDownloadPath) \ + X(ScreenshotPath, {}) \ X(EnableNotifications, true) \ X(EnableTypingIndicator, true) \ X(EnableReadReceipt, true) \ diff --git a/src/app/calladapter.cpp b/src/app/calladapter.cpp index 3c5d99bf2..645fe3e06 100644 --- a/src/app/calladapter.cpp +++ b/src/app/calladapter.cpp @@ -553,7 +553,7 @@ CallAdapter::fillParticipantData(QJsonObject& participant) const auto uri = participant[URI].toString(); participant[ISLOCAL] = false; if (uri == accInfo.profileInfo.uri && participant[DEVICE] == getCurrentDeviceId(accInfo)) { - participant[BESTNAME] = tr("me"); + participant[BESTNAME] = tr("Me"); participant[ISLOCAL] = true; } else { try { @@ -1145,6 +1145,27 @@ CallAdapter::updateAdvancedInformation() } } +bool +CallAdapter::takeScreenshot(const QImage& image, const QString& path) +{ + QString name = QString("%1 %2") + .arg(tr("Screenshot")) + .arg(QDateTime::currentDateTime().toString(Qt::ISODate)); + + bool fileAlreadyExists = true; + int nb = 0; + QString filePath = QString("%1%2.png").arg(path).arg(name); + while (fileAlreadyExists) { + filePath = QString("%1%2.png").arg(path).arg(name); + if (nb) + filePath = QString("%1(%2).png").arg(filePath).arg(QString::number(nb)); + QFileInfo check_file(filePath); + fileAlreadyExists = check_file.exists() && check_file.isFile(); + nb++; + } + return image.save(filePath, "PNG"); +} + void CallAdapter::preventScreenSaver(bool state) { diff --git a/src/app/calladapter.h b/src/app/calladapter.h index e83387e6a..8f37d8a22 100644 --- a/src/app/calladapter.h +++ b/src/app/calladapter.h @@ -98,6 +98,8 @@ public: Q_INVOKABLE void setCallInfo(); Q_INVOKABLE void updateAdvancedInformation(); + Q_INVOKABLE bool takeScreenshot(const QImage &image, const QString &path); + Q_SIGNALS: void callStatusChanged(int index, const QString& accountId, const QString& convUid); void callInfosChanged(const QVariant& infos, const QString& accountId, const QString& convUid); diff --git a/src/app/commoncomponents/contextmenu/GeneralMenuItem.qml b/src/app/commoncomponents/contextmenu/GeneralMenuItem.qml index d42b5cd5e..6d77c9acb 100644 --- a/src/app/commoncomponents/contextmenu/GeneralMenuItem.qml +++ b/src/app/commoncomponents/contextmenu/GeneralMenuItem.qml @@ -49,6 +49,7 @@ MenuItem { property int itemTextMargin: 20 signal clicked + property bool itemHovered: menuItemContentRect.hovered contentItem: AbstractButton { id: menuItemContentRect diff --git a/src/app/constant/JamiStrings.qml b/src/app/constant/JamiStrings.qml index f69be343e..20ff8d70e 100644 --- a/src/app/constant/JamiStrings.qml +++ b/src/app/constant/JamiStrings.qml @@ -298,6 +298,8 @@ Item { property string lowerHand: qsTr("Lower hand") property string raiseHand: qsTr("Raise hand") property string layoutSettings: qsTr("Layout settings") + property string tileScreenshot: qsTr("Take tile screenshot") + property string screenshotTaken: qsTr("Screenshot saved to %1") //advanced information property string renderersInformation: qsTr("Renderers information") @@ -508,15 +510,16 @@ Item { // Context Menu property string saveFile: qsTr("Save file") property string openLocation: qsTr("Open location") + property string me: qsTr("Me") // Updates property string betaInstall: qsTr("Install beta version") property string checkForUpdates: qsTr("Check for updates now") property string enableAutoUpdates: qsTr("Enable/Disable automatic updates") - property string tipAutoUpdate: qsTr("toggle automatic updates") + property string tipAutoUpdate: qsTr("Toggle automatic updates") property string updatesTitle: qsTr("Updates") property string updateDialogTitle: qsTr("Update") - property string updateFound: qsTr("A new version of Jami was found\n Would you like to update now?") + property string updateFound: qsTr("A new version of Jami was found\nWould you like to update now?") property string updateNotFound: qsTr("No new version of Jami was found") property string updateCheckError: qsTr("An error occured when checking for a new version") property string updateNetworkError: qsTr("Network error") @@ -538,7 +541,8 @@ Item { // Recording Settings property string tipRecordFolder: qsTr("Select a record directory") property string quality: qsTr("Quality") - property string saveIn: qsTr("Save in") + property string saveRecordingsTo: qsTr("Save recordings to") + property string saveScreenshotsTo: qsTr("Save screenshots to") property string callRecording: qsTr("Call Recording") property string alwaysRecordCalls: qsTr("Always record calls") diff --git a/src/app/constant/JamiTheme.qml b/src/app/constant/JamiTheme.qml index 80c71c474..b81d13441 100644 --- a/src/app/constant/JamiTheme.qml +++ b/src/app/constant/JamiTheme.qml @@ -146,6 +146,11 @@ Item { property color spinboxBackgroundColor: darkTheme ? editBackgroundColor : selectedColor property color spinboxBorderColor: darkTheme ? tintedBlue : Qt.rgba(0, 0.34, 0.6, 0.36) + //Toast + property color toastColor: darkTheme ? "#f0f0f0" : "#000000" + property color toastRectColor: !darkTheme ? "#f0f0f0" : "#000000" + property real toastFontSize: calcSize(15) + // Call buttons property color acceptButtonGreen: "#4caf50" property color acceptButtonHoverGreen: "#5db761" diff --git a/src/app/mainapplication.cpp b/src/app/mainapplication.cpp index efa1012a3..8119764cb 100644 --- a/src/app/mainapplication.cpp +++ b/src/app/mainapplication.cpp @@ -174,10 +174,12 @@ MainApplication::init() Qt::DirectConnection); auto downloadPath = settingsManager_->getValue(Settings::Key::DownloadPath); + auto screenshotPath = settingsManager_->getValue(Settings::Key::ScreenshotPath); auto allowTransferFromTrusted = settingsManager_->getValue(Settings::Key::AutoAcceptFiles) .toBool(); auto acceptTransferBelow = settingsManager_->getValue(Settings::Key::AcceptTransferBelow).toInt(); lrcInstance_->accountModel().downloadDirectory = downloadPath.toString() + "/"; + lrcInstance_->accountModel().screenshotDirectory = screenshotPath.toString(); lrcInstance_->accountModel().autoTransferFromTrusted = allowTransferFromTrusted; lrcInstance_->accountModel().autoTransferSizeThreshold = acceptTransferBelow; diff --git a/src/app/mainview/components/CallOverlay.qml b/src/app/mainview/components/CallOverlay.qml index a506609ed..0a8f6d9a5 100644 --- a/src/app/mainview/components/CallOverlay.qml +++ b/src/app/mainview/components/CallOverlay.qml @@ -39,6 +39,7 @@ Item { signal chatButtonClicked signal fullScreenClicked + signal closeClicked function closeContextMenuAndRelatedWindows() { ContactPickerCreation.closeContactPicker() @@ -46,14 +47,22 @@ Item { SelectScreenWindowCreation.destroySelectScreenWindow() ScreenRubberBandCreation.destroyScreenRubberBandWindow() PluginHandlerPickerCreation.closePluginHandlerPicker() + root.closeClicked() callInformationOverlay.close() } // x, y position does not need to be translated // since they all fill the call page - function openCallViewContextMenuInPos(x, y) { + function openCallViewContextMenuInPos(x, y, + hoveredOverlayUri, + hoveredOverlaySinkId, + hoveredOverVideoMuted) + { callViewContextMenu.x = x callViewContextMenu.y = y + callViewContextMenu.hoveredOverlayUri = hoveredOverlayUri + callViewContextMenu.hoveredOverlaySinkId = hoveredOverlaySinkId + callViewContextMenu.hoveredOverVideoMuted = hoveredOverVideoMuted callViewContextMenu.openMenu() } @@ -171,10 +180,16 @@ Item { onTransferCallButtonClicked: openContactPicker(ContactList.TRANSFER) onPluginItemClicked: openPluginsMenu() + onScreenshotTaken: { + toastManager.instantiateToast(); + } onRecordCallClicked: CallAdapter.recordThisCallToggle() onOpenSelectionWindow: { SelectScreenWindowCreation.createSelectScreenWindowObject(appWindow) SelectScreenWindowCreation.showSelectScreenWindow(callPreviewId, windowSelection) } + onScreenshotButtonHoveredChanged: { + participantsLayer.screenshotButtonHovered = screenshotButtonHovered + } } } diff --git a/src/app/mainview/components/CallViewContextMenu.qml b/src/app/mainview/components/CallViewContextMenu.qml index 8f27ed55b..7d00825a4 100644 --- a/src/app/mainview/components/CallViewContextMenu.qml +++ b/src/app/mainview/components/CallViewContextMenu.qml @@ -37,6 +37,12 @@ ContextMenuAutoLoader { signal transferCallButtonClicked signal recordCallClicked signal openSelectionWindow + signal screenshotTaken + property bool screenshotButtonHovered: screenShot.itemHovered + + property string hoveredOverlayUri: "" + property string hoveredOverlaySinkId: "" + property bool hoveredOverVideoMuted: true property list<GeneralMenuItem> menuItems: [ GeneralMenuItem { @@ -194,8 +200,34 @@ ContextMenuAutoLoader { CallAdapter.startTimerInformation(); callInformationOverlay.open() } + }, + GeneralMenuItem { + id: screenShot + + canTrigger: hoveredOverlayUri !== "" && hoveredOverVideoMuted === false + itemName: JamiStrings.tileScreenshot + iconSource: JamiResources.baseline_camera_alt_24dp_svg + + MaterialToolTip { + id: tooltip + + parent: screenShot + visible: screenShot.itemHovered + delay: Qt.styleHints.mousePressAndHoldInterval + property bool isMe: CurrentAccount.uri === hoveredOverlayUri + text: isMe ? JamiStrings.me + : UtilsAdapter.getBestNameForUri(CurrentAccount.id, hoveredOverlayUri) + } + + onClicked: { + if (CallAdapter.takeScreenshot(videoProvider.captureRawVideoFrame(hoveredOverlaySinkId), + UtilsAdapter.getDirScreenshot())) { + screenshotTaken() + } + } } ] + Component.onCompleted: menuItemsToLoad = menuItems } diff --git a/src/app/mainview/components/KeyboardShortcutKeyDelegate.qml b/src/app/mainview/components/KeyboardShortcutKeyDelegate.qml index b354c3bd3..c5ee9d804 100644 --- a/src/app/mainview/components/KeyboardShortcutKeyDelegate.qml +++ b/src/app/mainview/components/KeyboardShortcutKeyDelegate.qml @@ -62,7 +62,9 @@ RowLayout { anchors.centerIn: parent - text: shortcut + text: shortcut2 === "" ? + shortcut : + shortcut + " + " + shortcut2 font.pointSize: JamiTheme.textFontSize + 3 font.weight: Font.DemiBold color: JamiTheme.textColor diff --git a/src/app/mainview/components/KeyboardShortcutTable.qml b/src/app/mainview/components/KeyboardShortcutTable.qml index 377af5538..3cc455bba 100644 --- a/src/app/mainview/components/KeyboardShortcutTable.qml +++ b/src/app/mainview/components/KeyboardShortcutTable.qml @@ -40,42 +40,52 @@ Window { ListElement { shortcut: "Ctrl + J" + shortcut2: "" description: qsTr("Open account list") } ListElement { shortcut: "Ctrl + L" + shortcut2: "" description: qsTr("Focus conversations list") } ListElement { shortcut: "Ctrl + R" + shortcut2: "" description: qsTr("Requests list") } ListElement { shortcut: "Ctrl + ↑" + shortcut2: "" description: qsTr("Previous conversation") } ListElement { shortcut: "Ctrl + ↓" + shortcut2: "" description: qsTr("Next conversation") } ListElement { shortcut: "Ctrl + F" + shortcut2: "" description: qsTr("Search bar") } ListElement { shortcut: "F11" + shortcut2: "" description: qsTr("Full screen") } ListElement { shortcut: "Ctrl + +" + shortcut2: "" description: qsTr("Increase font size") } ListElement { shortcut: "Ctrl + -" + shortcut2: "" description: qsTr("Decrease font size") } ListElement { shortcut: "Ctrl + 0" + shortcut2: "" description: qsTr("Reset font size") } } @@ -85,34 +95,42 @@ Window { ListElement { shortcut: "Ctrl + Shift + C" + shortcut2: "" description: qsTr("Start an audio call") } ListElement { shortcut: "Ctrl + Shift + X" + shortcut2: "" description: qsTr("Start a video call") } ListElement { shortcut: "Ctrl + Shift + L" + shortcut2: "" description: qsTr("Clear history") } ListElement { shortcut: "Ctrl + Shift + B" + shortcut2: "" description: qsTr("Block contact") } ListElement { shortcut: "Ctrl + Shift + Delete" + shortcut2: "" description: qsTr("Remove conversation") } ListElement { shortcut: "Shift + Ctrl + A" + shortcut2: "" description: qsTr("Accept contact request") } ListElement { shortcut: "↑" + shortcut2: "" description: qsTr("Edit last message") } ListElement { shortcut: "Esc" + shortcut2: "" description: qsTr("Cancel message edition") } } @@ -122,26 +140,32 @@ Window { ListElement { shortcut: "Ctrl + M" + shortcut2: "" description: qsTr("Media settings") } ListElement { shortcut: "Ctrl + G" + shortcut2: "" description: qsTr("General settings") } ListElement { shortcut: "Ctrl + I" + shortcut2: "" description: qsTr("Account settings") } ListElement { shortcut: "Ctrl + P" + shortcut2: "" description: qsTr("Plugin settings") } ListElement { shortcut: "Ctrl + Shift + N" + shortcut2: "" description: qsTr("Open account creation wizard") } ListElement { shortcut: "F10" + shortcut2: "" description: qsTr("Open keyboard shortcut table") } } @@ -151,24 +175,34 @@ Window { ListElement { shortcut: "Ctrl + Y" + shortcut2: "" description: qsTr("Answer an incoming call") } ListElement { shortcut: "Ctrl + D" + shortcut2: "" description: qsTr("End call") } ListElement { shortcut: "Ctrl + Shift + D" + shortcut2: "" description: qsTr("Decline the call request") } ListElement { shortcut: "M" + shortcut2: "" description: qsTr("Mute microphone") } ListElement { shortcut: "V" + shortcut2: "" description: qsTr("Stop camera") } + ListElement { + shortcut: "Ctrl" + shortcut2: qsTr("Mouse middle click") + description: qsTr("Take tile screenshot") + } } Rectangle { diff --git a/src/app/mainview/components/OngoingCallPage.qml b/src/app/mainview/components/OngoingCallPage.qml index a1adc306b..ad147b5a6 100644 --- a/src/app/mainview/components/OngoingCallPage.qml +++ b/src/app/mainview/components/OngoingCallPage.qml @@ -166,7 +166,10 @@ Rectangle { onTapped: function (eventPoint, button) { if (button === Qt.RightButton) { callOverlay.openCallViewContextMenuInPos(eventPoint.position.x, - eventPoint.position.y) + eventPoint.position.y, + participantsLayer.hoveredOverlayUri, + participantsLayer.hoveredOverlaySinkId, + participantsLayer.hoveredOverVideoMuted) } } } @@ -184,6 +187,7 @@ Rectangle { ParticipantsLayer { id: participantsLayer + anchors.fill: parent anchors.centerIn: parent anchors.margins: 1 @@ -191,9 +195,18 @@ Rectangle { participantsSide: callOverlay.participantsSide } + ToastManager { + id: toastManager + + anchors.fill: parent + + function instantiateToast() { + instantiate(JamiStrings.screenshotTaken.arg(UtilsAdapter.getDirScreenshot()),1000,400) + } + } + LocalVideo { id: previewRenderer - visible: (CurrentCall.isSharing || !CurrentCall.isVideoMuted) && !CurrentCall.isConference @@ -329,6 +342,11 @@ Rectangle { openInCallConversation() } } + onCloseClicked: { + participantsLayer.hoveredOverlayUri = "" + participantsLayer.hoveredOverlaySinkId = "" + participantsLayer.hoveredOverVideoMuted = true + } onChatButtonClicked: { inCallMessageWebViewStack.visible ? diff --git a/src/app/mainview/components/ParticipantOverlay.qml b/src/app/mainview/components/ParticipantOverlay.qml index 3de0ad19a..3bbd4af9a 100644 --- a/src/app/mainview/components/ParticipantOverlay.qml +++ b/src/app/mainview/components/ParticipantOverlay.qml @@ -67,6 +67,18 @@ Item { property string muteAlertMessage: "" property bool muteAlertActive: false + property bool participantHovered: hoverIndicator.hovered + property bool isScreenshotButtonHovered: false + + function takeScreenshot() { + if (!hoveredOverVideoMuted) { + if (CallAdapter.takeScreenshot(videoProvider.captureRawVideoFrame(hoveredOverlaySinkId), + UtilsAdapter.getDirScreenshot())) { + toastManager.instantiateToast(); + } + } + } + onMuteAlertActiveChanged: { if (muteAlertActive) { alertTimer.restart() @@ -94,9 +106,11 @@ Item { Rectangle { z: -1 - color: JamiTheme.buttonTintedBlue + border.color: JamiTheme.buttonTintedBlue + border.width: 2 + color: "transparent" radius: 10 - visible:voiceActive + visible: voiceActive || isScreenshotButtonHovered width: participantIsActive ? mediaDistRender.contentRect.width + 2 : undefined height: participantIsActive ? mediaDistRender.contentRect.height + 2 : undefined anchors.centerIn: participantIsActive ? parent : undefined @@ -109,7 +123,6 @@ Item { anchors.margins: 2 rendererId: root.sinkId crop: !participantIsActive - underlayItems: Avatar { property real componentSize: Math.min(mediaDistRender.contentRect.width / 2, mediaDistRender.contentRect.height / 2) height: componentSize @@ -140,7 +153,25 @@ Item { anchors.centerIn: participantIsActive ? parent : undefined anchors.fill: participantIsActive ? undefined : parent + TapHandler { + acceptedButtons: Qt.MiddleButton + acceptedModifiers: Qt.ControlModifier + onTapped: { + takeScreenshot() + } + } + + MultiPointTouchArea { + anchors.fill: parent + minimumTouchPoints: 3 + onPressed: { + takeScreenshot() + } + } + HoverHandler { + id: hoverIndicator + onPointChanged: { participantRect.opacity = 1 fadeOutTimer.restart() @@ -164,6 +195,7 @@ Item { // Participant buttons for moderation ParticipantOverlayMenu { id: overlayMenu + visible: isMe || meModerator anchors.fill: parent @@ -209,6 +241,7 @@ Item { RowLayout { id: participantFootInfo + height: parent.height anchors.verticalCenter: parent.verticalCenter Text { diff --git a/src/app/mainview/components/ParticipantsLayer.qml b/src/app/mainview/components/ParticipantsLayer.qml index fa17aecca..9b20f5432 100644 --- a/src/app/mainview/components/ParticipantsLayer.qml +++ b/src/app/mainview/components/ParticipantsLayer.qml @@ -37,6 +37,10 @@ Item { property bool inLine: CallParticipantsModel.conferenceLayout === CallParticipantsModel.ONE_WITH_SMALL property bool participantsSide property bool enableHideSpectators: CallParticipantsModel.count > 1 && CurrentCall.hideSpectators + property string hoveredOverlayUri: "" + property string hoveredOverlaySinkId: "" + property bool hoveredOverVideoMuted: true + property bool screenshotButtonHovered: false onVisibleChanged: { CurrentCall.hideSelf = UtilsAdapter.getAppValue(Settings.HideSelf) @@ -51,7 +55,10 @@ Item { anchors.fill: parent anchors.leftMargin: leftMargin_ - + isScreenshotButtonHovered: screenshotButtonHovered && hoveredOverlaySinkId === sinkId_ + opacity: screenshotButtonHovered + ? hoveredOverlaySinkId !== sinkId ? 0.1 : 1 + : 1 sinkId: sinkId_ uri: uri_ deviceId: deviceId_ @@ -70,6 +77,14 @@ Item { participantIsModeratorMuted: audioModeratorMuted_ participantHandIsRaised: isHandRaised_ + onParticipantHoveredChanged: { + if (participantHovered) { + hoveredOverlayUri = overlay.uri + hoveredOverlaySinkId = overlay.sinkId + hoveredOverVideoMuted = videoMuted_ + } + } + Connections { id: registeredNameFoundConnection diff --git a/src/app/mainview/components/Toast.qml b/src/app/mainview/components/Toast.qml new file mode 100644 index 000000000..db2347b62 --- /dev/null +++ b/src/app/mainview/components/Toast.qml @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2023 Savoir-faire Linux Inc. + * Author: Vengeon Nicolas <nicolas.vengeon@savoirfairelinux.com> + * + * 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; either version 3 of the License, or + * (at your option) any later version. + * + * 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 <https://www.gnu.org/licenses/>. + */ + +import QtQuick +import net.jami.Constants 1.1 + +Rectangle { + id: root + + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + width: textMessage.width + 20 + height: textMessage.height + 10 + anchors.topMargin: 10 + radius: 15 + color: JamiTheme.toastRectColor + + property int duration + property int fadingTime + property string message + + Component.onCompleted: { + anim.start(); + } + + Text { + id: textMessage + + anchors.centerIn: root + text: message + font.pointSize: JamiTheme.toastFontSize + color: JamiTheme.toastColor + } + + SequentialAnimation on opacity { + id: anim + + running: false + + NumberAnimation { + to: 0.9 + duration: root.fadingTime + } + PauseAnimation { + duration: root.duration + } + NumberAnimation { + to: 0 + duration: root.fadingTime + } + + onRunningChanged: { + if (!running) + root.destroy(); + } + } +} diff --git a/src/app/mainview/components/ToastManager.qml b/src/app/mainview/components/ToastManager.qml new file mode 100644 index 000000000..3ade93324 --- /dev/null +++ b/src/app/mainview/components/ToastManager.qml @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023 Savoir-faire Linux Inc. + * Author: Vengeon Nicolas <nicolas.vengeon@savoirfairelinux.com> + * + * 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; either version 3 of the License, or + * (at your option) any later version. + * + * 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 <https://www.gnu.org/licenses/>. + */ + +import QtQuick + +Item { + id: root + + function instantiate(message, duration, fadingTime) { + var component = Qt.createComponent("Toast.qml"); + var sprite = component.createObject(root, {message: message, duration: duration, fadingTime: fadingTime}); + } +} diff --git a/src/app/settingsview/components/RecordingSettings.qml b/src/app/settingsview/components/RecordingSettings.qml index 0ea97e8f3..b52d3284d 100644 --- a/src/app/settingsview/components/RecordingSettings.qml +++ b/src/app/settingsview/components/RecordingSettings.qml @@ -28,24 +28,32 @@ import net.jami.Constants 1.1 import "../../commoncomponents" ColumnLayout { - id:root + id: root property int itemWidth property string recordPath: AVModel.getRecordPath() + property string screenshotPath: UtilsAdapter.getDirScreenshot() onRecordPathChanged: { - if(recordPath === "") return + if(recordPath === "") + return - if(AVModel){ + if(AVModel) { AVModel.setRecordPath(recordPath) } } + onScreenshotPathChanged: { + if (screenshotPath === "") + return + UtilsAdapter.setScreenshotPath(screenshotPath) + } + FolderDialog { id: recordPathDialog title: JamiStrings.selectFolder - currentFolder: StandardPaths.writableLocation(StandardPaths.HomeLocation) + currentFolder: UtilsAdapter.getDirScreenshot() options: FolderDialog.ShowDirsOnly onAccepted: { @@ -54,6 +62,19 @@ ColumnLayout { } } + FolderDialog { + id: screenshotPathDialog + + title: JamiStrings.selectFolder + currentFolder: StandardPaths.writableLocation(StandardPaths.PicturesLocation) + options: FolderDialog.ShowDirsOnly + + onAccepted: { + var dir = UtilsAdapter.getAbsPath(folder.toString()) + screenshotPath = dir + } + } + Timer{ id: updateRecordQualityTimer @@ -172,7 +193,7 @@ ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true - text: JamiStrings.saveIn + text: JamiStrings.saveRecordingsTo color: JamiTheme.textColor font.pointSize: JamiTheme.settingsFontSize font.kerning: true @@ -199,4 +220,41 @@ ColumnLayout { onClicked: recordPathDialog.open() } } + + RowLayout { + Layout.fillWidth: true + Layout.preferredHeight: JamiTheme.preferredFieldHeight + Layout.leftMargin: JamiTheme.preferredMarginSize + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + + text: JamiStrings.saveScreenshotsTo + color: JamiTheme.textColor + font.pointSize: JamiTheme.settingsFontSize + font.kerning: true + + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + } + + MaterialButton { + id: screenshotPathButton + + Layout.alignment: Qt.AlignRight + + preferredWidth: itemWidth + preferredHeight: JamiTheme.preferredFieldHeight + + toolTipText: UtilsAdapter.getDirScreenshot() + text: screenshotPath + iconSource: JamiResources.round_folder_24dp_svg + color: JamiTheme.buttonTintedGrey + hoveredColor: JamiTheme.buttonTintedGreyHovered + pressedColor: JamiTheme.buttonTintedGreyPressed + + onClicked: screenshotPathDialog.open() + } + } } diff --git a/src/app/utilsadapter.cpp b/src/app/utilsadapter.cpp index 49d6c2a01..e32f5c576 100644 --- a/src/app/utilsadapter.cpp +++ b/src/app/utilsadapter.cpp @@ -387,6 +387,22 @@ UtilsAdapter::getDirDocument() QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); } +QString +UtilsAdapter::getDirScreenshot() +{ + QString screenshotPath = lrcInstance_->accountModel().screenshotDirectory; + if (screenshotPath.isEmpty()) { + QString folderName = "Jami"; + auto picture = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); + QDir dir; + dir.mkdir(picture + QDir::separator() + folderName); + screenshotPath = picture + QDir::separator() + folderName; + setScreenshotPath(screenshotPath); + lrcInstance_->accountModel().screenshotDirectory = screenshotPath; + } + return screenshotPath; +} + QString UtilsAdapter::getDirDownload() { @@ -425,6 +441,13 @@ UtilsAdapter::setDownloadPath(QString dir) lrcInstance_->accountModel().downloadDirectory = dir + "/"; } +void +UtilsAdapter::setScreenshotPath(QString dir) +{ + setAppValue(Settings::Key::ScreenshotPath, dir); + lrcInstance_->accountModel().screenshotDirectory = dir + QDir::separator(); +} + void UtilsAdapter::monitor(const bool& continuous) { diff --git a/src/app/utilsadapter.h b/src/app/utilsadapter.h index 9fb0c96c0..6ee62ad3a 100644 --- a/src/app/utilsadapter.h +++ b/src/app/utilsadapter.h @@ -107,9 +107,11 @@ public: Q_INVOKABLE QVariant getAppValue(const Settings::Key key); Q_INVOKABLE void setAppValue(const Settings::Key key, const QVariant& value); Q_INVOKABLE QString getDirDocument(); + Q_INVOKABLE QString getDirScreenshot(); Q_INVOKABLE QString getDirDownload(); Q_INVOKABLE void setRunOnStartUp(bool state); Q_INVOKABLE void setDownloadPath(QString dir); + Q_INVOKABLE void setScreenshotPath(QString dir); Q_INVOKABLE void monitor(const bool& continuous); Q_INVOKABLE void clearInteractionsCache(const QString& accountId, const QString& convUid); Q_INVOKABLE QVariantMap supportedLang(); diff --git a/src/app/videoprovider.cpp b/src/app/videoprovider.cpp index ac9ccde79..d0320e889 100644 --- a/src/app/videoprovider.cpp +++ b/src/app/videoprovider.cpp @@ -117,6 +117,13 @@ VideoProvider::frame(const QString& id) QString VideoProvider::captureVideoFrame(const QString& id) +{ + auto img = captureRawVideoFrame(id); + return Utils::byteArrayToBase64String(Utils::QImageToByteArray(img)); +} + +QImage +VideoProvider::captureRawVideoFrame(const QString& id) { QMutexLocker framesLk(&framesObjsMutex_); if (auto* videoFrame = frame(id)) { @@ -127,7 +134,7 @@ VideoProvider::captureVideoFrame(const QString& id) videoFrame->height(), videoFrame->bytesPerLine(0), imageFormat); - return Utils::byteArrayToBase64String(Utils::QImageToByteArray(img)); + return img; } return {}; } diff --git a/src/app/videoprovider.h b/src/app/videoprovider.h index 775e42500..6d496de6e 100644 --- a/src/app/videoprovider.h +++ b/src/app/videoprovider.h @@ -46,6 +46,7 @@ public: Q_INVOKABLE void registerSink(const QString& id, QVideoSink* obj); Q_INVOKABLE void unregisterSink(QVideoSink* obj); Q_INVOKABLE QString captureVideoFrame(const QString& id); + Q_INVOKABLE QImage captureRawVideoFrame(const QString& id); private Q_SLOTS: void onRendererStarted(const QString& id, const QSize& size); diff --git a/src/libclient/api/accountmodel.h b/src/libclient/api/accountmodel.h index 1eca832a0..8e1b47a13 100644 --- a/src/libclient/api/accountmodel.h +++ b/src/libclient/api/accountmodel.h @@ -73,6 +73,7 @@ public: * Should contains the full directory with the end marker (/ on linux for example) */ QString downloadDirectory; + QString screenshotDirectory; /** * Accept transfer from trusted contacts */ -- GitLab