Page MenuHomePhorge

No OneTemporary

Size
35 KB
Referenced Files
None
Subscribers
None
diff --git a/src/contents/ui/FileHandler.qml b/src/contents/ui/FileHandler.qml
index 2073f0b..5b33266 100644
--- a/src/contents/ui/FileHandler.qml
+++ b/src/contents/ui/FileHandler.qml
@@ -1,149 +1,172 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2023 nannanko <nannanko@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import moe.kazv.mxc.kazv 0.0 as MK
import QtQml 2.15
import QtQuick 2.15
/**
* Used to handle files described in event, such as caching(if autoCache is true) and download.
* It is either used for caching or downloading. These two operations can be performed in same FileHandler.(Set autoCache to true and then call downloadFile)
* But one Filehandler can only track one download/cache job.
*/
QtObject {
id: fileHandler
/**
* Just set these four properties, any other properties will be automatically evaluated.
* The eventContent can be modified at any time.
* Modifying other properties can lead to unpredictable behavior.
*/
required property var eventContent
required property bool autoCache // thumbnail(if exists) will be downloaded if set to true
required property var kazvIOManager
required property var matrixSdk
readonly property var httpVer: matrixSdk.checkSpecVersion("v1.11") ? MK.MatrixSdk.AuthenticatedMediaV1 : MK.MatrixSdk.UnauthenticatedMediaV3
readonly property var token: (httpVer === MK.MatrixSdk.AuthenticatedMediaV1) ? matrixSdk.token : ""
property var kazvIOJob
readonly property url localFile: kazvIOJob ? '' : cachedFile
readonly property real percent: fileHandler.kazvIOJob ? fileHandler.kazvIOJob.progress : 0.0 // from 0 to 1
property var result
readonly property bool success: fileHandler.result == MK.KazvIOBaseJob.NoError
/**
* This attribute is only used after result is set (that is, after signal resultChanged is emitted),
* and only when success is true, this attribute has a correct and valid value.
*/
property url cachedFile
/**
* Emit when start download, not emit when start cache.
* Can be used to indicate progress bar visible
*/
signal startDownload
function getMediaId(mxcUri) {
const temp = mxcUri.toString().split('/')
return temp[2] + '_' + temp[3]
}
function downloadFile(saveFileUrl) {
+ fileHandler.result = undefined;
const file = getFile();
if (!file) {
return;
}
const mediaId = getMediaId(file.mxcUri);
kazvIOManager.startNewDownloadJob(mxcToHttp(file.mxcUri), saveFileUrl, mediaId,
file.sha256, token, file.key, file.iv);
- fileHandler.updateKazvIOJob(mediaId);
+ fileHandler.updateKazvIOJob(mediaId, false);
fileHandler.startDownload();
}
function cacheFile() {
+ fileHandler.result = undefined;
const file = getThumbnail() || getFile();
if (!file) {
console.error('No valid file to cache');
return;
}
const mediaId = getMediaId(file.mxcUri);
- fileHandler.cachedFile = kazvIOManager.cacheFile(mxcToHttp(file.mxcUri), mediaId,
- file.sha256, token, file.key, file.iv);
- fileHandler.updateKazvIOJob(mediaId);
+ const cachedFile = kazvIOManager.cacheFile(
+ mxcToHttp(file.mxcUri), mediaId,
+ file.sha256, token, file.key, file.iv);
+ fileHandler.cachedFile = cachedFile;
+ fileHandler.updateKazvIOJob(mediaId, true);
+ return cachedFile;
}
- function updateKazvIOJob(mediaId) {
+ function updateKazvIOJob(mediaId, isCache) {
fileHandler.kazvIOJob = null
- if (fileHandler.autoCache) {
+ if (isCache) {
fileHandler.kazvIOJob = fileHandler.kazvIOManager.getCacheJob(mediaId)
} else {
fileHandler.kazvIOJob = fileHandler.kazvIOManager.getDownloadJob(mediaId)
}
if (kazvIOJob) {
fileHandler.kazvIOJob.result.connect(function (ec) {
fileHandler.result = ec
})
}
}
function mxcToHttp(mxcUri) {
if (!mxcUri) {
return "";
}
if (httpVer === MK.MatrixSdk.UnauthenticatedMediaV3) {
return matrixSdk.mxcUriToHttp(mxcUri);
} else if (httpVer === MK.MatrixSdk.AuthenticatedMediaV1) {
return matrixSdk.mxcUriToHttpAuthenticatedV1(mxcUri);
} else {
return "";
}
}
function getThumbnail() {
return getFileOrThumbnail(eventContent?.info?.thumbnail_file, eventContent?.info?.thumbnail_url);
}
function getFile() {
return getFileOrThumbnail(eventContent?.file, eventContent?.url);
}
// https://spec.matrix.org/v1.16/client-server-api/#mfile
function getFileOrThumbnail(encryptedFile, unencryptedMxcUri) {
const encryptedMxcUri = encryptedFile?.url;
const encryptedSha256 = encryptedFile?.hashes?.sha256;
const encryptedKey = encryptedFile?.key?.k;
const encryptedIv = encryptedFile?.iv;
const isEncrypted = !!(encryptedMxcUri && encryptedSha256 && encryptedKey && encryptedIv);
const hasFile = !!(isEncrypted || unencryptedMxcUri);
if (!hasFile) {
return undefined;
}
return ({
mxcUri: isEncrypted ? encryptedMxcUri : unencryptedMxcUri,
sha256: isEncrypted ? encryptedSha256 : "",
key: isEncrypted ? encryptedKey : "",
iv: isEncrypted ? encryptedIv : ""
})
}
+ // Cache file and then open it
+ function openFile() {
+ const kazvIOManager = fileHandler.kazvIOManager;
+ const filePath = this.cacheFile();
+ if (kazvIOManager.fileExists(filePath)) {
+ Qt.openUrlExternally(filePath);
+ return;
+ }
+
+ this.kazvIOJob.result.connect((ec, data) => {
+ if (ec == MK.KazvIOBaseJob.NoError) {
+ Qt.openUrlExternally(filePath);
+ } else {
+ showPassiveNotification(kazvIOManager.jobResultL10nMsg(ec, false));
+ }
+ });
+ }
+
onEventContentChanged: {
if (fileHandler.autoCache) {
fileHandler.cacheFile();
} else {
// Check if the file is downloading
const mediaId = getMediaId(getFile()?.mxcUri || "");
- fileHandler.updateKazvIOJob(mediaId);
+ fileHandler.updateKazvIOJob(mediaId, autoCache);
fileHandler.onSuccessChanged.connect(function () {
fileHandler.kazvIOManager.deleteDownloadJob(mediaId);
});
}
}
}
diff --git a/src/contents/ui/KazvIOMenu.qml b/src/contents/ui/KazvIOMenu.qml
index f1c22d1..e4d52ab 100644
--- a/src/contents/ui/KazvIOMenu.qml
+++ b/src/contents/ui/KazvIOMenu.qml
@@ -1,157 +1,147 @@
/*
* This file is part of kazv.
* 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
required property var jobId
required property bool isUpload
property var progress: kazvIOJob ? kazvIOJob.progress : 0
visible: kazvIOJob ? true : false
onKazvIOJobChanged: {
if (kazvIOJob == null) {
progressBarLayout.visible = true
resultMsgLayout.visible = false
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
if (ec == MK.KazvIOBaseJob.NoError) {
if (isUpload) {
return
}
resultMsg.text = l10n.get('kazv-io-download-success-prompt')
return
}
- let resultL10nId = isUpload ? 'kazv-io-upload-failure-prompt' : 'kazv-io-download-failure-prompt'
- let detailL10nId = new Map([
- [MK.KazvIOBaseJob.UserCancel, 'kazv-io-failure-detail-user-cancel'],
- [MK.KazvIOBaseJob.OpenFileError, 'kazv-io-failure-detail-open-file-error'],
- [MK.KazvIOBaseJob.WriteFileError, 'kazv-io-failure-detail-write-file-error'],
- [MK.KazvIOBaseJob.KIOError, 'kazv-io-failure-detail-network-error'],
- [MK.KazvIOBaseJob.HashError, 'kazv-io-failure-detail-hash-error'],
- [MK.KazvIOBaseJob.ResponseError, 'kazv-io-failure-detail-response-error'],
- [MK.KazvIOBaseJob.KazvError, 'kazv-io-failure-detail-kazv-error']
- ])
- resultMsg.text = l10n.get(resultL10nId, {detail: l10n.get(detailL10nId.get(ec))})
+ resultMsg.text = kazvIOManager.jobResultL10nMsg(ec, isUpload);
}
}
Label {
objectName: 'fileNamePrompt'
property string l10nId: isUpload ? 'kazv-io-uploading-prompt' : 'kazv-io-downloading-prompt'
text: kazvIOJob ? l10n.get(l10nId, {fileName: kazvIOJob.fileName()}) : ""
wrapMode: Text.Wrap
Layout.preferredWidth: parent.width
}
RowLayout {
id: progressBarLayout
objectName: 'progressLayout'
visible: true
Layout.preferredWidth: parent.width
ProgressBar {
id: progressBar
objectName: 'progressBar'
Layout.fillWidth: true
value: progress
}
Label {
id: progressLabel
objectName: 'progressText'
text: l10n.get("kazv-io-progress", { progress: Math.floor(progress * 100) })
}
Kirigami.Action {
id: pauseAction
text: suspended ? l10n.get("kazv-io-resume") : l10n.get("kazv-io-pause")
icon.name: 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")
icon.name: "dialog-cancel"
onTriggered: {
pauseAction.suspended = false
kazvIOJob.cancel()
}
}
RoundButton {
id: pauseBtn
objectName: '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 {
objectName: 'cancelBtn'
icon.name: "dialog-cancel"
Accessible.name: l10n.get('kazv-io-cancel')
onClicked: cancelAction.trigger()
}
}
ColumnLayout {
id: resultMsgLayout
objectName: 'resultLayout'
visible: false
width: parent.width
Label {
id: resultMsg
objectName: 'resultMsg'
wrapMode: Text.Wrap
Layout.preferredWidth: parent.width
}
Button {
objectName: 'closeBtn'
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
}
}
}
}
}
diff --git a/src/contents/ui/Main.qml b/src/contents/ui/Main.qml
index 8ab1339..87e7191 100644
--- a/src/contents/ui/Main.qml
+++ b/src/contents/ui/Main.qml
@@ -1,377 +1,391 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import QtQuick
import org.kde.kirigami as Kirigami
import QtQuick.Layouts
import QtQuick.Controls
import moe.kazv.mxc.kazv as MK
import '.' as Kazv
import 'shortcuts' as KazvShortcuts
import 'l10n.js' as L10n
Kirigami.ApplicationWindow {
id: root
property var kazvIOManager: MK.KazvIOManager {
cacheDirectory: kazvConfig.cacheDirectory
+
+ function jobResultL10nMsg(ec: MK.KazvIOBaseJob.ErrorCode, isUpload: bool): string {
+ const resultL10nId = isUpload ? 'kazv-io-upload-failure-prompt' : 'kazv-io-download-failure-prompt'
+ const detailL10nId = new Map([
+ [MK.KazvIOBaseJob.UserCancel, 'kazv-io-failure-detail-user-cancel'],
+ [MK.KazvIOBaseJob.OpenFileError, 'kazv-io-failure-detail-open-file-error'],
+ [MK.KazvIOBaseJob.WriteFileError, 'kazv-io-failure-detail-write-file-error'],
+ [MK.KazvIOBaseJob.KIOError, 'kazv-io-failure-detail-network-error'],
+ [MK.KazvIOBaseJob.HashError, 'kazv-io-failure-detail-hash-error'],
+ [MK.KazvIOBaseJob.ResponseError, 'kazv-io-failure-detail-response-error'],
+ [MK.KazvIOBaseJob.KazvError, 'kazv-io-failure-detail-kazv-error']
+ ]);
+ return l10n.get(resultL10nId, {detail: l10n.get(detailL10nId.get(ec))});
+ }
}
property bool loadingSession: false
property var matrixSdk: MK.MatrixSdk {
onLoginSuccessful: {
console.log('Login successful');
switchToMainPage();
recordLastSession();
root.purgeTimer.start();
}
onLoginFailed: {
console.log("Login Failed");
showPassiveNotification(l10n.get('login-page-request-failed-prompt', { errorCode, errorMsg }));
}
onSessionChanged: {
console.log('session changed');
reloadSdkVariables();
root.purgeTimer.start();
}
onLogoutSuccessful: {
root.loggedIn = false;
pageStack.clear();
pushLoginPage();
}
onLogoutFailed: {
console.warn('Logout failed');
showPassiveNotification(l10n.get('logout-failed-prompt', { errorCode, errorMsg }));
}
onLoadSessionFinished: (sessionName, res) => {
root.loadingSession = false;
if (res === MK.MatrixSdk.SessionLoadSuccess) {
console.log('load session successful');
switchToMainPage();
recordLastSession();
matrixSdk.getSpecVersions();
} else {
showPassiveNotification(getSessionLoadError(res, sessionName));
}
}
}
property var loggedIn: !!matrixSdk.token
property var sdkVars: QtObject {
property var roomList: matrixSdk.roomList()
property var userGivenNicknameMap: matrixSdk.userGivenNicknameMap()
property string currentRoomId: ''
}
property var purgeTimer: Timer {
interval: 1000 * 60 * 5
repeat: true
triggeredOnStart: true
onTriggered: () => {
matrixSdk.purgeEventsExceptRooms(
sdkVars.currentRoomId ? [sdkVars.currentRoomId] : []);
}
}
property var l10nProvider: MK.L10nProvider {
}
property var kazvConfig: MK.KazvConfig {
}
function initializeL10n () {
const availableLocales = l10nProvider.availableLocaleCodes();
console.log('available locales:', availableLocales);
const desiredLanguages = [Qt.uiLanguage];
console.log('ui language is: ', desiredLanguages);
const defaultLocale = 'en';
const wantedLocales = L10n.negotiateLanguages(desiredLanguages, availableLocales, defaultLocale);
console.log('wanted locales:', wantedLocales);
const bundles = L10n.generateBundles(wantedLocales, l10nProvider.getFtlData(wantedLocales));
const provider = new L10n.FluentProvider(bundles);
return provider;
}
property var l10n: initializeL10n();
property var confirmLogoutPopup: Kirigami.OverlaySheet {
id: confirmLogoutPopup
title: l10n.get('confirm-logout-popup-title')
ColumnLayout {
Label {
Layout.fillWidth: true
text: l10n.get('confirm-logout-popup-prompt')
}
RowLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignCenter
Button {
text: l10n.get('confirm-logout-popup-accept-button')
onClicked: {
hardLogout();
confirmLogoutPopup.close();
}
}
Button {
text: l10n.get('confirm-logout-popup-cancel-button')
onClicked: confirmLogoutPopup.close();
}
}
}
}
title: l10n.get('app-title')
globalDrawer: Kirigami.GlobalDrawer {
title: l10n.get('global-drawer-title')
titleIcon: "applications-graphics"
property var actionGroupName: 'global-drawer-actions'
actions: [
Kirigami.Action {
objectName: 'switch-account'
text: l10n.get('global-drawer-action-switch-account')
icon.name: 'system-switch-user'
onTriggered: switchAccount()
},
Kirigami.Action {
objectName: 'hard-logout'
text: l10n.get('global-drawer-action-hard-logout')
icon.name: 'application-exit'
onTriggered: confirmLogoutPopup.open()
},
Kirigami.Action {
objectName: 'save'
text: l10n.get('global-drawer-action-save-session')
icon.name: 'document-save'
onTriggered: {
recordLastSession()
matrixSdk.serializeToFile()
}
},
Kirigami.Action {
objectName: 'configure-shortcuts'
text: l10n.get('global-drawer-action-configure-shortcuts')
icon.name: 'configure-shortcuts'
onTriggered: {
pushActionSettingsPage();
}
},
Kirigami.Action {
objectName: 'settings'
text: l10n.get('global-drawer-action-settings')
icon.name: 'configure'
onTriggered: {
pushSettingsPage();
}
},
Kirigami.Action {
objectName: 'create-room'
enabled: root.loggedIn
icon.name: 'contact-new'
text: l10n.get('global-drawer-action-create-room')
onTriggered: {
pushCreateRoomPage();
}
},
Kirigami.Action {
objectName: 'join-room'
enabled: root.loggedIn
text: l10n.get('global-drawer-action-join-room')
icon.name: 'list-add-user'
onTriggered: {
pushJoinRoomPage();
}
},
Kirigami.Action {
objectName: 'aboutAction'
text: l10n.get('global-drawer-action-about')
icon.name: 'help-about'
onTriggered: pageStack.pushDialogLayer(Qt.resolvedUrl('About.qml'))
}
]
}
contextDrawer: Kirigami.ContextDrawer {
id: contextDrawer
}
property var actionCollection: KazvShortcuts.ActionCollection {
id: globalActionCollection
children: [
root.globalDrawer,
root.contextDrawer,
]
shortcutsConfig: kazvConfig
}
property var notifier: Kazv.Notifier {
}
pageStack.initialPage: Qt.resolvedUrl("LoginPage.qml")
property var emptyPage: Kirigami.Page {
title: l10n.get('empty-room-page-title')
Label {
text: l10n.get('empty-room-page-description')
}
}
signal switchToRoomRequested(string roomId)
property var pageManager: Kazv.PageManager {
pageStack: root.pageStack
sdkVars: root.sdkVars
main: root
}
function activateRoomSettingsPage(room) {
pageStack.layers.push(Qt.resolvedUrl("room-settings/RoomSettingsPage.qml"), {
room
});
}
function activateRoomPinnedEventsPage(room) {
pageStack.push(Qt.resolvedUrl('RoomPinnedEventsPage.qml'), {
room
});
}
function activateRoomInvitePage(room) {
pageStack.layers.push(Qt.resolvedUrl("room-settings/RoomInvitePage.qml"), {
room
});
}
function activateRoomStickerPacksPage(room) {
pageStack.layers.push(Qt.resolvedUrl("room-settings/RoomStickerPacksPage.qml"), {
room
});
}
function activateRoomStatePage(room) {
pageStack.layers.push(Qt.resolvedUrl("room-settings/RoomStatePage.qml"), {
room
});
}
function activateUserPage(user, room, userId) {
pageStack.layers.push(Qt.resolvedUrl("UserPage.qml"), {
userId: userId || user.userId,
user,
room
});
}
function switchToMainPage() {
pageStack.clear();
pageStack.layers.clear();
pageStack.replace([Qt.resolvedUrl('MainPage.qml')]);
pageStack.currentIndex = 0;
}
function switchToLoginPage() {
pageStack.replace([Qt.resolvedUrl("LoginPage.qml")])
pageStack.currentIndex = 0
}
function pushLoginPage() {
pageStack.layers.push(Qt.resolvedUrl("LoginPage.qml"), {
isSwitchingAccount: true,
});
}
function pushActionSettingsPage() {
pageStack.layers.push(Qt.resolvedUrl("ActionSettingsPage.qml"), {
actionsToConfigure: globalActionCollection.allActions(),
});
}
function pushSettingsPage() {
pageStack.layers.push(Qt.resolvedUrl("SettingsPage.qml"));
}
function pushCreateRoomPage() {
pageStack.layers.push(Qt.resolvedUrl("CreateRoomPage.qml"), {});
}
function pushJoinRoomPage() {
pageStack.layers.push(Qt.resolvedUrl("JoinRoomPage.qml"), {});
}
function sessionNameFor(userId, deviceId) {
return userId + '/' + deviceId;
}
function getSessionLoadError(res, sessionName) {
switch (res) {
case MK.MatrixSdk.SessionNotFound:
return l10n.get('session-load-failure-not-found', { sessionName });
case MK.MatrixSdk.SessionFormatUnknown:
return l10n.get('session-load-failure-format-unknown', { sessionName });
case MK.MatrixSdk.SessionCannotBackup:
return l10n.get('session-load-failure-cannot-backup', { sessionName });
case MK.MatrixSdk.SessionLockFailed:
return l10n.get('session-load-failure-lock-failed', { sessionName });
case MK.MatrixSdk.SessionCannotOpenFile:
return l10n.get('session-load-failure-cannot-open-file', { sessionName });
case MK.MatrixSdk.SessionDeserializeFailed:
return l10n.get('session-load-failure-deserialize-failed', { sessionName });
default:
console.error('loadSession: Encountered an unknown error code');
}
}
function loadSession(sessionName) {
root.loadingSession = true;
matrixSdk.loadSession(sessionName);
}
function loadLastSession() {
console.log('last session is:', kazvConfig.lastSession);
return loadSession(kazvConfig.lastSession);
}
function recordLastSession() {
kazvConfig.lastSession = sessionNameFor(matrixSdk.userId, matrixSdk.deviceId);
}
function reloadSdkVariables() {
sdkVars.roomList = matrixSdk.roomList();
sdkVars.userGivenNicknameMap = matrixSdk.userGivenNicknameMap();
sdkVars.currentRoomId = '';
}
function switchAccount() {
pushLoginPage();
}
function hardLogout() {
matrixSdk.logout();
}
Component.onCompleted: {
actionCollection.setupShortcuts();
loadLastSession();
}
onClosing: {
kazvIOManager.clearJobs()
}
}
diff --git a/src/contents/ui/MediaFileMenu.qml b/src/contents/ui/MediaFileMenu.qml
index 247941f..50731bd 100644
--- a/src/contents/ui/MediaFileMenu.qml
+++ b/src/contents/ui/MediaFileMenu.qml
@@ -1,69 +1,68 @@
/*
* This file is part of kazv.
* 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 moe.kazv.mxc.kazv 0.0 as MK
import '.' as Kazv
QtObject {
id: mediaFileMenu
required property var eventContent
property var jobManager: kazvIOManager
property var mtxSdk: matrixSdk
signal startDownload
property var fileHandler: FileHandler {
id: fileHandler
autoCache: false
eventContent: mediaFileMenu.eventContent
kazvIOManager: mediaFileMenu.jobManager
matrixSdk: mediaFileMenu.mtxSdk
}
property var viewAction: Kirigami.Action {
text: l10n.get('media-file-menu-option-view')
onTriggered: {
- fileHandler.cacheFile()
- Qt.openUrlExternally(fileHandler.cachedFile)
+ fileHandler.openFile();
}
}
property var saveAction: Kirigami.Action {
text: l10n.get('media-file-menu-option-save-as')
onTriggered: {
fileDialog.open()
}
}
property var optionMenu: [viewAction, saveAction]
property var fileDialog: Kazv.FileDialogAdapter {
id: fileDialog
folder: Platform.StandardPaths.writableLocation(Platform.StandardPaths.DownloadLocation)
onAccepted: {
mediaFileMenu.fileHandler.downloadFile(fileUrl)
}
Component.onCompleted: {
if (MK.KazvUtil.kfQtMajorVersion === 5) {
fileDialog.selectExisting = false;
} else {
fileDialog.fileMode = Kazv.FileDialogAdapter.SaveFile;
}
}
}
Component.onCompleted: {
mediaFileMenu.fileHandler.startDownload.connect(mediaFileMenu.startDownload)
}
}
diff --git a/src/kazv-io-manager.cpp b/src/kazv-io-manager.cpp
index 96a2da1..f8cb6dd 100644
--- a/src/kazv-io-manager.cpp
+++ b/src/kazv-io-manager.cpp
@@ -1,231 +1,236 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2022-2023 nannanko <nannanko@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <kazv-defs.hpp>
#include "kazv-io-manager.hpp"
#include "kazv-io-job.hpp"
#include "matrix-room.hpp"
#include "upload-job-model.hpp"
#include "qt-rand-adapter.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 <QTemporaryDir>
#include <string>
#include <algorithm>
using namespace Qt::Literals::StringLiterals;
static const QString ROOMLESS = QStringLiteral("not-a-room");
struct KazvIOManagerPrivate
{
/**
* Used to store various temporary files.
* If the user does not specify a cache directory,
* this directory is also used as a cache directory.
*/
QTemporaryDir tmpDir = QTemporaryDir{};
/**
* Stores files that need to be previewed (e.g. in a timeline)
* and are not downloaded by the user, such as images, audio, video covers, and videos.
*/
QDir cacheDir{tmpDir.path()};
Kazv::RandomInterface randomGenerator = QtRandAdapter{};
QMap<QString, QPointer<KazvIODownloadJob>> cacheJobs;
QMap<QString, QSharedPointer<KazvIODownloadJob>> downloadJobs;
QMap<QString, QSharedPointer<UploadJobModel>> uploadJobs;
};
KazvIOManager::KazvIOManager(QObject *parent)
: QObject(parent)
, m_d(new KazvIOManagerPrivate)
{
}
KazvIOManager::~KazvIOManager() = default;
QPointer<KazvIODownloadJob> KazvIOManager::makeDownloadJob(
const QUrl &serverUrl, const QUrl &localFileUrl,
const bool showPrograssBar, const QString &hash, const QString &token,
const QString &key, const QString &iv) const
{
QPointer<KazvIODownloadJob> job{};
if (key.isEmpty()) {
job = new KazvIODownloadJob{localFileUrl.toLocalFile(), serverUrl,
showPrograssBar, hash, token, std::nullopt};
} else {
auto aes = Kazv::AES256CTRDesc{key.toStdString(), iv.toStdString()};
job = new KazvIODownloadJob{localFileUrl.toLocalFile(), serverUrl,
showPrograssBar, hash, token, aes};
}
return job;
}
QUrl KazvIOManager::cacheFile(const QUrl &fileUrl, const QString &id,
const QString &hash, const QString &token, const QString &key,
const QString &iv)
{
const QString filePath = m_d->cacheDir.filePath(id);
QDir dir(m_d->cacheDir.path());
if (!dir.exists(id) && this->getCacheJob(id) == nullptr) {
auto job = this->makeDownloadJob(fileUrl,
QUrl::fromLocalFile(filePath),
/* showProgressBar = */ false, hash, token, key, iv);
m_d->cacheJobs[id] = job;
connect(job, &KazvIOBaseJob::result, this, [this, id](auto /* result */) {
m_d->cacheJobs[id]->deleteLater();
m_d->cacheJobs.remove(id);
});
}
return QUrl::fromLocalFile(filePath);
}
KazvIOBaseJob *KazvIOManager::startNewDownloadJob(const QUrl &serverUrl,
const QUrl &localFileUrl, const QString &jobId, const QString &hash,
const QString &token, const QString &key, const QString &iv)
{
auto downloadJob = this->makeDownloadJob(serverUrl, localFileUrl,
/* showProgressBar = */ true, hash, token, key, iv);
m_d->downloadJobs[jobId].reset(downloadJob);
QQmlEngine::setObjectOwnership(downloadJob.data(), QQmlEngine::CppOwnership);
return downloadJob.data();
}
KazvIOBaseJob *KazvIOManager::startNewUploadJob(
const QUrl &serverUrl, const QUrl &localFileUrl, const QString &token,
const QString &roomId, MatrixRoomList *roomList, const bool encrypted,
const QString &relType, const QString &relatedTo)
{
auto uploadJob = QPointer<KazvIOUploadJob>();
if (encrypted) {
auto aes = Kazv::AES256CTRDesc::fromRandom(
m_d->randomGenerator.generateRange<Kazv::RandomData>(Kazv::AES256CTRDesc::randomSize));
uploadJob = new KazvIOUploadJob(localFileUrl.toLocalFile(), serverUrl,
/* showProgressBar = */ true, roomList, roomId, token, aes, relType, relatedTo);
} else {
uploadJob = new KazvIOUploadJob(localFileUrl.toLocalFile(), serverUrl,
/* showProgressBar = */ true, roomList, roomId, token, std::nullopt, relType, relatedTo);
}
if (!m_d->uploadJobs.contains(roomId) || m_d->uploadJobs[roomId].isNull()) {
m_d->uploadJobs[roomId].reset(new UploadJobModel());
}
m_d->uploadJobs[roomId]->addJob(uploadJob);
QQmlEngine::setObjectOwnership(uploadJob.data(), QQmlEngine::CppOwnership);
return uploadJob.data();
}
KazvIOBaseJob *KazvIOManager::startNewRoomlessUploadJob(const QUrl &serverUrl, const QUrl &localFileUrl, const QString &token)
{
return startNewUploadJob(
serverUrl, localFileUrl, token, ROOMLESS, 0, false,
QStringLiteral(""), QStringLiteral(""));
}
KazvIOBaseJob *KazvIOManager::getCacheJob(const QString &jobId) const
{
auto targetJob = m_d->cacheJobs.find(jobId);
if (targetJob == m_d->cacheJobs.end()) {
return nullptr;
}
QQmlEngine::setObjectOwnership(targetJob.value().data(), QQmlEngine::CppOwnership);
return targetJob.value().data();
}
KazvIOBaseJob *KazvIOManager::getDownloadJob(const QString &jobId)
{
auto targetJob = m_d->downloadJobs.find(jobId);
if (targetJob == m_d->downloadJobs.end()) {
return nullptr;
}
QQmlEngine::setObjectOwnership(targetJob.value().data(), QQmlEngine::CppOwnership);
return targetJob.value().data();
}
void KazvIOManager::deleteDownloadJob(const QString &jobId)
{
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].reset(new UploadJobModel());
}
QQmlEngine::setObjectOwnership(m_d->uploadJobs[roomId].data(), QQmlEngine::CppOwnership);
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::deleteRoomlessUploadJob(KazvIOBaseJob *job)
{
deleteUploadJob(ROOMLESS, job);
}
void KazvIOManager::deleteModelIfEmpty(const QString &roomId)
{
if (!m_d->uploadJobs.contains(roomId)) {
return;
}
if (m_d->uploadJobs[roomId]->rowCount() == 0) {
m_d->uploadJobs.remove(roomId);
}
}
void KazvIOManager::clearJobs()
{
/**
* KazvIOBaseJob::cancel() for cache jobs will remove itself from cacheJobs,
* so iterators and loops for m_d->cacheJobs cannot be used reliably.
*/
for (auto key : m_d->cacheJobs.keys()) {
m_d->cacheJobs[key]->cancel();
}
std::for_each(m_d->downloadJobs.begin(), m_d->downloadJobs.end(), [this](auto job){
job->cancel();
});
std::for_each(m_d->uploadJobs.begin(), m_d->uploadJobs.end(), [this](auto room) {
room->clearJobs();
});
m_d->downloadJobs.clear();
m_d->uploadJobs.clear();
}
QString KazvIOManager::cacheDirectory() const
{
return m_d->cacheDir.path();
}
void KazvIOManager::setCacheDirectory(QString cacheDirectory)
{
if (cacheDirectory.isEmpty()) {
m_d->cacheDir = QDir{m_d->tmpDir.path()};
} else {
auto cacheDir = QDir{QUrl{cacheDirectory}.toLocalFile()};
if (!cacheDir.mkpath(u"."_s)) {
return;
}
m_d->cacheDir = cacheDir;
}
m_d->downloadJobs.clear();
Q_EMIT cacheDirectoryChanged();
}
+
+bool KazvIOManager::fileExists(const QUrl &filePath) const
+{
+ return QFile(filePath.toLocalFile()).exists();
+}
diff --git a/src/kazv-io-manager.hpp b/src/kazv-io-manager.hpp
index e0e6114..1b8a3e8 100644
--- a/src/kazv-io-manager.hpp
+++ b/src/kazv-io-manager.hpp
@@ -1,78 +1,79 @@
/*
* This file is part of kazv.
* SPDX-FileCopyrightText: 2022-2023 nannanko <nannanko@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <kazv-defs.hpp>
#include "kazv-io-job.hpp"
#include "matrix-room.hpp"
#include "upload-job-model.hpp"
#include "matrix-room-list.hpp"
#include "matrix-sdk.hpp"
#include <QObject>
#include <QtQml>
#include <QUrl>
#include <QString>
#include <KIO/TransferJob>
#include <QPointer>
#include <memory>
using namespace Qt::Literals::StringLiterals;
struct KazvIOManagerPrivate;
class KazvIOManager : public QObject
{
Q_OBJECT
QML_ELEMENT
std::unique_ptr<KazvIOManagerPrivate> m_d;
Q_PROPERTY(QString cacheDirectory READ cacheDirectory WRITE setCacheDirectory NOTIFY cacheDirectoryChanged)
QPointer<KazvIODownloadJob> makeDownloadJob(
const QUrl &serverUrl, const QUrl &localFileUrl,
const bool showPrograssBar, const QString &hash,
const QString &token = u""_s, const QString &key = u""_s,
const QString &iv = u""_s) const;
public:
explicit KazvIOManager(QObject *parent = 0);
~KazvIOManager() override;
/**
* Cache the file specified by fileUrl into a temporary folder
* Multiple calls do not repeat downloads
* Returns the local url of the cached file.
*/
Q_INVOKABLE QUrl cacheFile(const QUrl &fileUrl, const QString &id,
const QString &token = u""_s, const QString &hash = u""_s,
const QString &key = u""_s, const QString &iv = u""_s);
Q_INVOKABLE KazvIOBaseJob *startNewDownloadJob(const QUrl &serverUrl,
const QUrl &localFileName, const QString &jobId, const QString &hash,
const QString &token = u""_s, const QString &key = u""_s,
const QString &iv = u""_s);
Q_INVOKABLE KazvIOBaseJob *startNewUploadJob(
const QUrl &serverUrl, const QUrl &localFileName, const QString &token,
const QString &roomId, MatrixRoomList *roomList, const bool encrypted,
const QString &relType, const QString &relatedTo);
Q_INVOKABLE KazvIOBaseJob *startNewRoomlessUploadJob(const QUrl &serverUrl, const QUrl &localFileName, const QString &token);
Q_INVOKABLE KazvIOBaseJob *getCacheJob(const QString &jobId) const;
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 deleteRoomlessUploadJob(KazvIOBaseJob *job);
Q_INVOKABLE void deleteModelIfEmpty(const QString &roomId);
Q_INVOKABLE void clearJobs();
+ Q_INVOKABLE bool fileExists(const QUrl &filePath) const;
QString cacheDirectory() const;
void setCacheDirectory(QString cacheDirectory);
Q_SIGNALS:
void cacheDirectoryChanged();
};

File Metadata

Mime Type
text/x-diff
Expires
Sat, Oct 10, 1:39 PM (1 d, 11 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1785255
Default Alt Text
(35 KB)

Event Timeline