Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85711626
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
64 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/contents/ui/Bubble.qml b/src/contents/ui/Bubble.qml
index 43146e7..479a2d4 100644
--- a/src/contents/ui/Bubble.qml
+++ b/src/contents/ui/Bubble.qml
@@ -1,35 +1,35 @@
/*
* This file is part of kazv.
- * SPDX-FileCopyrightText: 2020-2021 Tusooa Zhu <tusooa@kazv.moe>
+ * SPDX-FileCopyrightText: 2020-2023 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.2
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import org.kde.kirigami 2.13 as Kirigami
import '.' as Kazv
Kirigami.AbstractCard {
id: upper
default property var children
readonly property var bubbleSpacing: leftPadding + rightPadding
Layout.fillWidth: false
contentItem: RowLayout {
anchors.left: parent.left
anchors.right: parent.right
property var encryptedIcon: Kirigami.Icon {
source: 'emblem-encrypted-locked'
Layout.preferredHeight: inlineBadgeSize
Layout.preferredWidth: inlineBadgeSize
}
data: [
...(event && event.encrypted ? [encryptedIcon] : []),
...(Array.isArray(upper.children) ? upper.children :
upper.children ? [upper.children] : [])
]
}
}
diff --git a/src/contents/ui/KazvIOMenu.qml b/src/contents/ui/KazvIOMenu.qml
index 075d4db..809d21a 100644
--- a/src/contents/ui/KazvIOMenu.qml
+++ b/src/contents/ui/KazvIOMenu.qml
@@ -1,137 +1,137 @@
/*
* This file is part of kazv.
- * SPDX-FileCopyrightText: 2022 nannanko <nannanko@kazv.moe>
+ * SPDX-FileCopyrightText: 2022-2023 nannanko <nannanko@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.13 as Kirigami
import moe.kazv.mxc.kazv 0.0 as MK
ColumnLayout {
id: kazvIOProgressBar
required property var kazvIOJob
property var jobId
property bool isUpload
visible: kazvIOJob ? true : false
onKazvIOJobChanged: {
if (kazvIOJob == null) {
return
}
if (kazvIOJob.isResulted()) {
jobSlots.onResult(kazvIOJob.error())
return
}
if (kazvIOJob.isSuspended()) {
pauseAction.suspended = true
}
}
Connections {
id: jobSlots
target: kazvIOJob
function onResult(ec) {
progressBarLayout.visible = false
resultMsgLayout.visible = true
let resultL10nId = isUpload ? 'kazv-io-upload-failure-prompt' : 'kazv-io-download-failure-prompt'
switch (ec) {
case MK.KazvIOBaseJob.NoError:
if (isUpload) {
return
}
resultMsg.text = l10n.get('kazv-io-download-success-prompt')
break
case MK.KazvIOBaseJob.UserCancel:
resultMsg.text = l10n.get(resultL10nId, {detail: l10n.get('kazv-io-failure-detail-user-cancel')})
break
case MK.KazvIOBaseJob.KIOError:
resultMsg.text = l10n.get(resultL10nId, {detail: l10n.get('kazv-io-failure-detail-network-error')})
break
case MK.KazvIOBaseJob.QtError:
resultMsg.text = l10n.get(resultL10nId, {detail: l10n.get('kazv-io-failure-detail-file-error')})
break
case MK.KazvIOBaseJob.KazvError:
resultMsg.text = l10n.get(resultL10nId, {detail: l10n.get('kazv-io-failure-detail-kazv-error')})
break
}
}
}
Label {
property string l10nId: isUpload ? 'kazv-io-uploading-prompt' : 'kazv-io-downloading-prompt'
text: kazvIOJob ? l10n.get(l10nId, {fileName: kazvIOJob.fileName()}) : ""
}
RowLayout {
id: progressBarLayout
visible: true
Layout.preferredWidth: parent.width
ProgressBar {
id: progressBar
Layout.fillWidth: true
value: kazvIOJob ? kazvIOJob.progress : 0
}
Kirigami.Action {
id: pauseAction
text: suspended ? l10n.get("kazv-io-resume") : l10n.get("kazv-io-pause")
iconName: suspended ? "media-playback-start" : "media-playback-pause"
property var suspended: false
onTriggered: {
if (pauseAction.suspended) {
kazvIOJob.resume()
} else {
kazvIOJob.suspend()
}
suspended = !suspended
}
}
Kirigami.Action {
id: cancelAction
text: l10n.get("kazv-io-cancel")
iconName: "dialog-cancel"
onTriggered: {
pauseAction.suspended = false
kazvIOJob.cancel()
}
}
RoundButton {
id: pauseBtn
Accessible.name: pauseAction.suspended ? l10n.get('kazv-io-resume') : l10n.get('kazv-io-pause')
icon.name: pauseAction.suspended ? "media-playback-start" : "media-playback-pause"
onClicked: pauseAction.trigger()
}
RoundButton {
icon.name: "dialog-cancel"
Accessible.name: l10n.get('kazv-io-cancel')
onClicked: cancelAction.trigger()
}
}
RowLayout {
id: resultMsgLayout
visible: false
Text {
id: resultMsg
}
Button {
text: l10n.get('kazv-io-prompt-close')
onClicked: {
if (isUpload) {
kazvIOManager.deleteUploadJob(jobId, kazvIOJob)
} else {
kazvIOManager.deleteDownloadJob(jobId)
resultMsgLayout.visible = false
progressBarLayout.visible = true
resultMsg.text = null
kazvIOJob = null
}
}
}
}
}
diff --git a/src/contents/ui/MediaFileMenu.qml b/src/contents/ui/MediaFileMenu.qml
index 2f2f35b..3323f54 100644
--- a/src/contents/ui/MediaFileMenu.qml
+++ b/src/contents/ui/MediaFileMenu.qml
@@ -1,62 +1,62 @@
/*
* This file is part of kazv.
- * SPDX-FileCopyrightText: 2022 nannanko <nannanko@kazv.moe>
+ * SPDX-FileCopyrightText: 2022-2023 nannanko <nannanko@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Controls 2.15
import Qt.labs.platform 1.1 as Platform
import org.kde.kirigami 2.13 as Kirigami
import '.' as Types
TapHandler {
id: mediaFileMenu
required property var serverUrl
required property var jobId
required property var fileName
signal startDownload
acceptedButtons: Qt.LeftButton | Qt.RightButton
onSingleTapped: {
if (eventPoint.event.button === Qt.RightButton) {
optionMenu.popup(parent)
}
if (eventPoint.event.button === Qt.LeftButton) {
// TODO: Open with default program
}
}
onLongPressed: {
optionMenu.popup(parent)
}
property var optionMenu: Menu {
Kirigami.Action {
text: l10n.get('media-file-menu-option-view')
// shortcut: StandardKey.Open
// TODO: open with default program
}
Kirigami.Action {
text: l10n.get('media-file-menu-option-save-as')
// Can't think of a suitable shortcut key
// shortcut: StandardKey.Save
onTriggered: {
fileDialog.open()
}
}
}
property var fileDialog: Platform.FileDialog {
fileMode: Platform.FileDialog.SaveFile
folder: Platform.StandardPaths.writableLocation(Platform.StandardPaths.DownloadLocation)
// This is not work
// currentFile: fileName
onAccepted: {
root.kazvIOManager.startNewDownloadJob(mediaFileMenu.serverUrl, file, mediaFileMenu.jobId)
startDownload()
}
}
}
diff --git a/src/contents/ui/RoomPage.qml b/src/contents/ui/RoomPage.qml
index 07e7631..69de123 100644
--- a/src/contents/ui/RoomPage.qml
+++ b/src/contents/ui/RoomPage.qml
@@ -1,53 +1,53 @@
/*
* This file is part of kazv.
- * SPDX-FileCopyrightText: 2020 Tusooa Zhu <tusooa@kazv.moe>
+ * SPDX-FileCopyrightText: 2020-2023 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.2
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import org.kde.kirigami 2.13 as Kirigami
import moe.kazv.mxc.kazv 0.0 as MK
import '.' as Kazv
Kirigami.ScrollablePage {
id: roomPage
property string roomId: ''
property var room: sdkVars.roomList.room(roomId)
title: room.name || roomId
RoomTimelineView {
timeline: room.timeline()
}
onRoomChanged: {
root.kazvIOManager.deleteModelIfEmpty(roomId)
uploadList.model = root.kazvIOManager.getUploadJobs(roomId)
}
footer: Item {
height: childrenRect.height
width: parent.width
ListView {
id: uploadList
width: parent.width
height: contentHeight
model: root.kazvIOManager.getUploadJobs(roomId)
delegate: Kazv.KazvIOMenu {
width: parent.width
jobId: roomId
isUpload: true
}
}
Kazv.SendMessageBox {
id: sendMessageBos
width: parent.width
room: roomPage.room
anchors.top: uploadList.bottom
}
}
}
diff --git a/src/contents/ui/SendMessageBox.qml b/src/contents/ui/SendMessageBox.qml
index 17c4e02..441573e 100644
--- a/src/contents/ui/SendMessageBox.qml
+++ b/src/contents/ui/SendMessageBox.qml
@@ -1,71 +1,71 @@
/*
* This file is part of kazv.
- * SPDX-FileCopyrightText: 2020-2021 Tusooa Zhu <tusooa@kazv.moe>
+ * SPDX-FileCopyrightText: 2020-2023 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import Qt.labs.platform 1.1 as Platform
import org.kde.kirigami 2.13 as Kirigami
import moe.kazv.mxc.kazv 0.0 as MK
import '.' as Kazv
RowLayout {
property var room
onRoomChanged: {
textArea.text = room.localDraft
}
TextArea {
id: textArea
property var shortcutList: ["Ctrl+Return", "Ctrl+Enter"]
placeholderText: l10n.get('send-message-box-input-placeholder')
Layout.fillWidth: true
wrapMode: TextEdit.Wrap
onTextChanged: {
room.localDraft = text
}
// Shortcut keys for sending messages (TODO: Shortcut keys customized by the user)
Shortcut {
sequences: textArea.shortcutList
onActivated: {
sendAction.trigger()
}
}
}
Kirigami.Action {
id: sendAction
iconName: "document-send"
text: l10n.get("send-message-box-send")
onTriggered: {
room.sendTextMessage(textArea.text)
textArea.text = ""
}
}
Kirigami.Action {
id:sendMediaFileAction
iconName: "document-send-symbolic"
text: l10n.get("send-message-box-send-file")
onTriggered: {
fileDialog.open()
}
}
property var fileDialog: Platform.FileDialog {
onAccepted: {
root.kazvIOManager.startNewUploadJob(matrixSdk.serverUrl, file, matrixSdk.token, room.roomId, sdkVars.roomList)
}
}
ToolButton {
icon.name: "document-send-symbolic"
onClicked: sendMediaFileAction.trigger()
}
ToolButton {
icon.name: "document-send"
onClicked: sendAction.trigger()
}
}
diff --git a/src/contents/ui/event-types/Audio.qml b/src/contents/ui/event-types/Audio.qml
index 1b53f11..48afe02 100644
--- a/src/contents/ui/event-types/Audio.qml
+++ b/src/contents/ui/event-types/Audio.qml
@@ -1,70 +1,70 @@
/*
* This file is part of kazv.
- * SPDX-FileCopyrightText: 2020-2021 Tusooa Zhu <tusooa@kazv.moe>
+ * SPDX-FileCopyrightText: 2020-2023 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import moe.kazv.mxc.kazv 0.0 as MK
import QtMultimedia 5.15
import org.kde.kirigami 2.13 as Kirigami
import '.' as Types
Simple {
id: upper
property var gender: 'neutral'
property var body: event.content.body
property var mxcUri: event.content.url
property var audioUri: matrixSdk.mxcUriToHttp(mxcUri)
property var innerContentWidth: upper.contentMaxWidth - bubble.bubbleSpacing
Types.MediaBubble {
id: bubble
eventId: event.eventId
serverUrl: upper.audioUri
fileName: body
ColumnLayout {
property var msgLabel: Label {
Layout.fillWidth: false
Layout.maximumWidth: innerContentWidth
wrapMode: Text.Wrap
text: l10n.get('event-message-audio-sent', { gender, body })
}
property var audio: Audio {
id: audio
source: audioUri
autoLoad: false
autoPlay: false
}
property var playLabel: Label {
Layout.fillWidth: true
Layout.maximumWidth: innerContentWidth
wrapMode: Text.Wrap
text: l10n.get('event-message-audio-play-audio')
TapHandler {
onSingleTapped: {
audio.play()
}
}
}
data: [
msgLabel,
audio,
playLabel,
bubble.mediaFileMenu,
bubble.progressBar
]
}
}
}
diff --git a/src/contents/ui/event-types/File.qml b/src/contents/ui/event-types/File.qml
index 39544c0..c4b53b2 100644
--- a/src/contents/ui/event-types/File.qml
+++ b/src/contents/ui/event-types/File.qml
@@ -1,55 +1,55 @@
/*
* This file is part of kazv.
- * SPDX-FileCopyrightText: 2020-2021 Tusooa Zhu <tusooa@kazv.moe>
+ * SPDX-FileCopyrightText: 2020-2023 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.2
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import moe.kazv.mxc.kazv 0.0 as MK
import org.kde.kirigami 2.13 as Kirigami
import '.' as Types
Simple {
id: upper
property var gender: 'neutral'
property var body: event.content.body
property var mxcUri: event.content.url
property var fileUri: matrixSdk.mxcUriToHttp(mxcUri)
property var innerContentWidth: upper.contentMaxWidth - bubble.bubbleSpacing
Types.MediaBubble {
id: bubble
eventId: event.eventId
serverUrl: upper.fileUri
fileName: body
ColumnLayout {
property var msgLabel: Label {
Layout.fillWidth: false
Layout.maximumWidth: innerContentWidth
wrapMode: Text.Wrap
text: l10n.get('event-message-file-sent', { gender, body })
}
property var uriLabel: Label {
Layout.fillWidth: false
Layout.maximumWidth: innerContentWidth
wrapMode: Text.Wrap
text: fileUri
}
data: [
msgLabel,
uriLabel,
bubble.mediaFileMenu,
bubble.progressBar
]
}
}
}
diff --git a/src/contents/ui/event-types/Image.qml b/src/contents/ui/event-types/Image.qml
index c0c6253..73a13de 100644
--- a/src/contents/ui/event-types/Image.qml
+++ b/src/contents/ui/event-types/Image.qml
@@ -1,63 +1,63 @@
/*
* This file is part of kazv.
- * SPDX-FileCopyrightText: 2020-2021 Tusooa Zhu <tusooa@kazv.moe>
+ * SPDX-FileCopyrightText: 2020-2023 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import moe.kazv.mxc.kazv 0.0 as MK
import org.kde.kirigami 2.13 as Kirigami
import '.' as Types
import '..' as Kazv
Simple {
id: upper
property var gender: 'neutral'
property var body: event.content.body
property var mxcUri: event.content.url
property var imageUri: matrixSdk.mxcUriToHttp(mxcUri)
property var imageInfo: event.content.info || {}
property var imageWidth: image.implicitWidth || imageInfo.w || 1 // lest we divide by 0
property var imageHeight: image.implicitHeight || imageInfo.h || 1
property var innerContentWidth: upper.contentMaxWidth - bubble.bubbleSpacing
Types.MediaBubble {
id: bubble
eventId: event.eventId
serverUrl: upper.imageUri
fileName: body
property var label: Label {
Layout.fillWidth: false
Layout.maximumWidth: innerContentWidth
wrapMode: Text.Wrap
text: l10n.get('event-message-image-sent', { gender, body })
}
property var image: Image {
id: image
source: imageUri
Layout.maximumWidth: innerContentWidth
Layout.preferredWidth: imageWidth
Layout.preferredHeight: imageHeight / imageWidth * width
fillMode: Image.PreserveAspectFit
data: [bubble.mediaFileMenu]
}
ColumnLayout {
id: layout
data: [
bubble.label,
bubble.image,
bubble.progressBar
]
}
}
}
diff --git a/src/contents/ui/event-types/MediaBubble.qml b/src/contents/ui/event-types/MediaBubble.qml
index 1c30732..c2daf4f 100644
--- a/src/contents/ui/event-types/MediaBubble.qml
+++ b/src/contents/ui/event-types/MediaBubble.qml
@@ -1,33 +1,33 @@
/*
* This file is part of kazv.
- * SPDX-FileCopyrightText: 2022 nannanko <nannanko@kazv.moe>
+ * SPDX-FileCopyrightText: 2022-2023 nannanko <nannanko@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import moe.kazv.mxc.kazv 0.0 as MK
import '..' as Kazv
Kazv.Bubble {
id: bubble
required property var eventId
required property var serverUrl
required property var fileName
property var mediaFileMenu: Kazv.MediaFileMenu {
serverUrl: bubble.serverUrl
jobId: bubble.eventId
fileName: bubble.fileName
onStartDownload: {
progressBar.kazvIOJob = kazvIOManager.getDownloadJob(eventId)
}
}
property var progressBar: Kazv.KazvIOMenu {
id: progressBar
jobId: bubble.eventId
isUpload: false
kazvIOJob: root.kazvIOManager.getDownloadJob(jobId)
}
}
diff --git a/src/contents/ui/event-types/Video.qml b/src/contents/ui/event-types/Video.qml
index 7041285..b908a57 100644
--- a/src/contents/ui/event-types/Video.qml
+++ b/src/contents/ui/event-types/Video.qml
@@ -1,117 +1,117 @@
/*
* This file is part of kazv.
- * SPDX-FileCopyrightText: 2020-2021 Tusooa Zhu <tusooa@kazv.moe>
+ * SPDX-FileCopyrightText: 2020-2023 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import moe.kazv.mxc.kazv 0.0 as MK
import QtMultimedia 5.15
import org.kde.kirigami 2.13 as Kirigami
import '.' as Types
Simple {
id: upper
property var gender: 'neutral'
property var body: event.content.body
property var mxcUri: event.content.url
property var videoUri: matrixSdk.mxcUriToHttp(mxcUri)
property var videoInfo: event.content.info || {}
property var videoResolution: video.metaData.resolution
property var videoWidth: videoResolution && videoResolution.width || (videoInfo.w || 1)
property var videoHeight: videoResolution && videoResolution.height || (videoInfo.h || 1)
property var thumbnailInfo: videoInfo.thumbnail_info || {}
property var thumbnailMxcUri: videoInfo.thumbnail_url
property var thumbnailUri: matrixSdk.mxcUriToHttp(thumbnailMxcUri)
property var hasThumbnail: !! thumbnailMxcUri
property var innerContentWidth: upper.contentMaxWidth - bubble.bubbleSpacing
Types.MediaBubble {
id: bubble
eventId: event.eventId
serverUrl: upper.videoUri
fileName: body
ColumnLayout {
id: layout
property var label: Label {
Layout.fillWidth: false
Layout.maximumWidth: innerContentWidth
wrapMode: Text.Wrap
text: l10n.get('event-message-video-sent', { gender, body })
}
property var video: Video {
id: video
source: videoUri
autoLoad: false
autoPlay: false
loops: MediaPlayer.Infinite
Layout.minimumHeight: Kirigami.Units.gridUnit * 10
Layout.minimumWidth: Kirigami.Units.gridUnit * 10
Layout.maximumWidth: innerContentWidth
Layout.preferredWidth: videoWidth
Layout.preferredHeight: videoHeight / videoWidth * width
fillMode: VideoOutput.PreserveAspectFit
TapHandler {
onSingleTapped: {
video.playOrPause();
}
}
Image {
anchors.fill: parent
source: thumbnailUri
visible: video.playbackState == MediaPlayer.StoppedState && hasThumbnail
fillMode: Image.PreserveAspectFit
data: [bubble.mediaFileMenu]
}
Rectangle {
anchors.fill: parent
color: Kirigami.Theme.negativeBackgroundColor //Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15)
visible: {
video.playbackState == MediaPlayer.StoppedState &&
! hasThumbnail
}
}
onStatusChanged: {
console.log('status = ', video.status);
}
onPlaybackStateChanged: {
console.log('playback state = ', video.playbackState);
}
function playOrPause() {
console.log('playback state ==', video.playbackState);
if (video.playbackState == MediaPlayer.PlayingState) {
video.pause();
} else {
if (video.playbackState == MediaPlayer.StoppedState) {
video.seek(0);
}
video.play();
}
}
}
data: [
label,
video,
bubble.progressBar
]
}
}
}
diff --git a/src/kazv-io-job.cpp b/src/kazv-io-job.cpp
index 974a550..06f9c51 100644
--- a/src/kazv-io-job.cpp
+++ b/src/kazv-io-job.cpp
@@ -1,311 +1,316 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2022-2023 nannanko <nannanko@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
#include "kazv-io-job.hpp"
#include "matrix-room.hpp"
#include <QObject>
#include <QPointer>
#include <QSharedPointer>
#include <QSaveFile>
#include <QFile>
#include <QByteArray>
#include <QIODevice>
#include <QString>
#include <KJob>
#include <KIO/TransferJob>
#include <nlohmann/json.hpp>
#include <optional>
#include <string>
using json = nlohmann::json;
struct KazvIOBaseJobPrivate
{
QPointer<KIO::TransferJob> job;
std::optional<KazvIOBaseJob::ErrorCode> result = std::nullopt;
};
KazvIOBaseJob::KazvIOBaseJob(QObject *parent)
: QObject(parent)
, m_d(new KazvIOBaseJobPrivate)
{
connect(this, &KazvIOBaseJob::jobChanged, this, &KazvIOBaseJob::connectJob);
}
KazvIOBaseJob::~KazvIOBaseJob() = default;
QPointer<KIO::TransferJob> KazvIOBaseJob::job()
{
return m_d->job;
}
void KazvIOBaseJob::setJob(QPointer<KIO::TransferJob> job)
{
resetResult();
if (!m_d->job.isNull()) {
m_d->job->disconnect(this);
}
m_d->job = job;
Q_EMIT jobChanged();
}
float KazvIOBaseJob::progress() {
if (m_d->job.isNull()) {
return 0;
}
return static_cast<float>(m_d->job->percent()) / 100;
}
void KazvIOBaseJob::suspend()
{
if (m_d->job.isNull()) {
emitResult(KazvError);
return;
}
m_d->job->suspend();
}
void KazvIOBaseJob::resume()
{
if (m_d->job.isNull()) {
emitResult(KazvError);
return;
}
m_d->job->resume();
}
void KazvIOBaseJob::cancel()
{
if (m_d->job.isNull()) {
emitResult(KazvError);
return;
}
emitResult(UserCancel);
m_d->job->kill();
}
bool KazvIOBaseJob::isSuspended()
{
if (m_d->job.isNull()) {
return false;
}
return m_d->job->isSuspended();
}
bool KazvIOBaseJob::isResulted()
{
return m_d->result.has_value();
}
KazvIOBaseJob::ErrorCode KazvIOBaseJob::error()
{
if (m_d->result.has_value()) {
return m_d->result.value();
}
// Shouldn't call this function before result are emited
return NoError;
}
void KazvIOBaseJob::connectJob()
{
connect(m_d->job, &KJob::result, this, [this](KJob *job) {
if (m_d->result.has_value()) {
return;
}
if (job->error()) {
emitResult(KIOError);
}
});
connect(m_d->job, &KJob::percentChanged, this,
[this](KJob * /* job */, unsigned long /* percent */) { Q_EMIT progressChanged(); });
}
void KazvIOBaseJob::emitResult(ErrorCode ec)
{
m_d->result = ec;
Q_EMIT result(m_d->result.value());
}
void KazvIOBaseJob::resetResult()
{
m_d->result = std::nullopt;
}
struct KazvIODownloadJobPrivate
{
QSharedPointer<QSaveFile> file;
};
KazvIODownloadJob::KazvIODownloadJob(QObject *parent)
: KazvIOBaseJob(parent)
, m_d(new KazvIODownloadJobPrivate)
{
}
KazvIODownloadJob::~KazvIODownloadJob() = default;
QPointer<QSaveFile> KazvIODownloadJob::file()
{
return m_d->file.data();
}
void KazvIODownloadJob::setFile(QPointer<QSaveFile> file)
{
resetResult();
if (!file->isOpen()) {
auto succ = file->open(QIODevice::WriteOnly);
if (!succ) {
KazvIOBaseJob::emitResult(QtError);
return;
}
}
m_d->file.reset(file.data());
Q_EMIT fileChanged();
}
QString KazvIODownloadJob::fileName() {
if (m_d->file.isNull()) {
return QStringLiteral("");
}
return m_d->file->fileName();
}
void KazvIODownloadJob::connectJob()
{
KazvIOBaseJob::connectJob();
connect(this->job(), &KIO::TransferJob::data, this, [this](KJob *job, const QByteArray &data) {
if (!m_d->file.isNull()) {
auto len = m_d->file->write(data);
if (len == -1) {
if (!this->isResulted()){
KazvIOBaseJob::emitResult(QtError);
}
job->kill();
}
return;
}
if (!this->isResulted()) {
KazvIOBaseJob::emitResult(KazvError);
}
job->kill();
});
connect(this->job(), &KJob::result, this, [this](KJob *job) {
if (m_d->file.isNull()) {
return;
}
if (job->error()) {
m_d->file->cancelWriting();
}
m_d->file->commit();
KazvIOBaseJob::emitResult(NoError);
});
}
struct KazvIOUploadJobPrivate
{
QSharedPointer<QFile> file;
QString response;
QSharedPointer<MatrixRoom> room;
QUrl localFileUrl; // For send media file message
};
KazvIOUploadJob::KazvIOUploadJob(QObject *parent)
: KazvIOBaseJob(parent)
, m_d(new KazvIOUploadJobPrivate)
{
connect(this, &KazvIOBaseJob::jobChanged, [this]() { m_d->response.clear(); });
}
KazvIOUploadJob::~KazvIOUploadJob() = default;
QPointer<QFile> KazvIOUploadJob::file()
{
return m_d->file.data();
}
void KazvIOUploadJob::setFile(QPointer<QFile> file)
{
resetResult();
m_d->response.clear();
if (!file->isOpen()) {
auto succ = file->open(QIODevice::ReadOnly);
if (!succ) {
KazvIOBaseJob::emitResult(QtError);
return;
}
}
m_d->file.reset(file.data());
Q_EMIT fileChanged();
}
QString KazvIOUploadJob::response()
{
return m_d->response;
}
QPointer<MatrixRoom> KazvIOUploadJob::room()
{
return m_d->room.data();
}
void KazvIOUploadJob::setRoom(QPointer<MatrixRoom> room)
{
m_d->room.reset(room);
Q_EMIT roomChanged();
}
QUrl KazvIOUploadJob::localFileUrl()
{
return m_d->localFileUrl;
}
void KazvIOUploadJob::setLocalFileUrl(QUrl localFileUrl)
{
m_d->localFileUrl = localFileUrl;
Q_EMIT localFileUrlChanged();
}
QString KazvIOUploadJob::fileName()
{
if (m_d->file.isNull()) {
return QStringLiteral("");
}
return m_d->file->fileName();
}
void KazvIOUploadJob::connectJob()
{
KazvIOBaseJob::connectJob();
connect(this->job(), &KIO::TransferJob::data, this, [this](KJob *job, const QByteArray &data) {
if (!m_d->file.isNull()) {
m_d->response.append(data);
Q_EMIT responseChanged();
return;
}
if (!this->isResulted()) {
KazvIOBaseJob::emitResult(KazvError);
}
job->kill();
});
connect(this->job(), &KJob::result, this, [this](KJob *job) {
if (m_d->file.isNull()) {
if (!this->isResulted()) {
KazvIOBaseJob::emitResult(KazvError);
}
return;
}
m_d->file->close();
if (job->error()) {
m_d->response.clear();
if (!this->isResulted()) {
KazvIOBaseJob::emitResult(KIOError);
}
return;
}
json j = json::parse(m_d->response.toStdString());
auto mxcUri = j["content_uri"].get<std::string>();
m_d->room->sendMediaFileMessage(m_d->localFileUrl, QString(mxcUri.data()));
KazvIOBaseJob::emitResult(NoError);
});
}
diff --git a/src/kazv-io-job.hpp b/src/kazv-io-job.hpp
index 00998b5..9ec316e 100644
--- a/src/kazv-io-job.hpp
+++ b/src/kazv-io-job.hpp
@@ -1,163 +1,163 @@
/*
* This file is part of kazv.
- * SPDX-FileCopyrightText: 2022 nannanko <nannanko@kazv.moe>
+ * SPDX-FileCopyrightText: 2022-2023 nannanko <nannanko@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include "matrix-room.hpp"
#include <QObject>
#include <QPointer>
#include <QSaveFile>
#include <QFile>
#include <QString>
#include <QByteArray>
#include <QUrl>
#include <KIO/TransferJob>
#include <memory>
#include <optional>
struct KazvIOBaseJobPrivate;
/**
* This is considered for an object to perform a job only once,
* but it is also designed to be able to use an object to perform multiple job (just when the previous job has ended.)
* to deal with possible future needs.
* Resetting file, job, etc. will reset the object to prepare for the next job.
* As a suggestion, make sure the file is opened normally before calling setJob to avoid strange problems.
*/
class KazvIOBaseJob : public QObject
{
Q_OBJECT
Q_PROPERTY(QPointer<KIO::TransferJob> job READ job WRITE setJob NOTIFY jobChanged)
Q_PROPERTY(float progress READ progress NOTIFY progressChanged)
std::unique_ptr<KazvIOBaseJobPrivate> m_d;
public:
enum ErrorCode {
/**
* No Error or the signal result has not been emitted yet.
*/
NoError = 0,
/**
* The user actively canceled the job.
*/
UserCancel,
/**
* Errors caused by various reasons such as file read and write failures, open failures, etc.
*/
QtError,
/**
* Errors caused by KIO, such network error.
*/
KIOError,
/**
* Errors caused by bad code, means the bug is somewhere.
*/
KazvError
};
Q_ENUM(ErrorCode)
KazvIOBaseJob(QObject *parent = 0);
~KazvIOBaseJob();
QPointer<KIO::TransferJob> job();
/**
* Before calling this function, make sure to use the setFile of the subclass to set a file that has been opened normally
*/
void setJob(QPointer<KIO::TransferJob> job);
float progress();
/**
* Only use it after emitted the result signal
*/
Q_INVOKABLE void suspend();
Q_INVOKABLE void resume();
Q_INVOKABLE void cancel();
Q_INVOKABLE bool isSuspended();
Q_INVOKABLE bool isResulted();
Q_INVOKABLE ErrorCode error();
Q_SIGNALS:
void jobChanged();
void progressChanged();
void result(ErrorCode ec);
protected Q_SLOTS:
virtual void connectJob();
protected:
/**
* If m_d->job needs to be killed before this function is called, then this function should be called first
* Unless you know what you are doing, it is recommended to use the isResulted function to check whether
* the object has emitted a result signal before calling this function
*/
void emitResult(ErrorCode ec);
void resetResult();
};
struct KazvIODownloadJobPrivate;
class KazvIODownloadJob : public KazvIOBaseJob
{
Q_OBJECT
Q_PROPERTY(QPointer<QSaveFile> file READ file WRITE setFile NOTIFY fileChanged)
std::unique_ptr<KazvIODownloadJobPrivate> m_d;
public:
KazvIODownloadJob(QObject *parent = 0);
~KazvIODownloadJob();
QPointer<QSaveFile> file();
void setFile(QPointer<QSaveFile> file);
Q_INVOKABLE QString fileName();
Q_SIGNALS:
void fileChanged();
protected Q_SLOTS:
void connectJob() override;
protected:
void emitResult(ErrorCode ec);
};
struct KazvIOUploadJobPrivate;
class KazvIOUploadJob : public KazvIOBaseJob
{
Q_OBJECT
Q_PROPERTY(QPointer<QFile> file READ file WRITE setFile NOTIFY fileChanged)
Q_PROPERTY(QString response READ response NOTIFY responseChanged)
Q_PROPERTY(QPointer<MatrixRoom> room READ room WRITE setRoom NOTIFY roomChanged)
Q_PROPERTY(QUrl localFileUrl READ localFileUrl WRITE setLocalFileUrl NOTIFY localFileUrlChanged)
std::unique_ptr<KazvIOUploadJobPrivate> m_d;
public:
KazvIOUploadJob(QObject *parent = 0);
~KazvIOUploadJob();
QPointer<QFile> file();
void setFile(QPointer<QFile> file);
QString response();
QPointer<MatrixRoom> room();
void setRoom(QPointer<MatrixRoom> room);
QUrl localFileUrl();
void setLocalFileUrl(QUrl localFileUrl);
Q_INVOKABLE QString fileName();
Q_SIGNALS:
void fileChanged();
void responseChanged();
void roomChanged();
void localFileUrlChanged();
protected Q_SLOTS:
void connectJob() override;
};
diff --git a/src/kazv-io-manager.cpp b/src/kazv-io-manager.cpp
index 68f1651..a96f4d2 100644
--- a/src/kazv-io-manager.cpp
+++ b/src/kazv-io-manager.cpp
@@ -1,110 +1,110 @@
/*
* This file is part of kazv.
- * SPDX-FileCopyrightText: 2022 nannanko <nannanko@kazv.moe>
+ * SPDX-FileCopyrightText: 2022-2023 nannanko <nannanko@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include "kazv-io-manager.hpp"
#include "kazv-io-job.hpp"
#include "matrix-room.hpp"
#include "upload-job-model.hpp"
#include <api/csapi/content-repo.hpp>
#include <file-desc.hpp>
#include <QObject>
#include <QIODevice>
#include <QFile>
#include <QSaveFile>
#include <QPointer>
#include <QSharedPointer>
#include <QUrl>
#include <QMap>
#include <QMultiMap>
#include <string>
struct KazvIOManagerPrivate
{
QMap<QString, QPointer<KazvIODownloadJob>> downloadJobs;
QMap<QString, QPointer<UploadJobModel>> uploadJobs;
};
KazvIOManager::KazvIOManager(QObject *parent)
: QObject(parent)
, m_d(new KazvIOManagerPrivate)
{
}
KazvIOManager::~KazvIOManager() = default;
KazvIOBaseJob *KazvIOManager::startNewDownloadJob(const QUrl &serverUrl, const QUrl &localFileUrl, const QString &jobId)
{
auto downloadJob = QPointer<KazvIODownloadJob>(new KazvIODownloadJob());
downloadJob->setFile(new QSaveFile(localFileUrl.path()));
downloadJob->setJob(KIO::get(serverUrl));
m_d->downloadJobs[jobId] = downloadJob;
return downloadJob.data();
}
KazvIOBaseJob *KazvIOManager::startNewUploadJob(const QUrl &serverUrl, const QUrl &localFileUrl, const QString &token, const QString &roomId, MatrixRoomList *roomList)
{
auto uploadJob = QPointer<KazvIOUploadJob>(new KazvIOUploadJob());
uploadJob->setFile(new QFile(localFileUrl.path()));
auto kazvUploadJob = Kazv::Api::UploadContentJob(serverUrl.toString().toStdString(), token.toStdString(), Kazv::FileDesc(std::string()));
uploadJob->setJob(KIO::http_post(QString(kazvUploadJob.url().data()), uploadJob->file().data()));
uploadJob->job()->addMetaData(QString("content-type") , QMimeDatabase().mimeTypeForFile(localFileUrl.path()).name());
uploadJob->job()->addMetaData("customHTTPHeader", QStringLiteral("Authorization: ").append(kazvUploadJob.requestHeader()->at("Authorization").data()));
uploadJob->job()->addMetaData("PropagateHttpHeader", "true");
uploadJob->setRoom(roomList->room(roomId));
uploadJob->setLocalFileUrl(localFileUrl);
connect(uploadJob.data(), &KazvIOBaseJob::result, this, [this, roomId, uploadJob](KazvIOBaseJob::ErrorCode ec) {
if (!ec) {
m_d->uploadJobs[roomId]->removeJob(uploadJob);
}
});
if (!m_d->uploadJobs.contains(roomId) || m_d->uploadJobs[roomId].isNull()) {
m_d->uploadJobs[roomId] = new UploadJobModel(this);
}
m_d->uploadJobs[roomId]->addJob(uploadJob);
return uploadJob.data();
}
KazvIOBaseJob *KazvIOManager::getDownloadJob(const QString &jobId)
{
auto targetJob = m_d->downloadJobs.find(jobId);
if (targetJob == m_d->downloadJobs.end()) {
return nullptr;
}
return targetJob.value().data();
}
void KazvIOManager::deleteDownloadJob(const QString &jobId)
{
m_d->downloadJobs[jobId]->deleteLater();
m_d->downloadJobs.remove(jobId);
}
UploadJobModel *KazvIOManager::getUploadJobs(const QString &roomId)
{
if (!m_d->uploadJobs.contains(roomId) || m_d->uploadJobs[roomId].isNull()) {
m_d->uploadJobs[roomId] = new UploadJobModel(this);
}
return m_d->uploadJobs[roomId].data();
}
void KazvIOManager::deleteUploadJob(const QString &roomId, KazvIOBaseJob *job)
{
m_d->uploadJobs[roomId]->removeJob(QPointer<KazvIOUploadJob>(qobject_cast<KazvIOUploadJob*>(job)));
}
void KazvIOManager::deleteModelIfEmpty(const QString &roomId)
{
if (!m_d->uploadJobs.contains(roomId)) {
return;
} else if (m_d->uploadJobs[roomId]->rowCount() == 0) {
auto job = m_d->uploadJobs[roomId];
m_d->uploadJobs.remove(roomId);
job->deleteLater();
}
}
diff --git a/src/kazv-io-manager.hpp b/src/kazv-io-manager.hpp
index 51a6c78..95ddf6a 100644
--- a/src/kazv-io-manager.hpp
+++ b/src/kazv-io-manager.hpp
@@ -1,42 +1,42 @@
/*
* This file is part of kazv.
- * SPDX-FileCopyrightText: 2022 nannanko <nannanko@kazv.moe>
+ * SPDX-FileCopyrightText: 2022-2023 nannanko <nannanko@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include "kazv-io-job.hpp"
#include "matrix-room.hpp"
#include "upload-job-model.hpp"
#include "matrix-room-list.hpp"
#include <QObject>
#include <QtQml>
#include <QUrl>
#include <QString>
#include <KIO/TransferJob>
#include <QPointer>
#include <memory>
struct KazvIOManagerPrivate;
class KazvIOManager : public QObject
{
Q_OBJECT
QML_ELEMENT
std::unique_ptr<KazvIOManagerPrivate> m_d;
public:
explicit KazvIOManager(QObject *parent = 0);
~KazvIOManager() override;
Q_INVOKABLE KazvIOBaseJob *startNewDownloadJob(const QUrl &serverUrl, const QUrl &localFileName, const QString &jobId);
Q_INVOKABLE KazvIOBaseJob *startNewUploadJob(const QUrl &serverUrl, const QUrl &localFileName, const QString &token, const QString &roomId, MatrixRoomList *roomList);
Q_INVOKABLE KazvIOBaseJob *getDownloadJob(const QString &jobId);
Q_INVOKABLE void deleteDownloadJob(const QString &jobId);
Q_INVOKABLE UploadJobModel *getUploadJobs(const QString &roomId);
Q_INVOKABLE void deleteUploadJob(const QString &roomId, KazvIOBaseJob *job);
Q_INVOKABLE void deleteModelIfEmpty(const QString &roomId);
};
diff --git a/src/main.cpp b/src/main.cpp
index 0c4f2b9..f96411f 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,60 +1,60 @@
/*
* This file is part of kazv.
- * SPDX-FileCopyrightText: 2021 Tusooa Zhu <tusooa@kazv.moe>
+ * SPDX-FileCopyrightText: 2021-2023 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <immer/config.hpp> // https://github.com/arximboldi/immer/issues/168
#include <QApplication>
#include <QQmlApplicationEngine>
#include <QtQml>
#include <QUrl>
#include "matrix-sdk.hpp"
#include "matrix-room-list.hpp"
#include "matrix-room.hpp"
#include "matrix-room-timeline.hpp"
#include "matrix-room-member.hpp"
#include "matrix-event.hpp"
#include "l10n-provider.hpp"
#include "kazv-config.hpp"
#include "kazv-io-manager.hpp"
#include "shortcuts/shortcut-util.hpp"
#include "kazv-io-job.hpp"
Q_DECL_EXPORT int main(int argc, char *argv[])
{
QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QApplication app(argc, argv);
QCoreApplication::setOrganizationName("project-kazv");
QCoreApplication::setOrganizationDomain("mxc.kazv.moe");
QCoreApplication::setApplicationName("kazv");
QQmlApplicationEngine engine;
qmlRegisterType<MatrixSdk>("moe.kazv.mxc.kazv", 0, 0, "MatrixSdk");
qmlRegisterType<L10nProvider>("moe.kazv.mxc.kazv", 0, 0, "L10nProvider");
qmlRegisterType<KazvConfig>("moe.kazv.mxc.kazv", 0, 0, "KazvConfig");
qmlRegisterType<KazvIOBaseJob>("moe.kazv.mxc.kazv", 0, 0, "KazvIOBaseJob");
qmlRegisterType<KazvIOManager>("moe.kazv.mxc.kazv", 0, 0, "KazvIOManager");
qmlRegisterType<UploadJobModel>("moe.kazv.mxc.kazv", 0, 0, "UploadJobModel");
qmlRegisterUncreatableType<MatrixRoomList>("moe.kazv.mxc.kazv", 0, 0, "MatrixRoomList", "");
qmlRegisterUncreatableType<MatrixRoom>("moe.kazv.mxc.kazv", 0, 0, "MatrixRoom", "");
qmlRegisterUncreatableType<MatrixRoomMember>("moe.kazv.mxc.kazv", 0, 0, "MatrixRoomMember", "");
qmlRegisterUncreatableType<MatrixRoomTimeline>("moe.kazv.mxc.kazv", 0, 0, "MatrixRoomTimeline", "");
qmlRegisterUncreatableType<MatrixEvent>("moe.kazv.mxc.kazv", 0, 0, "MatrixEvent", "");
qmlRegisterSingletonInstance<ShortcutUtil>("moe.kazv.mxc.kazvshortcuts", 0, 0, "ShortcutUtil", new ShortcutUtil());
engine.load(QUrl(QStringLiteral("qrc:///main.qml")));
if (engine.rootObjects().isEmpty()) {
return -1;
}
return app.exec();
}
diff --git a/src/matrix-room.cpp b/src/matrix-room.cpp
index 0c6aa04..ef04cee 100644
--- a/src/matrix-room.cpp
+++ b/src/matrix-room.cpp
@@ -1,83 +1,83 @@
/*
* This file is part of kazv.
- * SPDX-FileCopyrightText: 2020 Tusooa Zhu <tusooa@kazv.moe>
+ * SPDX-FileCopyrightText: 2020-2023 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <immer/config.hpp> // https://github.com/arximboldi/immer/issues/168
#include <lager/setter.hpp>
#include <nlohmann/json.hpp>
#include <event.hpp>
#include <QFile>
#include <QMimeDatabase>
#include "matrix-room.hpp"
#include "matrix-room-timeline.hpp"
#include "matrix-room-member.hpp"
#include "helper.hpp"
MatrixRoom::MatrixRoom(Kazv::Room room, QObject *parent)
: QObject(parent)
, m_room(room)
, m_memberNames(m_room.members())
, LAGER_QT(roomId)(m_room.roomId().xform(strToQt))
, LAGER_QT(name)(m_room.nameOpt()[lager::lenses::or_default].xform(strToQt))
, LAGER_QT(heroNames)(m_room.heroDisplayNames().xform(strListToQt))
, LAGER_QT(avatarMxcUri)(m_room.avatarMxcUri().xform(strToQt))
, LAGER_QT(localDraft)(lager::with_setter(
m_room.localDraft().xform(strToQt).make(),
[this](QString localDraft) {
m_room.setLocalDraft(localDraft.toStdString());
},
lager::automatic_tag{}))
, LAGER_QT(memberNames)(m_memberNames.xform(strListToQt))
{
}
MatrixRoom::~MatrixRoom() = default;
MatrixRoomTimeline *MatrixRoom::timeline() const
{
return new MatrixRoomTimeline(m_room);
}
void MatrixRoom::sendTextMessage(QString text) const
{
m_room.sendTextMessage(text.toStdString());
}
void MatrixRoom::sendMediaFileMessage(QUrl localFileUrl, QString mxcUri) const
{
auto mimeTypeStr = QMimeDatabase().mimeTypeForFile(localFileUrl.toLocalFile()).name().toStdString();
nlohmann::json j{
{"type", "m.room.message"},
{"content", {
// TODO: According to the user's choice
{"msgtype", std::string("m.") + mimeTypeStr.substr(0, mimeTypeStr.find('/'))},
{"body", localFileUrl.fileName().toStdString()},
{"url", mxcUri.toStdString()},
{"info", {
{"size", QFile(localFileUrl.toLocalFile()).size()},
{"mimetype", mimeTypeStr}
}}
}}
};
Kazv::Event e{j};
m_room.sendMessage(e);
}
MatrixRoomMember *MatrixRoom::memberAt(int index) const
{
return new MatrixRoomMember(m_room.memberEventByCursor(
m_memberNames[index][lager::lenses::or_default]));
}
MatrixRoomMember *MatrixRoom::member(QString userId) const
{
return new MatrixRoomMember(m_room.memberEventFor(userId.toStdString()));
}
diff --git a/src/matrix-room.hpp b/src/matrix-room.hpp
index babf6a4..174e8de 100644
--- a/src/matrix-room.hpp
+++ b/src/matrix-room.hpp
@@ -1,53 +1,53 @@
/*
* This file is part of kazv.
- * SPDX-FileCopyrightText: 2020 Tusooa Zhu <tusooa@kazv.moe>
+ * SPDX-FileCopyrightText: 2020-2023 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include <immer/config.hpp> // https://github.com/arximboldi/immer/issues/168
#include <QObject>
#include <QQmlEngine>
#include <QUrl>
#include <lager/extra/qt.hpp>
#include <client/room/room.hpp>
class MatrixRoomTimeline;
class MatrixRoomMember;
class MatrixRoom : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
Kazv::Room m_room;
lager::reader<immer::flex_vector<std::string>> m_memberNames;
public:
explicit MatrixRoom(Kazv::Room room, QObject *parent = 0);
~MatrixRoom() override;
LAGER_QT_READER(QString, roomId);
LAGER_QT_READER(QString, name);
LAGER_QT_READER(QStringList, heroNames);
LAGER_QT_READER(QString, avatarMxcUri);
LAGER_QT_CURSOR(QString, localDraft);
LAGER_QT_READER(QStringList, memberNames);
Q_INVOKABLE MatrixRoomMember *memberAt(int index) const;
Q_INVOKABLE MatrixRoomMember *member(QString userId) const;
Q_INVOKABLE MatrixRoomTimeline *timeline() const;
Q_INVOKABLE void sendTextMessage(QString text) const;
Q_INVOKABLE void sendMediaFileMessage(QUrl localFileUrl, QString mxcUri) const;
};
diff --git a/src/matrix-sdk.cpp b/src/matrix-sdk.cpp
index f53ff2b..a3eef17 100644
--- a/src/matrix-sdk.cpp
+++ b/src/matrix-sdk.cpp
@@ -1,419 +1,419 @@
/*
* This file is part of kazv.
- * SPDX-FileCopyrightText: 2020-2021 Tusooa Zhu <tusooa@kazv.moe>
+ * SPDX-FileCopyrightText: 2020-2023 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <immer/config.hpp> // https://github.com/arximboldi/immer/issues/168
#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <fstream>
#include <filesystem>
#include <chrono>
#include <QMutex>
#include <QMutexLocker>
#include <QtConcurrent>
#include <KConfig>
#include <KConfigGroup>
#include <eventemitter/lagerstoreeventemitter.hpp>
#include <job/cprjobhandler.hpp>
#include <asio-promise-handler.hpp>
#include <client/sdk.hpp>
#include <base/descendent.hpp>
#include <zug/util.hpp>
#include <boost/asio.hpp>
#include <lager/event_loop/boost_asio.hpp>
#include <lager/event_loop/qt.hpp>
#include "matrix-sdk.hpp"
#include "matrix-room-list.hpp"
#include "helper.hpp"
#include "kazv-path-config.hpp"
#include "kazv-version.hpp"
#include "qt-rand-adapter.hpp"
using namespace Kazv;
// Sdk with Boost event loop, identity transform and no enhancers
using ExecT = decltype(std::declval<boost::asio::io_context>().get_executor());
using SdkT =
decltype(makeSdk(
SdkModel{},
detail::declref<JobInterface>(),
detail::declref<EventInterface>(),
AsioPromiseHandler{std::declval<boost::asio::io_context>().get_executor()},
zug::identity,
withRandomGenerator(detail::declref<RandomInterface>())));
struct MatrixSdkPrivate
{
MatrixSdkPrivate(MatrixSdk *q);
MatrixSdkPrivate(MatrixSdk *q, SdkModel model);
RandomInterface randomGenerator;
boost::asio::io_context io;
CprJobHandler jobHandler;
LagerStoreEventEmitter ee;
LagerStoreEventEmitter::Watchable watchable;
SdkT sdk;
QTimer saveTimer;
QMutex ioContextLock;
using SecondaryRootT = decltype(sdk.createSecondaryRoot(std::declval<lager::with_qt_event_loop>()));
SecondaryRootT secondaryRoot;
Client clientOnSecondaryRoot;
void runIoContext() {
std::thread([this] {
// This lock is to persist as long as io.run() does not return.
// Together with the measures in emplace(), it ensures that
// the io context is not destructed prematurely.
QMutexLocker l(&ioContextLock);
qDebug() << "running io context...";
io.run();
qDebug() << "io context is done";
}).detach();
}
};
static void serializeClientToFile(Client c)
{
using namespace Kazv::CursorOp;
auto userId = +c.userId();
auto deviceId = +c.deviceId();
if (userId.empty() || deviceId.empty()) {
qDebug() << "Not logged in, nothing to serialize";
return;
}
using StdPath = std::filesystem::path;
auto userDataDir = StdPath(kazvUserDataDir().toStdString());
auto sessionDir = userDataDir / "sessions"
/ userId / deviceId;
auto storeFile = sessionDir / "store";
auto metadataFile = sessionDir / "metadata";
qDebug() << "storeFile=" << QString::fromStdString(storeFile.native());
std::error_code err;
if ((! std::filesystem::create_directories(sessionDir, err))
&& err) {
qDebug() << "Unable to create sessionDir";
return;
}
{
auto storeStream = std::ofstream(storeFile);
if (! storeStream) {
qDebug() << "Unable to open storeFile";
return;
}
using OAr = boost::archive::text_oarchive;
auto archive = OAr{storeStream};
c.serializeTo(archive);
qDebug() << "Serialization done";
}
// store metadata
{
KConfig metadata(QString::fromStdString(metadataFile.native()));
KConfigGroup mdGroup(&metadata, "Metadata");
mdGroup.writeEntry("kazvVersion", QString::fromStdString(kazvVersionString()));
mdGroup.writeEntry("archiveFormat", "text");
}
}
MatrixSdkPrivate::MatrixSdkPrivate(MatrixSdk *q)
: randomGenerator(QtRandAdapter{})
, io{}
, jobHandler{io.get_executor()}
, ee{lager::with_boost_asio_event_loop{io.get_executor()}}
, watchable(ee.watchable())
, sdk(makeDefaultSdkWithCryptoRandom(
randomGenerator.generateRange<std::string>(makeDefaultSdkWithCryptoRandomSize()),
static_cast<JobInterface &>(jobHandler),
static_cast<EventInterface &>(ee),
AsioPromiseHandler{io.get_executor()},
zug::identity,
withRandomGenerator(randomGenerator)))
, secondaryRoot(sdk.createSecondaryRoot(lager::with_qt_event_loop{*q}))
, clientOnSecondaryRoot(sdk.clientFromSecondaryRoot(secondaryRoot))
{
}
MatrixSdkPrivate::MatrixSdkPrivate(MatrixSdk *q, SdkModel model)
: randomGenerator(QtRandAdapter{})
, io{}
, jobHandler{io.get_executor()}
, ee{lager::with_boost_asio_event_loop{io.get_executor()}}
, watchable(ee.watchable())
, sdk(makeSdk(
std::move(model),
static_cast<JobInterface &>(jobHandler),
static_cast<EventInterface &>(ee),
AsioPromiseHandler{io.get_executor()},
zug::identity,
withRandomGenerator(randomGenerator)))
, secondaryRoot(sdk.createSecondaryRoot(lager::with_qt_event_loop{*q}))
, clientOnSecondaryRoot(sdk.clientFromSecondaryRoot(secondaryRoot))
{
}
MatrixSdk::MatrixSdk(std::unique_ptr<MatrixSdkPrivate> d, QObject *parent)
: QObject(parent)
, m_d(std::move(d))
{
init();
connect(this, &MatrixSdk::trigger,
this, [](KazvEvent e) {
qDebug() << "receiving trigger:";
if (std::holds_alternative<LoginSuccessful>(e)) {
qDebug() << "Login successful";
}
});
}
void MatrixSdk::init()
{
LAGER_QT(serverUrl) = m_d->clientOnSecondaryRoot.serverUrl().xform(strToQt); Q_EMIT serverUrlChanged(serverUrl());
LAGER_QT(userId) = m_d->clientOnSecondaryRoot.userId().xform(strToQt); Q_EMIT userIdChanged(userId());
LAGER_QT(token) = m_d->clientOnSecondaryRoot.token().xform(strToQt); Q_EMIT tokenChanged(token());
LAGER_QT(deviceId) = m_d->clientOnSecondaryRoot.deviceId().xform(strToQt); Q_EMIT deviceIdChanged(deviceId());
m_d->watchable.afterAll(
[this](KazvEvent e) {
Q_EMIT this->trigger(e);
});
m_d->watchable.after<LoginSuccessful>(
[this](LoginSuccessful e) {
Q_EMIT this->loginSuccessful(e);
});
m_d->watchable.after<LoginFailed>(
[this](LoginFailed e) {
Q_EMIT this->loginFailed(
QString::fromStdString(e.errorCode),
QString::fromStdString(e.error)
);
});
connect(&m_d->saveTimer, &QTimer::timeout, &m_d->saveTimer,
[m_d=m_d.get()]() {
serializeClientToFile(m_d->clientOnSecondaryRoot);
});
const int saveIntervalMs = 1000 * 60 * 5;
m_d->saveTimer.start(std::chrono::milliseconds{saveIntervalMs});
}
MatrixSdk::MatrixSdk(QObject *parent)
: MatrixSdk(std::make_unique<MatrixSdkPrivate>(this), parent)
{
}
MatrixSdk::MatrixSdk(SdkModel model, QObject *parent)
: MatrixSdk(std::make_unique<MatrixSdkPrivate>(this, std::move(model)), parent)
{
}
static void cleanupDPointer(std::unique_ptr<MatrixSdkPrivate> oldD)
{
oldD->saveTimer.disconnect();
oldD->saveTimer.stop();
QtConcurrent::run(
[oldD=std::shared_ptr(std::move(oldD))] {
qDebug() << "start to clean up everything";
// Not running io_context::stop() because it will
// disregard any remaining work guards. Here we just
// want to end any periodic jobs but still wait for
// current remaining jobs.
oldD->clientOnSecondaryRoot.stopSyncing();
oldD->jobHandler.stop();
// This lock will not be unlocked as long as io.run()
// in runIoContext() does not return.
QMutexLocker l(&(oldD->ioContextLock));
// qDebug() << "serialize final client state";
// serializeClientToFile(oldD->sdk.client());
qDebug() << "from here, the old data can be destructed";
});
}
MatrixSdk::~MatrixSdk()
{
if (m_d) {
serializeToFile();
cleanupDPointer(std::move(m_d));
}
}
QString MatrixSdk::mxcUriToHttp(QString mxcUri) const
{
return QString::fromStdString(m_d->clientOnSecondaryRoot.mxcUriToHttp(mxcUri.toStdString()));
}
void MatrixSdk::login(const QString &userId, const QString &password)
{
m_d->clientOnSecondaryRoot
.autoDiscover(userId.toStdString())
.then([
this,
client=m_d->clientOnSecondaryRoot.toEventLoop(),
userId,
password
](auto res) {
if (!res.success()) {
// FIXME use real error codes and msgs when available in libkazv
Q_EMIT this->discoverFailed("", "");
return res;
}
auto serverUrl = res.dataStr("homeserverUrl");
client.passwordLogin(
serverUrl,
userId.toStdString(),
password.toStdString(),
"kazv 0.0.0"
);
return res;
});
m_d->runIoContext();
}
MatrixRoomList *MatrixSdk::roomList() const
{
return new MatrixRoomList(m_d->clientOnSecondaryRoot);
}
void MatrixSdk::emplace(std::optional<SdkModel> model)
{
if (m_d) {
cleanupDPointer(std::move(m_d));
}
m_d = (model.has_value()
? std::make_unique<MatrixSdkPrivate>(this, std::move(model.value()))
: std::make_unique<MatrixSdkPrivate>(this));
// Re-initialize lager-qt cursors and watchable connections
init();
m_d->runIoContext();
m_d->clientOnSecondaryRoot.startSyncing();
Q_EMIT sessionChanged();
}
QStringList MatrixSdk::allSessions() const
{
using StdPath = std::filesystem::path;
auto userDataDir = StdPath(kazvUserDataDir().toStdString());
auto allSessionsDir = userDataDir / "sessions";
QStringList sessionNames;
try {
for (const auto &p : std::filesystem::directory_iterator(allSessionsDir)) {
if (p.is_directory()) {
auto userId = p.path().filename().native();
for (const auto &q : std::filesystem::directory_iterator(p.path())) {
auto path = q.path();
auto deviceId = path.filename().native();
std::error_code err;
if (std::filesystem::exists(path / "store", err)) {
sessionNames.append(QString::fromStdString(userId + "/" + deviceId));
}
}
}
}
} catch (const std::filesystem::filesystem_error &) {
qDebug() << "sessionDir not available, ignoring";
}
return sessionNames;
}
void MatrixSdk::serializeToFile() const
{
serializeClientToFile(m_d->clientOnSecondaryRoot);
}
bool MatrixSdk::loadSession(QString sessionName)
{
qDebug() << "in loadSession(), sessionName=" << sessionName;
using StdPath = std::filesystem::path;
auto userDataDir = StdPath(kazvUserDataDir().toStdString());
auto sessionDir = userDataDir / "sessions" / sessionName.toStdString();
auto storeFile = sessionDir / "store";
auto metadataFile = sessionDir / "metadata";
if (! std::filesystem::exists(storeFile)) {
qDebug() << "storeFile does not exist, skip loading session " << sessionName;
return false;
}
if (std::filesystem::exists(metadataFile)) {
KConfig metadata(QString::fromStdString(metadataFile.native()));
KConfigGroup mdGroup(&metadata, "Metadata");
auto format = mdGroup.readEntry("archiveFormat");
if (format != QStringLiteral("text")) {
qDebug() << "Unknown archive format:" << format;
return false;
}
auto version = mdGroup.readEntry("kazvVersion");
auto curVersion = kazvVersionString();
if (version != QString::fromStdString(curVersion)) {
qDebug() << "A different version from the current one, making a backup";
std::error_code err;
auto now = std::chrono::system_clock::now();
auto backupName =
std::to_string(std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count());
auto backupDir = sessionDir / "backup" / backupName;
if (! std::filesystem::create_directories(backupDir, err)
&& err) {
qDebug() << "Cannot create backup directory";
return false;
}
std::filesystem::copy_file(storeFile, backupDir / "store");
std::filesystem::copy_file(metadataFile, backupDir / "metadata");
}
}
SdkModel model;
try {
auto storeStream = std::ifstream(storeFile);
if (! storeStream) {
qDebug() << "Unable to open storeFile";
return false;
}
using IAr = boost::archive::text_iarchive;
auto archive = IAr{storeStream};
archive >> model;
qDebug() << "Finished loading session";
} catch (const std::exception &e) {
qDebug() << "Error when loading session:" << QString::fromStdString(e.what());
return false;
}
emplace(std::move(model));
return true;
}
bool MatrixSdk::startNewSession()
{
emplace(std::nullopt);
return true;
}
diff --git a/src/matrix-sdk.hpp b/src/matrix-sdk.hpp
index b1d22b6..aceb447 100644
--- a/src/matrix-sdk.hpp
+++ b/src/matrix-sdk.hpp
@@ -1,102 +1,102 @@
/*
* This file is part of kazv.
- * SPDX-FileCopyrightText: 2020-2021 Tusooa Zhu <tusooa@kazv.moe>
+ * SPDX-FileCopyrightText: 2020-2023 Tusooa Zhu <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include <immer/config.hpp> // https://github.com/arximboldi/immer/issues/168
#include <QObject>
#include <QQmlEngine>
#include <QString>
#include <memory>
#include <lager/extra/qt.hpp>
#include <sdk-model.hpp>
#include "meta-types.hpp"
class MatrixRoomList;
struct MatrixSdkPrivate;
class MatrixSdk : public QObject
{
Q_OBJECT
QML_ELEMENT
std::unique_ptr<MatrixSdkPrivate> m_d;
/// @param d A dynamically allocated d-pointer, whose ownership
/// will be transferred to this MatrixSdk.
explicit MatrixSdk(std::unique_ptr<MatrixSdkPrivate> d, QObject *parent);
void init();
public:
explicit MatrixSdk(QObject *parent = 0);
explicit MatrixSdk(Kazv::SdkModel model, QObject *parent = 0);
~MatrixSdk() override;
LAGER_QT_READER(QString, serverUrl);
LAGER_QT_READER(QString, userId);
LAGER_QT_READER(QString, token);
LAGER_QT_READER(QString, deviceId);
Q_INVOKABLE MatrixRoomList *roomList() const;
Q_INVOKABLE QString mxcUriToHttp(QString mxcUri) const;
private:
// Replaces the store with another one
void emplace(std::optional<Kazv::SdkModel> model);
Q_SIGNALS:
void trigger(Kazv::KazvEvent e);
void loginSuccessful(Kazv::KazvEvent e);
void loginFailed(QString errorCode, QString errorMsg);
void discoverFailed(QString errorCode, QString errorMsg);
void sessionChanged();
public Q_SLOTS:
void login(const QString &userId, const QString &password);
/**
* Serialize data to <AppDataDir>/sessions/<userid>/<deviceid>/
*
* If not logged in, do nothing.
*/
void serializeToFile() const;
/**
* Load session at <AppDataDir>/sessions/<sessionName> .
*
* @param sessionName A string in the form of <userid>/<deviceid> .
*
* @return true if successful, false otherwise.
*/
bool loadSession(QString sessionName);
/**
* Start an empty session.
*
* The new session is not logged in, and need to call login().
*
* @return true if successful, false otherwise.
*/
bool startNewSession();
/**
* Get all saved sessions.
*
* @return A list of session names in the form of <userid>/<deviceid> .
*/
QStringList allSessions() const;
};
diff --git a/src/upload-job-model.cpp b/src/upload-job-model.cpp
index 1073e71..a9ffdf6 100644
--- a/src/upload-job-model.cpp
+++ b/src/upload-job-model.cpp
@@ -1,51 +1,56 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2023 nannanko <nannanko@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
#include "upload-job-model.hpp"
#include "kazv-io-job.hpp"
struct UploadJobModelPrivate {
QList<QPointer<KazvIOUploadJob>> uploadJobs;
};
UploadJobModel::UploadJobModel(QObject *parent)
: QAbstractListModel(parent)
, m_d(new UploadJobModelPrivate)
{
}
UploadJobModel::~UploadJobModel() = default;
QVariant UploadJobModel::data(const QModelIndex &index, int role) const
{
if (index.row() < 0 || index.row() > m_d->uploadJobs.size() || role != JobRole) {
return QVariant();
} else {
return QVariant::fromValue(m_d->uploadJobs[index.row()].data());
}
}
int UploadJobModel::rowCount(const QModelIndex & /* parent */) const
{
return m_d->uploadJobs.size();
}
QHash<int, QByteArray> UploadJobModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[JobRole] = "kazvIOJob";
return roles;
}
void UploadJobModel::addJob(QPointer<KazvIOUploadJob> job)
{
beginInsertRows(QModelIndex(), rowCount(), rowCount());
m_d->uploadJobs.push_back(job);
endInsertRows();
}
void UploadJobModel::removeJob(QPointer<KazvIOUploadJob> job)
{
const int index = m_d->uploadJobs.indexOf(job);
beginRemoveRows(QModelIndex(), index, index);
m_d->uploadJobs.removeOne(job);
job->deleteLater();
endRemoveRows();
}
diff --git a/src/upload-job-model.hpp b/src/upload-job-model.hpp
index 5abe2c7..5a1b260 100644
--- a/src/upload-job-model.hpp
+++ b/src/upload-job-model.hpp
@@ -1,36 +1,41 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2023 nannanko <nannanko@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
#pragma once
#include "kazv-io-job.hpp"
#include <QAbstractListModel>
#include <QtQml>
#include <QVariant>
#include <QModelIndex>
#include <QHash>
#include <QByteArray>
#include <memory>
struct UploadJobModelPrivate;
class UploadJobModel : public QAbstractListModel {
Q_OBJECT
QML_ELEMENT
std::unique_ptr<UploadJobModelPrivate> m_d;
public:
enum Roles {
JobRole = Qt::UserRole + 1,
};
UploadJobModel(QObject *parent = nullptr);
~UploadJobModel() override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QHash<int, QByteArray> roleNames() const override;
void addJob(QPointer<KazvIOUploadJob> job);
void removeJob(QPointer<KazvIOUploadJob> job);
};
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Sep 19, 6:31 AM (1 d, 11 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1769035
Default Alt Text
(64 KB)
Attached To
Mode
rK kazv
Attached
Detach File
Event Timeline
Log In to Comment