Page MenuHomePhorge

No OneTemporary

Size
64 KB
Referenced Files
None
Subscribers
None
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

Mime Type
text/x-diff
Expires
Sat, Sep 19, 6:31 AM (1 d, 12 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1769035
Default Alt Text
(64 KB)

Event Timeline