Turn utils.js into a Utils.qml QtObject

When a .js module is used, every single file that imports it creates
its own duplicated environment in memory for that module.

Instead, use a simple QtObject with all the functions, and declare it in
Window.qml so that it is available to all children everywhere in the
app.
This commit is contained in:
miruka 2019-12-17 17:59:53 -04:00
parent c16731b239
commit 2cb64c5346
48 changed files with 400 additions and 430 deletions

View File

@ -58,11 +58,10 @@ def convert_to_qml(theme_content: str) -> str:
lines = [ lines = [
"import QtQuick 2.12", "import QtQuick 2.12",
'import "Base"', 'import "Base"',
'import "utils.js" as Utils',
"QtObject {", "QtObject {",
" function hsluv(h, s, l, a) { return Utils.hsluv(h, s, l, a) }", " function hsluv(h, s, l, a) { return utils.hsluv(h, s, l, a) }",
" function hsl(h, s, l) { return Utils.hsl(h, s, l) }", " function hsl(h, s, l) { return utils.hsl(h, s, l) }",
" function hsla(h, s, l, a) { return Utils.hsla(h, s, l, a) }", " function hsla(h, s, l, a) { return utils.hsla(h, s, l, a) }",
" id: theme", " id: theme",
] ]
lines += [f" {line}" for line in _process_lines(theme_content)] lines += [f" {line}" for line in _process_lines(theme_content)]

View File

@ -1,15 +1,14 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
import "../Base" import "../Base"
import "../utils.js" as Utils
Rectangle { Rectangle {
id: avatar id: avatar
implicitWidth: theme.controls.avatar.size implicitWidth: theme.controls.avatar.size
implicitHeight: theme.controls.avatar.size implicitHeight: theme.controls.avatar.size
color: avatarImage.visible ? "transparent" : Utils.hsluv( color: avatarImage.visible ? "transparent" : utils.hsluv(
name ? Utils.hueFrom(name) : 0, name ? utils.hueFrom(name) : 0,
name ? theme.controls.avatar.background.saturation : 0, name ? theme.controls.avatar.background.saturation : 0,
theme.controls.avatar.background.lightness, theme.controls.avatar.background.lightness,
theme.controls.avatar.background.opacity theme.controls.avatar.background.opacity
@ -34,8 +33,8 @@ Rectangle {
text: name ? name.charAt(0) : "?" text: name ? name.charAt(0) : "?"
font.pixelSize: parent.height / 1.4 font.pixelSize: parent.height / 1.4
color: Utils.hsluv( color: utils.hsluv(
name ? Utils.hueFrom(name) : 0, name ? utils.hueFrom(name) : 0,
name ? theme.controls.avatar.letter.saturation : 0, name ? theme.controls.avatar.letter.saturation : 0,
theme.controls.avatar.letter.lightness, theme.controls.avatar.letter.lightness,
theme.controls.avatar.letter.opacity theme.controls.avatar.letter.opacity

View File

@ -1,6 +1,5 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../utils.js" as Utils
Rectangle { Rectangle {
id: box id: box
@ -114,10 +113,10 @@ Rectangle {
property string name: modelData.name property string name: modelData.name
property Item next: buttonRepeater.itemAt( property Item next: buttonRepeater.itemAt(
Utils.numberWrapAt(index + 1, buttonRepeater.count), utils.numberWrapAt(index + 1, buttonRepeater.count),
) )
property Item previous: buttonRepeater.itemAt( property Item previous: buttonRepeater.itemAt(
Utils.numberWrapAt(index - 1, buttonRepeater.count), utils.numberWrapAt(index - 1, buttonRepeater.count),
) )
} }
} }

View File

@ -1,7 +1,6 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../utils.js" as Utils
CheckBox { CheckBox {
id: box id: box

View File

@ -1,6 +1,5 @@
import QtQuick 2.13 import QtQuick 2.13
import QtQuick.Controls 2.13 import QtQuick.Controls 2.13
import "../utils.js" as Utils
Drawer { Drawer {
id: drawer id: drawer

View File

@ -1,5 +1,4 @@
import QtQuick 2.12 import QtQuick 2.12
import "../utils.js" as Utils
Image { Image {
id: image id: image
@ -13,7 +12,7 @@ Image {
property bool broken: false property bool broken: false
property bool animate: true property bool animate: true
property bool animated: Utils.urlExtension(image.source) === "gif" property bool animated: utils.urlExtension(image.source) === "gif"
property alias showProgressBar: progressBarLoader.active property alias showProgressBar: progressBarLoader.active
property bool inderterminateProgressBar: false property bool inderterminateProgressBar: false

View File

@ -1,6 +1,5 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
import "../utils.js" as Utils
TextEdit { TextEdit {
id: label id: label

View File

@ -1,5 +1,4 @@
import QtQuick 2.12 import QtQuick 2.12
import "../utils.js" as Utils
FocusScope { FocusScope {
signal deselectAll() signal deselectAll()

View File

@ -1,6 +1,5 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../utils.js" as Utils
HButton { HButton {
id: tile id: tile

View File

@ -2,7 +2,6 @@ import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import QtAV 1.7 import QtAV 1.7
import "../../Base" import "../../Base"
import "../../utils.js" as Utils
HColumnLayout { HColumnLayout {
id: osd id: osd
@ -118,7 +117,7 @@ HColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.margins: padding / 4 anchors.margins: padding / 4
text: Utils.formatDuration(previewToolTip.wantTimestamp) text: utils.formatDuration(previewToolTip.wantTimestamp)
padding: theme.spacing / 2 padding: theme.spacing / 2
opacity: previewToolTip.wantTimestamp === -1 ? 0 : 1 opacity: previewToolTip.wantTimestamp === -1 ? 0 : 1
@ -197,7 +196,7 @@ HColumnLayout {
OSDButton { OSDButton {
id: speedButton id: speedButton
icon.name: "player-speed" icon.name: "player-speed"
text: qsTr("%1x").arg(Utils.round(media.playbackRate)) text: qsTr("%1x").arg(utils.round(media.playbackRate))
toolTip.text: qsTr("Reset speed") toolTip.text: qsTr("Reset speed")
onClicked: media.playbackRate = 1 onClicked: media.playbackRate = 1
} }
@ -225,11 +224,11 @@ HColumnLayout {
text: boundPosition && savedDuration ? text: boundPosition && savedDuration ?
qsTr("%1 / %2") qsTr("%1 / %2")
.arg(Utils.formatDuration(boundPosition)) .arg(utils.formatDuration(boundPosition))
.arg(Utils.formatDuration(savedDuration)) : .arg(utils.formatDuration(savedDuration)) :
boundPosition || savedDuration ? boundPosition || savedDuration ?
Utils.formatDuration(boundPosition || savedDuration) : utils.formatDuration(boundPosition || savedDuration) :
"" ""
} }
@ -239,7 +238,7 @@ HColumnLayout {
OSDLabel { OSDLabel {
text: boundPosition && savedDuration ? text: boundPosition && savedDuration ?
qsTr("-%1").arg( qsTr("-%1").arg(
Utils.formatDuration(savedDuration - boundPosition) utils.formatDuration(savedDuration - boundPosition)
) : "" ) : ""
} }

View File

@ -1,6 +1,5 @@
import QtQuick 2.12 import QtQuick 2.12
import "../../Base" import "../../Base"
import "../../utils.js" as Utils
Banner { Banner {
property string inviterId: chat.roomInfo.inviter property string inviterId: chat.roomInfo.inviter
@ -14,7 +13,7 @@ Banner {
avatar.mxc: inviterAvatar avatar.mxc: inviterAvatar
labelText: qsTr("%1 invited you to this room").arg( labelText: qsTr("%1 invited you to this room").arg(
Utils.coloredNameHtml(inviterName, inviterId) utils.coloredNameHtml(inviterName, inviterId)
) )
buttonModel: [ buttonModel: [

View File

@ -1,6 +1,5 @@
import QtQuick 2.12 import QtQuick 2.12
import "../../Base" import "../../Base"
import "../../utils.js" as Utils
Banner { Banner {
color: theme.chat.leftBanner.background color: theme.chat.leftBanner.background
@ -22,7 +21,7 @@ Banner {
buttonCallbacks: ({ buttonCallbacks: ({
forget: button => { forget: button => {
Utils.makePopup( utils.makePopup(
"Popups/ForgetRoomPopup.qml", "Popups/ForgetRoomPopup.qml",
mainUI, // Must not be destroyed with chat mainUI, // Must not be destroyed with chat
{ {

View File

@ -1,6 +1,5 @@
import QtQuick 2.12 import QtQuick 2.12
import "../../Base" import "../../Base"
import "../utils.js" as ChatJS
Banner { Banner {
color: theme.chat.unknownDevices.background color: theme.chat.unknownDevices.background

View File

@ -1,7 +1,6 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../Base" import "../Base"
import "../utils.js" as Utils
import "RoomPane" import "RoomPane"
Item { Item {
@ -16,10 +15,10 @@ Item {
property bool ready: userInfo !== "waiting" && roomInfo !== "waiting" property bool ready: userInfo !== "waiting" && roomInfo !== "waiting"
readonly property var userInfo: readonly property var userInfo:
Utils.getItem(modelSources["Account"] || [], "user_id", userId) || utils.getItem(modelSources["Account"] || [], "user_id", userId) ||
"waiting" "waiting"
readonly property var roomInfo: Utils.getItem( readonly property var roomInfo: utils.getItem(
modelSources[["Room", userId]] || [], "room_id", roomId modelSources[["Room", userId]] || [], "room_id", roomId
) || "waiting" ) || "waiting"

View File

@ -1,7 +1,6 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../Base" import "../Base"
import "../utils.js" as Utils
import "Banners" import "Banners"
import "Timeline" import "Timeline"
import "FileTransfer" import "FileTransfer"

View File

@ -2,7 +2,6 @@ import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../Base" import "../Base"
import "../Dialogs" import "../Dialogs"
import "../utils.js" as Utils
Rectangle { Rectangle {
property string indent: " " property string indent: " "
@ -12,7 +11,7 @@ Rectangle {
property string writingUserId: chat.userId property string writingUserId: chat.userId
readonly property var writingUserInfo: readonly property var writingUserInfo:
Utils.getItem(modelSources["Account"] || [], "user_id", writingUserId) utils.getItem(modelSources["Account"] || [], "user_id", writingUserId)
property bool textChangedSinceLostFocus: false property bool textChangedSinceLostFocus: false
@ -90,7 +89,7 @@ Rectangle {
} }
onTextChanged: { onTextChanged: {
if (Utils.isEmptyObject(aliases)) { if (utils.isEmptyObject(aliases)) {
writingUserId = Qt.binding(() => chat.userId) writingUserId = Qt.binding(() => chat.userId)
toSend = text toSend = text
setTyping(Boolean(text)) setTyping(Boolean(text))

View File

@ -1,7 +1,6 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../../Base" import "../../Base"
import "../../utils.js" as Utils
HColumnLayout { HColumnLayout {
id: transfer id: transfer
@ -102,7 +101,7 @@ HColumnLayout {
Repeater { Repeater {
model: [ model: [
msLeft ? qsTr("-%1").arg(Utils.formatDuration(msLeft)) : "", msLeft ? qsTr("-%1").arg(utils.formatDuration(msLeft)) : "",
speed ? qsTr("%1/s").arg(CppUtils.formattedBytes(speed)) : "", speed ? qsTr("%1/s").arg(CppUtils.formattedBytes(speed)) : "",

View File

@ -1,6 +1,5 @@
import QtQuick 2.12 import QtQuick 2.12
import "../../Base" import "../../Base"
import "../../utils.js" as Utils
HTileDelegate { HTileDelegate {
id: memberDelegate id: memberDelegate
@ -20,7 +19,7 @@ HTileDelegate {
title.text: model.display_name || model.user_id title.text: model.display_name || model.user_id
title.color: title.color:
memberDelegate.hovered ? memberDelegate.hovered ?
Utils.nameColor(model.display_name || model.user_id.substring(1)) : utils.nameColor(model.display_name || model.user_id.substring(1)) :
theme.chat.roomPane.member.name theme.chat.roomPane.member.name
subtitle.text: model.display_name ? model.user_id : "" subtitle.text: model.display_name ? model.user_id : ""

View File

@ -1,7 +1,6 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../../Base" import "../../Base"
import "../../utils.js" as Utils
HColumnLayout { HColumnLayout {
HListView { HListView {
@ -21,7 +20,7 @@ HColumnLayout {
function filterSource() { function filterSource() {
model.source = model.source =
Utils.filterModelSource(originSource, filterField.text) utils.filterModelSource(originSource, filterField.text)
} }
@ -77,7 +76,7 @@ HColumnLayout {
topPadding: 0 // XXX topPadding: 0 // XXX
bottomPadding: 0 bottomPadding: 0
onClicked: Utils.makePopup( onClicked: utils.makePopup(
"Popups/InviteToRoomPopup.qml", "Popups/InviteToRoomPopup.qml",
chat, chat,
{ {

View File

@ -1,7 +1,6 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../../Base" import "../../Base"
import "../../utils.js" as Utils
HBox { HBox {
color: "transparent" color: "transparent"

View File

@ -3,7 +3,6 @@ import QtQuick.Layouts 1.12
import QtAV 1.7 import QtAV 1.7
import "../../Base" import "../../Base"
import "../../Base/MediaPlayer" import "../../Base/MediaPlayer"
import "../../utils.js" as Utils
AudioPlayer { AudioPlayer {
id: audio id: audio

View File

@ -1,7 +1,6 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../../Base" import "../../Base"
import "../../utils.js" as Utils
HRowLayout { HRowLayout {
id: eventContent id: eventContent
@ -12,11 +11,11 @@ HRowLayout {
readonly property string senderText: readonly property string senderText:
hideNameLine ? "" : ( hideNameLine ? "" : (
"<div class='sender'>" + "<div class='sender'>" +
Utils.coloredNameHtml(model.sender_name, model.sender_id) + utils.coloredNameHtml(model.sender_name, model.sender_id) +
"</div>" "</div>"
) )
readonly property string contentText: Utils.processedEventText(model) readonly property string contentText: utils.processedEventText(model)
readonly property string timeText: Utils.formatTime(model.date, false) readonly property string timeText: utils.formatTime(model.date, false)
readonly property string localEchoText: readonly property string localEchoText:
model.is_local_echo ? model.is_local_echo ?
`&nbsp;<font size=${theme.fontSize.small}px></font>` : `&nbsp;<font size=${theme.fontSize.small}px></font>` :
@ -40,7 +39,7 @@ HRowLayout {
TapHandler { TapHandler {
enabled: debugMode enabled: debugMode
onDoubleTapped: onDoubleTapped:
Utils.debug(eventContent, null, con => { con.runJS("json()") }) utils.debug(eventContent, null, con => { con.runJS("json()") })
} }
Item { Item {
@ -149,7 +148,7 @@ HRowLayout {
visible: model.event_type === "RoomMessageNotice" visible: model.event_type === "RoomMessageNotice"
width: theme.chat.message.noticeLineWidth width: theme.chat.message.noticeLineWidth
height: parent.height height: parent.height
color: Utils.nameColor( color: utils.nameColor(
model.sender_name || model.sender_id.substring(1), model.sender_name || model.sender_id.substring(1),
) )
} }

View File

@ -1,7 +1,6 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../../Base" import "../../Base"
import "../../utils.js" as Utils
HColumnLayout { HColumnLayout {
id: eventDelegate id: eventDelegate
@ -63,7 +62,7 @@ HColumnLayout {
function json() { function json() {
return JSON.stringify( return JSON.stringify(
{ {
"model": Utils.getItem( "model": utils.getItem(
modelSources[[ modelSources[[
"Event", chat.userId, chat.roomId "Event", chat.userId, chat.roomId
]], ]],
@ -172,7 +171,7 @@ HColumnLayout {
HMenuItem { HMenuItem {
icon.name: "clear-messages" icon.name: "clear-messages"
text: qsTr("Clear messages") text: qsTr("Clear messages")
onTriggered: Utils.makePopup( onTriggered: utils.makePopup(
"Popups/ClearMessagesPopup.qml", "Popups/ClearMessagesPopup.qml",
chat, chat,
{userId: chat.userId, roomId: chat.roomId}, {userId: chat.userId, roomId: chat.roomId},

View File

@ -1,7 +1,6 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../../Base" import "../../Base"
import "../../utils.js" as Utils
HTile { HTile {
id: file id: file
@ -39,5 +38,5 @@ HTile {
property EventMediaLoader loader property EventMediaLoader loader
readonly property bool cryptDict: loader.singleMediaInfo.media_crypt_dict readonly property bool cryptDict: loader.singleMediaInfo.media_crypt_dict
readonly property bool isEncrypted: ! Utils.isEmptyObject(cryptDict) readonly property bool isEncrypted: ! utils.isEmptyObject(cryptDict)
} }

View File

@ -1,6 +1,5 @@
import QtQuick 2.12 import QtQuick 2.12
import "../../Base" import "../../Base"
import "../../utils.js" as Utils
HMxcImage { HMxcImage {
id: image id: image
@ -9,7 +8,7 @@ HMxcImage {
horizontalAlignment: Image.AlignLeft horizontalAlignment: Image.AlignLeft
animated: loader.singleMediaInfo.media_mime === "image/gif" || animated: loader.singleMediaInfo.media_mime === "image/gif" ||
Utils.urlExtension(loader.mediaUrl) === "gif" utils.urlExtension(loader.mediaUrl) === "gif"
thumbnail: ! animated && loader.thumbnailMxc thumbnail: ! animated && loader.thumbnailMxc
mxc: thumbnail ? mxc: thumbnail ?
(loader.thumbnailMxc || loader.mediaUrl) : (loader.thumbnailMxc || loader.mediaUrl) :
@ -21,12 +20,12 @@ HMxcImage {
property EventMediaLoader loader property EventMediaLoader loader
readonly property bool isEncrypted: ! Utils.isEmptyObject(cryptDict) readonly property bool isEncrypted: ! utils.isEmptyObject(cryptDict)
readonly property real maxHeight: readonly property real maxHeight:
theme.chat.message.thumbnailMaxHeightRatio theme.chat.message.thumbnailMaxHeightRatio
readonly property size fitSize: Utils.fitSize( readonly property size fitSize: utils.fitSize(
// Minimum display size // Minimum display size
theme.chat.message.thumbnailMinSize.width, theme.chat.message.thumbnailMinSize.width,
theme.chat.message.thumbnailMinSize.height, theme.chat.message.thumbnailMinSize.height,

View File

@ -1,6 +1,5 @@
import QtQuick 2.12 import QtQuick 2.12
import "../../Base" import "../../Base"
import "../../utils.js" as Utils
Rectangle { Rectangle {
property alias selectableLabelContainer: selectableLabelContainer property alias selectableLabelContainer: selectableLabelContainer
@ -100,7 +99,7 @@ Rectangle {
! canTalkBreak(item, itemAfter) && ! canTalkBreak(item, itemAfter) &&
! canDayBreak(item, itemAfter) && ! canDayBreak(item, itemAfter) &&
item.sender_id === itemAfter.sender_id && item.sender_id === itemAfter.sender_id &&
Utils.minutesBetween(item.date, itemAfter.date) <= 5 utils.minutesBetween(item.date, itemAfter.date) <= 5
) )
} }
@ -109,7 +108,7 @@ Rectangle {
return Boolean( return Boolean(
! canDayBreak(item, itemAfter) && ! canDayBreak(item, itemAfter) &&
Utils.minutesBetween(item.date, itemAfter.date) >= 20 utils.minutesBetween(item.date, itemAfter.date) >= 20
) )
} }

View File

@ -1,6 +1,5 @@
import QtQuick 2.12 import QtQuick 2.12
import "../../Base" import "../../Base"
import "../../utils.js" as Utils
HLoader { HLoader {
id: loader id: loader
@ -58,7 +57,7 @@ HLoader {
return EventDelegate.Media.File return EventDelegate.Media.File
// If this is a preview for a link in a normal message // If this is a preview for a link in a normal message
let ext = Utils.urlExtension(mediaUrl) let ext = utils.urlExtension(mediaUrl)
if (imageExtensions.includes(ext)) return EventDelegate.Media.Image if (imageExtensions.includes(ext)) return EventDelegate.Media.Image
if (videoExtensions.includes(ext)) return EventDelegate.Media.Video if (videoExtensions.includes(ext)) return EventDelegate.Media.Video

View File

@ -3,7 +3,6 @@ import QtQuick.Layouts 1.12
import QtAV 1.7 import QtAV 1.7
import "../../Base" import "../../Base"
import "../../Base/MediaPlayer" import "../../Base/MediaPlayer"
import "../../utils.js" as Utils
VideoPlayer { VideoPlayer {
id: video id: video

View File

@ -2,8 +2,6 @@ import QtQuick 2.12
import QtQuick.Window 2.12 import QtQuick.Window 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "Base" import "Base"
import "utils.js" as Utils
import "utils.js" as U
HDrawer { HDrawer {
id: debugConsole id: debugConsole
@ -31,9 +29,8 @@ HDrawer {
`Javascript debugging console `Javascript debugging console
Useful variables: Useful variables:
window, theme, settings, shortcuts, mainUI, pageLoader window, theme, settings, shortcuts, utils, mainUI, pageLoader
py Python interpreter py Python interpreter
U Utils/utils.js module
this The console itself this The console itself
t Target item to debug for which this console was opened t Target item to debug for which this console was opened
his History, list of commands entered his History, list of commands entered

View File

@ -1,6 +1,5 @@
import QtQuick 2.12 import QtQuick 2.12
import Qt.labs.platform 1.1 import Qt.labs.platform 1.1
import "../utils.js" as Utils
HFileDialogOpener { HFileDialogOpener {
fill: false fill: false
@ -11,7 +10,7 @@ HFileDialogOpener {
for (let file of files) { for (let file of files) {
let path = Qt.resolvedUrl(file).replace(/^file:/, "") let path = Qt.resolvedUrl(file).replace(/^file:/, "")
Utils.sendFile(userId, roomId, path, () => { utils.sendFile(userId, roomId, path, () => {
if (destroyWhenDone) destroy() if (destroyWhenDone) destroy()
}, },
(type, args, error, traceback) => { (type, args, error, traceback) => {

View File

@ -1,7 +1,6 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../Base" import "../Base"
import "../utils.js" as Utils
HTileDelegate { HTileDelegate {
id: accountDelegate id: accountDelegate
@ -113,7 +112,7 @@ HTileDelegate {
icon.name: "sign-out" icon.name: "sign-out"
icon.color: theme.colors.negativeBackground icon.color: theme.colors.negativeBackground
text: qsTr("Sign out") text: qsTr("Sign out")
onTriggered: Utils.makePopup( onTriggered: utils.makePopup(
"Popups/SignOutPopup.qml", "Popups/SignOutPopup.qml",
window, window,
{ "userId": model.data.user_id }, { "userId": model.data.user_id },

View File

@ -1,7 +1,6 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../Base" import "../Base"
import "../utils.js" as Utils
HListView { HListView {
id: mainPaneList id: mainPaneList
@ -28,12 +27,12 @@ HListView {
if (item.type === "Account" || if (item.type === "Account" ||
(filter ? (filter ?
Utils.filterMatches(filter, item.data.filter_string) : utils.filterMatches(filter, item.data.filter_string) :
! window.uiState.collapseAccounts[item.user_id])) ! window.uiState.collapseAccounts[item.user_id]))
{ {
if (filter && show.length && item.type === "Account" && if (filter && show.length && item.type === "Account" &&
show[show.length - 1].type === "Account" && show[show.length - 1].type === "Account" &&
! Utils.filterMatches( ! utils.filterMatches(
filter, show[show.length - 1].data.filter_string) filter, show[show.length - 1].data.filter_string)
) { ) {
// If filter active, current and previous items are // If filter active, current and previous items are
@ -48,7 +47,7 @@ HListView {
let last = show[show.length - 1] let last = show[show.length - 1]
if (show.length && filter && last.type === "Account" && if (show.length && filter && last.type === "Account" &&
! Utils.filterMatches(filter, last.data.filter_string)) ! utils.filterMatches(filter, last.data.filter_string))
{ {
// If filter active, last item is an account and last item // If filter active, last item is an account and last item
// doesn't match filter, that account had no matching rooms. // doesn't match filter, that account had no matching rooms.

View File

@ -1,7 +1,6 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../Base" import "../Base"
import "../utils.js" as Utils
HDrawer { HDrawer {
id: mainPane id: mainPane

View File

@ -1,7 +1,6 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../Base" import "../Base"
import "../utils.js" as Utils
HTileDelegate { HTileDelegate {
id: roomDelegate id: roomDelegate
@ -53,8 +52,8 @@ HTileDelegate {
! lastEvent || ! lastEvent.date ? ! lastEvent || ! lastEvent.date ?
"" : "" :
Utils.dateIsToday(lastEvent.date) ? utils.dateIsToday(lastEvent.date) ?
Utils.formatTime(lastEvent.date, false) : // no seconds utils.formatTime(lastEvent.date, false) : // no seconds
lastEvent.date.getFullYear() === new Date().getFullYear() ? lastEvent.date.getFullYear() === new Date().getFullYear() ?
Qt.formatDate(lastEvent.date, "d MMM") : // e.g. "5 Dec" Qt.formatDate(lastEvent.date, "d MMM") : // e.g. "5 Dec"
@ -76,10 +75,10 @@ HTileDelegate {
// If it's a general event // If it's a general event
if (isEmote || isUnknownMsg || (! isMsg && ! isCryptMedia)) { if (isEmote || isUnknownMsg || (! isMsg && ! isCryptMedia)) {
return Utils.processedEventText(lastEvent) return utils.processedEventText(lastEvent)
} }
let text = Utils.coloredNameHtml( let text = utils.coloredNameHtml(
lastEvent.sender_name, lastEvent.sender_id lastEvent.sender_name, lastEvent.sender_id
) + ": " + lastEvent.inline_content ) + ": " + lastEvent.inline_content
@ -96,7 +95,7 @@ HTileDelegate {
icon.name: "room-send-invite" icon.name: "room-send-invite"
text: qsTr("Invite members") text: qsTr("Invite members")
onTriggered: Utils.makePopup( onTriggered: utils.makePopup(
"Popups/InviteToRoomPopup.qml", "Popups/InviteToRoomPopup.qml",
window, window,
{ {
@ -118,7 +117,7 @@ HTileDelegate {
visible: invited visible: invited
icon.name: "invite-accept" icon.name: "invite-accept"
icon.color: theme.colors.positiveBackground icon.color: theme.colors.positiveBackground
text: qsTr("Accept %1's invite").arg(Utils.coloredNameHtml( text: qsTr("Accept %1's invite").arg(utils.coloredNameHtml(
model.data.inviter_name, model.data.inviter_id model.data.inviter_name, model.data.inviter_id
)) ))
label.textFormat: Text.StyledText label.textFormat: Text.StyledText
@ -134,7 +133,7 @@ HTileDelegate {
icon.color: theme.colors.negativeBackground icon.color: theme.colors.negativeBackground
text: invited ? qsTr("Decline invite") : qsTr("Leave") text: invited ? qsTr("Decline invite") : qsTr("Leave")
onTriggered: Utils.makePopup( onTriggered: utils.makePopup(
"Popups/LeaveRoomPopup.qml", "Popups/LeaveRoomPopup.qml",
window, window,
{ {
@ -150,7 +149,7 @@ HTileDelegate {
icon.color: theme.colors.negativeBackground icon.color: theme.colors.negativeBackground
text: qsTr("Forget") text: qsTr("Forget")
onTriggered: Utils.makePopup( onTriggered: utils.makePopup(
"Popups/ForgetRoomPopup.qml", "Popups/ForgetRoomPopup.qml",
window, window,
{ {

View File

@ -2,7 +2,6 @@ import QtQuick 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../../Base" import "../../Base"
import "../../utils.js" as Utils
HPage { HPage {
id: accountSettings id: accountSettings
@ -14,7 +13,7 @@ HPage {
readonly property bool ready: readonly property bool ready:
accountInfo !== "waiting" && Boolean(accountInfo.profile_updated) accountInfo !== "waiting" && Boolean(accountInfo.profile_updated)
readonly property var accountInfo: Utils.getItem( readonly property var accountInfo: utils.getItem(
modelSources["Account"] || [], "user_id", userId modelSources["Account"] || [], "user_id", userId
) || "waiting" ) || "waiting"
@ -22,7 +21,7 @@ HPage {
hideHeaderUnderHeight: avatarPreferredSize hideHeaderUnderHeight: avatarPreferredSize
headerLabel.text: qsTr("Account settings for %1").arg( headerLabel.text: qsTr("Account settings for %1").arg(
Utils.coloredNameHtml(headerName, userId) utils.coloredNameHtml(headerName, userId)
) )

View File

@ -1,7 +1,6 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../../Base" import "../../Base"
import "../../utils.js" as Utils
HBox { HBox {
buttonModel: [ buttonModel: [
@ -11,7 +10,7 @@ HBox {
buttonCallbacks: ({ buttonCallbacks: ({
export: button => { export: button => {
Utils.makeObject( utils.makeObject(
"Dialogs/ExportKeys.qml", "Dialogs/ExportKeys.qml",
accountSettings, accountSettings,
{ userId: accountSettings.userId }, { userId: accountSettings.userId },
@ -22,7 +21,7 @@ HBox {
) )
}, },
import: button => { import: button => {
Utils.makeObject( utils.makeObject(
"Dialogs/ImportKeys.qml", "Dialogs/ImportKeys.qml",
accountSettings, accountSettings,
{ userId: accountSettings.userId }, { userId: accountSettings.userId },

View File

@ -3,7 +3,6 @@ import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../../Base" import "../../Base"
import "../../Dialogs" import "../../Dialogs"
import "../../utils.js" as Utils
HGridLayout { HGridLayout {
function applyChanges() { function applyChanges() {
@ -77,7 +76,7 @@ HGridLayout {
1 : 0 1 : 0
anchors.fill: parent anchors.fill: parent
color: Utils.hsluv(0, 0, 0, color: utils.hsluv(0, 0, 0,
(! avatar.mxc && overlayHover.hovered) ? 0.8 : 0.7 (! avatar.mxc && overlayHover.hovered) ? 0.8 : 0.7
) )
@ -139,7 +138,7 @@ HGridLayout {
HLabel { HLabel {
text: qsTr("User ID:<br>%1") text: qsTr("User ID:<br>%1")
.arg(Utils.coloredNameHtml(userId, userId, userId)) .arg(utils.coloredNameHtml(userId, userId, userId))
textFormat: Text.StyledText textFormat: Text.StyledText
wrapMode: Text.Wrap wrapMode: Text.Wrap

View File

@ -1,7 +1,6 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../../Base" import "../../Base"
import "../../utils.js" as Utils
HPage { HPage {
id: addChatPage id: addChatPage
@ -10,7 +9,7 @@ HPage {
property string userId property string userId
readonly property var account: readonly property var account:
Utils.getItem(modelSources["Account"] || [], "user_id", userId) utils.getItem(modelSources["Account"] || [], "user_id", userId)
HTabContainer { HTabContainer {

View File

@ -1,7 +1,6 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../../Base" import "../../Base"
import "../../utils.js" as Utils
HBox { HBox {
id: addChatBox id: addChatBox

View File

@ -1,7 +1,6 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../../Base" import "../../Base"
import "../../utils.js" as Utils
HBox { HBox {
id: addChatBox id: addChatBox

View File

@ -1,7 +1,6 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../../Base" import "../../Base"
import "../../utils.js" as Utils
HBox { HBox {
id: addChatBox id: addChatBox

View File

@ -1,7 +1,6 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Layouts 1.12 import QtQuick.Layouts 1.12
import "../Base" import "../Base"
import "../utils.js" as Utils
HPopup { HPopup {
id: popup id: popup

View File

@ -1,5 +1,4 @@
import QtQuick 2.12 import QtQuick 2.12
import "../utils.js" as Utils
BoxPopup { BoxPopup {
id: popup id: popup
@ -25,7 +24,7 @@ BoxPopup {
box.buttonCallbacks: ({ box.buttonCallbacks: ({
ok: button => { ok: button => {
Utils.makeObject( utils.makeObject(
"Dialogs/ExportKeys.qml", "Dialogs/ExportKeys.qml",
mainUI, mainUI,
{ userId }, { userId },

View File

@ -1,7 +1,6 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
import "Base" import "Base"
import "utils.js" as Utils
Item { Item {
visible: false visible: false
@ -35,7 +34,7 @@ Item {
if (debugConsole) { if (debugConsole) {
debugConsole.visible = ! debugConsole.visible debugConsole.visible = ! debugConsole.visible
} else { } else {
Utils.debug(mainUI || window) utils.debug(mainUI || window)
} }
} }
} }
@ -72,37 +71,37 @@ Item {
HShortcut { HShortcut {
enabled: toFlick enabled: toFlick
sequences: settings.keys.scrollUp sequences: settings.keys.scrollUp
onActivated: Utils.flickPages(toFlick, -1 / 10) onActivated: utils.flickPages(toFlick, -1 / 10)
} }
HShortcut { HShortcut {
enabled: toFlick enabled: toFlick
sequences: settings.keys.scrollDown sequences: settings.keys.scrollDown
onActivated: Utils.flickPages(toFlick, 1 / 10) onActivated: utils.flickPages(toFlick, 1 / 10)
} }
HShortcut { HShortcut {
enabled: toFlick enabled: toFlick
sequences: settings.keys.scrollPageUp sequences: settings.keys.scrollPageUp
onActivated: Utils.flickPages(toFlick, -1) onActivated: utils.flickPages(toFlick, -1)
} }
HShortcut { HShortcut {
enabled: toFlick enabled: toFlick
sequences: settings.keys.scrollPageDown sequences: settings.keys.scrollPageDown
onActivated: Utils.flickPages(toFlick, 1) onActivated: utils.flickPages(toFlick, 1)
} }
HShortcut { HShortcut {
enabled: toFlick enabled: toFlick
sequences: settings.keys.scrollToTop sequences: settings.keys.scrollToTop
onActivated: Utils.flickToTop(toFlick) onActivated: utils.flickToTop(toFlick)
} }
HShortcut { HShortcut {
enabled: toFlick enabled: toFlick
sequences: settings.keys.scrollToBottom sequences: settings.keys.scrollToBottom
onActivated: Utils.flickToBottom(toFlick) onActivated: utils.flickToBottom(toFlick)
} }
@ -112,7 +111,7 @@ Item {
enabled: tabsTarget enabled: tabsTarget
sequences: settings.keys.previousTab sequences: settings.keys.previousTab
onActivated: tabsTarget.setCurrentIndex( onActivated: tabsTarget.setCurrentIndex(
Utils.numberWrapAt(tabsTarget.currentIndex - 1, tabsTarget.count), utils.numberWrapAt(tabsTarget.currentIndex - 1, tabsTarget.count),
) )
} }
@ -120,7 +119,7 @@ Item {
enabled: tabsTarget enabled: tabsTarget
sequences: settings.keys.nextTab sequences: settings.keys.nextTab
onActivated: tabsTarget.setCurrentIndex( onActivated: tabsTarget.setCurrentIndex(
Utils.numberWrapAt(tabsTarget.currentIndex + 1, tabsTarget.count), utils.numberWrapAt(tabsTarget.currentIndex + 1, tabsTarget.count),
) )
} }
@ -185,7 +184,7 @@ Item {
HShortcut { HShortcut {
enabled: window.uiState.page === "Chat/Chat.qml" enabled: window.uiState.page === "Chat/Chat.qml"
sequences: settings.keys.clearRoomMessages sequences: settings.keys.clearRoomMessages
onActivated: Utils.makePopup( onActivated: utils.makePopup(
"Popups/ClearMessagesPopup.qml", "Popups/ClearMessagesPopup.qml",
mainUI, mainUI,
{ {
@ -198,7 +197,7 @@ Item {
HShortcut { HShortcut {
enabled: window.uiState.page === "Chat/Chat.qml" enabled: window.uiState.page === "Chat/Chat.qml"
sequences: settings.keys.sendFile sequences: settings.keys.sendFile
onActivated: Utils.makeObject( onActivated: utils.makeObject(
"Dialogs/SendFilePicker.qml", "Dialogs/SendFilePicker.qml",
mainUI, mainUI,
{ {
@ -213,7 +212,7 @@ Item {
HShortcut { HShortcut {
enabled: window.uiState.page === "Chat/Chat.qml" enabled: window.uiState.page === "Chat/Chat.qml"
sequences: settings.keys.sendFileFromPathInClipboard sequences: settings.keys.sendFileFromPathInClipboard
onActivated: Utils.sendFile( onActivated: utils.sendFile(
window.uiState.pageProperties.userId, window.uiState.pageProperties.userId,
window.uiState.pageProperties.roomId, window.uiState.pageProperties.roomId,
Clipboard.text.trim(), Clipboard.text.trim(),

View File

@ -5,7 +5,6 @@ import QtQuick.Window 2.7
import QtGraphicalEffects 1.12 import QtGraphicalEffects 1.12
import "Base" import "Base"
import "MainPane" import "MainPane"
import "utils.js" as Utils
Item { Item {
id: mainUI id: mainUI

323
src/qml/Utils.qml Normal file
View File

@ -0,0 +1,323 @@
import QtQuick 2.12
QtObject {
function makeObject(urlComponent, parent=null, properties={},
callback=null) {
let comp = urlComponent
if (! Qt.isQtObject(urlComponent)) {
// It's an url or path string to a component
comp = Qt.createComponent(urlComponent, Component.Asynchronous)
}
let ready = false
comp.statusChanged.connect(status => {
if ([Component.Null, Component.Error].includes(status)) {
console.error("Failed creating component: ", comp.errorString())
} else if (! ready && status === Component.Ready) {
let incu = comp.incubateObject(
parent, properties, Qt.Asynchronous,
)
if (incu.status === Component.Ready) {
if (callback) callback(incu.object)
ready = true
return
}
incu.onStatusChanged = (istatus) => {
if (incu.status === Component.Error) {
console.error("Failed incubating object: ",
incu.errorString())
} else if (istatus === Component.Ready &&
callback && ! ready) {
if (callback) callback(incu.object)
ready = true
}
}
}
})
if (comp.status === Component.Ready) comp.statusChanged(comp.status)
}
function makePopup(urlComponent, parent=null, properties={}, callback=null,
autoDestruct=true) {
makeObject(urlComponent, parent, properties, (popup) => {
popup.open()
if (autoDestruct) popup.closed.connect(() => { popup.destroy() })
if (callback) callback(popup)
})
}
function debug(target, parent=null, callback=null) {
parent = parent || target
return utils.makeObject(
"DebugConsole.qml", parent, { target }, callback,
)
}
function isEmptyObject(obj) {
return Object.entries(obj).length === 0 && obj.constructor === Object
}
function objectUpdateRecursive(current, update) {
for (const key of Object.keys(update)) {
if ((key in current) && typeof(current[key]) === "object" &&
typeof(update[key]) === "object") {
objectUpdateRecursive(current[key], update[key])
} else {
current[key] = update[key]
}
}
}
function numberWrapAt(num, max) {
return num < 0 ? max + (num % max) : (num % max)
}
function hsluv(hue, saturation, lightness, alpha=1.0) {
hue = numberWrapAt(hue, 360)
let rgb = py.callSync("hsluv", [hue, saturation, lightness])
return Qt.rgba(rgb[0], rgb[1], rgb[2], alpha)
}
function hueFrom(string) {
// Calculate and return a unique hue between 0 and 360 for the string
let hue = 0
for (let i = 0; i < string.length; i++) {
hue += string.charCodeAt(i) * 99
}
return hue % 360
}
function nameColor(name) {
return hsluv(
hueFrom(name),
theme.controls.displayName.saturation,
theme.controls.displayName.lightness,
)
}
function coloredNameHtml(name, userId, displayText=null,
disambiguate=false) {
// substring: remove leading @
return `<font color="${nameColor(name || userId.substring(1))}">` +
escapeHtml(displayText || name || userId) +
"</font>"
}
function escapeHtml(string) {
// Replace special HTML characters by encoded alternatives
return string.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace('"', "&quot;")
.replace("'", "&#039;")
}
function processedEventText(ev) {
if (ev.event_type === "RoomMessageEmote")
return coloredNameHtml(ev.sender_name, ev.sender_id) + " " +
ev.content
let unknown = ev.event_type === "RoomMessageUnknown"
if (ev.event_type.startsWith("RoomMessage") && ! unknown)
return ev.content
if (ev.event_type.startsWith("RoomEncrypted")) return ev.content
let text = qsTr(ev.content).arg(
coloredNameHtml(ev.sender_name, ev.sender_id)
)
if (text.includes("%2") && ev.target_id)
text = text.arg(coloredNameHtml(ev.target_name, ev.target_id))
return text
}
function filterMatches(filter, text) {
let filter_lower = filter.toLowerCase()
if (filter_lower === filter) {
// Consider case only if filter isn't all lowercase (smart case)
filter = filter_lower
text = text.toLowerCase()
}
for (let word of filter.split(" ")) {
if (word && ! text.includes(word)) {
return false
}
}
return true
}
function filterModelSource(source, filter_text, property="filter_string") {
if (! filter_text) return source
let results = []
for (let i = 0; i < source.length; i++) {
if (filterMatches(filter_text, source[i][property])) {
results.push(source[i])
}
}
return results
}
function fitSize(minWidth, minHeight, width, height, maxWidth, maxHeight) {
if (width >= height) {
let new_width = Math.max(Math.min(width, maxWidth), minWidth)
return Qt.size(new_width, height / (width / new_width))
}
let new_height = Math.max(Math.min(height, maxHeight), minHeight)
return Qt.size(width / (height / new_height), new_height)
}
function minutesBetween(date1, date2) {
return ((date2 - date1) / 1000) / 60
}
function dateIsDay(date, dayDate) {
return date.getDate() === dayDate.getDate() &&
date.getMonth() === dayDate.getMonth() &&
date.getFullYear() === dayDate.getFullYear()
}
function dateIsToday(date) {
return dateIsDay(date, new Date())
}
function dateIsYesterday(date) {
const yesterday = new Date()
yesterday.setDate(yesterday.getDate() - 1)
return dateIsDay(date, yesterday)
}
function formatTime(time, seconds=true) {
return Qt.formatTime(
time,
Qt.locale().timeFormat(
seconds ? Locale.LongFormat : Locale.NarrowFormat
).replace(/\./g, ":").replace(/ t$/, "")
// en_DK.UTF-8 locale wrongfully gives "." separators;
// also remove the timezone at the end
)
}
function formatDuration(milliseconds) {
let totalSeconds = milliseconds / 1000
let hours = Math.floor(totalSeconds / 3600)
let minutes = Math.floor((totalSeconds % 3600) / 60)
let seconds = Math.floor(totalSeconds % 60)
if (seconds < 10) seconds = `0${seconds}`
if (hours < 1) return `${minutes}:${seconds}`
if (minutes < 10) minutes = `0${minutes}`
return `${hours}:${minutes}:${seconds}`
}
function round(floatNumber) {
return parseFloat(floatNumber.toFixed(2))
}
function getItem(array, mainKey, value) {
for (let i = 0; i < array.length; i++) {
if (array[i][mainKey] === value) { return array[i] }
}
return undefined
}
function flickPages(flickable, pages) {
// Adapt velocity and deceleration for the number of pages to flick.
// If this is a repeated flicking, flick faster than a single flick.
if (! flickable.interactive && flickable.allowDragging) return
const futureVelocity = -flickable.height * pages
const currentVelocity = -flickable.verticalVelocity
const goFaster =
(futureVelocity < 0 && currentVelocity < futureVelocity / 2) ||
(futureVelocity > 0 && currentVelocity > futureVelocity / 2)
const normalDecel = flickable.flickDeceleration
const fastMultiply = pages && 8 / (1 - Math.log10(Math.abs(pages)))
const magicNumber = 2.5
flickable.flickDeceleration = Math.max(
goFaster ? normalDecel : -Infinity,
Math.abs(normalDecel * magicNumber * pages),
)
flickable.flick(
0, futureVelocity * magicNumber * (goFaster ? fastMultiply : 1),
)
flickable.flickDeceleration = normalDecel
}
function flickToTop(flickable) {
if (! flickable.interactive && flickable.allowDragging) return
if (flickable.visibleArea.yPosition < 0) return
flickable.contentY -= flickable.contentHeight
flickable.returnToBounds()
flickable.flick(0, -100) // Force the delegates to load
}
function flickToBottom(flickable) {
if (! flickable.interactive && flickable.allowDragging) return
if (flickable.visibleArea.yPosition < 0) return
flickable.contentY = flickTarget.contentHeight - flickTarget.height
flickable.returnToBounds()
flickable.flick(0, 100)
}
function urlExtension(url) {
return url.toString().split("/").slice(-1)[0].split("?")[0].split(".")
.slice(-1)[0].toLowerCase()
}
function sendFile(userId, roomId, path, onSuccess, onError) {
py.callClientCoro(
userId, "send_file", [roomId, path], onSuccess, onError,
)
}
}

View File

@ -1,7 +1,6 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
import "Base" import "Base"
import "utils.js" as Utils
ApplicationWindow { ApplicationWindow {
id: window id: window
@ -55,7 +54,7 @@ ApplicationWindow {
propertyValues[prop] = obj[prop] propertyValues[prop] = obj[prop]
} }
Utils.objectUpdateRecursive(uiState, { utils.objectUpdateRecursive(uiState, {
[obj.saveName]: { [obj.saveId || "ALL"]: propertyValues }, [obj.saveName]: { [obj.saveId || "ALL"]: propertyValues },
}) })
@ -73,6 +72,8 @@ ApplicationWindow {
Python { id: py } Python { id: py }
Utils { id: utils }
HoverHandler { id: windowHover } HoverHandler { id: windowHover }
HLoader { HLoader {

View File

@ -1,307 +0,0 @@
function makeObject(urlComponent, parent=null, properties={}, callback=null) {
let comp = urlComponent
if (! Qt.isQtObject(urlComponent)) {
// It's an url or path string to a component
comp = Qt.createComponent(urlComponent, Component.Asynchronous)
}
let ready = false
comp.statusChanged.connect(status => {
if ([Component.Null, Component.Error].includes(status)) {
console.error("Failed creating component: ", comp.errorString())
} else if (! ready && status === Component.Ready) {
let incu = comp.incubateObject(parent, properties, Qt.Asynchronous)
if (incu.status === Component.Ready) {
if (callback) callback(incu.object)
ready = true
return
}
incu.onStatusChanged = (istatus) => {
if (incu.status === Component.Error) {
console.error("Failed incubating object: ",
incu.errorString())
} else if (istatus === Component.Ready && callback && ! ready) {
if (callback) callback(incu.object)
ready = true
}
}
}
})
if (comp.status === Component.Ready) comp.statusChanged(comp.status)
}
function makePopup(urlComponent, parent=null, properties={}, callback=null,
autoDestruct=true) {
makeObject(urlComponent, parent, properties, (popup) => {
popup.open()
if (autoDestruct) popup.closed.connect(() => { popup.destroy() })
if (callback) callback(popup)
})
}
function debug(target, parent=null, callback=null) {
parent = parent || target
return Utils.makeObject("DebugConsole.qml", parent, { target }, callback)
}
function isEmptyObject(obj) {
return Object.entries(obj).length === 0 && obj.constructor === Object
}
function objectUpdateRecursive(current, update) {
for (const key of Object.keys(update)) {
if ((key in current) && typeof(current[key]) === "object" &&
typeof(update[key]) === "object") {
objectUpdateRecursive(current[key], update[key])
} else {
current[key] = update[key]
}
}
}
function numberWrapAt(num, max) {
return num < 0 ? max + (num % max) : (num % max)
}
function hsluv(hue, saturation, lightness, alpha=1.0) {
hue = numberWrapAt(hue, 360)
let rgb = py.callSync("hsluv", [hue, saturation, lightness])
return Qt.rgba(rgb[0], rgb[1], rgb[2], alpha)
}
function hueFrom(string) {
// Calculate and return a unique hue between 0 and 360 for the string
let hue = 0
for (let i = 0; i < string.length; i++) {
hue += string.charCodeAt(i) * 99
}
return hue % 360
}
function nameColor(name) {
return hsluv(
hueFrom(name),
theme.controls.displayName.saturation,
theme.controls.displayName.lightness,
)
}
function coloredNameHtml(name, userId, displayText=null, disambiguate=false) {
// substring: remove leading @
return `<font color="${nameColor(name || userId.substring(1))}">` +
escapeHtml(displayText || name || userId) +
"</font>"
}
function escapeHtml(string) {
// Replace special HTML characters by encoded alternatives
return string.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace('"', "&quot;")
.replace("'", "&#039;")
}
function processedEventText(ev) {
if (ev.event_type === "RoomMessageEmote")
return coloredNameHtml(ev.sender_name, ev.sender_id) + " " + ev.content
let unknown = ev.event_type === "RoomMessageUnknown"
if (ev.event_type.startsWith("RoomMessage") && ! unknown) return ev.content
if (ev.event_type.startsWith("RoomEncrypted")) return ev.content
let text = qsTr(ev.content).arg(
coloredNameHtml(ev.sender_name, ev.sender_id)
)
if (text.includes("%2") && ev.target_id)
text = text.arg(coloredNameHtml(ev.target_name, ev.target_id))
return text
}
function filterMatches(filter, text) {
let filter_lower = filter.toLowerCase()
if (filter_lower === filter) {
// Consider case only if filter isn't all lowercase (smart case)
filter = filter_lower
text = text.toLowerCase()
}
for (let word of filter.split(" ")) {
if (word && ! text.includes(word)) {
return false
}
}
return true
}
function filterModelSource(source, filter_text, property="filter_string") {
if (! filter_text) return source
let results = []
for (let i = 0; i < source.length; i++) {
if (filterMatches(filter_text, source[i][property])) {
results.push(source[i])
}
}
return results
}
function fitSize(minWidth, minHeight, width, height, maxWidth, maxHeight) {
if (width >= height) {
let new_width = Math.max(Math.min(width, maxWidth), minWidth)
return Qt.size(new_width, height / (width / new_width))
}
let new_height = Math.max(Math.min(height, maxHeight), minHeight)
return Qt.size(width / (height / new_height), new_height)
}
function minutesBetween(date1, date2) {
return ((date2 - date1) / 1000) / 60
}
function dateIsDay(date, dayDate) {
return date.getDate() === dayDate.getDate() &&
date.getMonth() === dayDate.getMonth() &&
date.getFullYear() === dayDate.getFullYear()
}
function dateIsToday(date) {
return dateIsDay(date, new Date())
}
function dateIsYesterday(date) {
const yesterday = new Date()
yesterday.setDate(yesterday.getDate() - 1)
return dateIsDay(date, yesterday)
}
function formatTime(time, seconds=true) {
return Qt.formatTime(
time,
Qt.locale().timeFormat(
seconds ? Locale.LongFormat : Locale.NarrowFormat
).replace(/\./g, ":").replace(/ t$/, "")
// en_DK.UTF-8 locale wrongfully gives "." separators;
// also remove the timezone at the end
)
}
function formatDuration(milliseconds) {
let totalSeconds = milliseconds / 1000
let hours = Math.floor(totalSeconds / 3600)
let minutes = Math.floor((totalSeconds % 3600) / 60)
let seconds = Math.floor(totalSeconds % 60)
if (seconds < 10) seconds = `0${seconds}`
if (hours < 1) return `${minutes}:${seconds}`
if (minutes < 10) minutes = `0${minutes}`
return `${hours}:${minutes}:${seconds}`
}
function round(float) {
return parseFloat(float.toFixed(2))
}
function getItem(array, mainKey, value) {
for (let i = 0; i < array.length; i++) {
if (array[i][mainKey] === value) { return array[i] }
}
return undefined
}
function flickPages(flickable, pages) {
// Adapt velocity and deceleration for the number of pages to flick.
// If this is a repeated flicking, flick faster than a single flick.
if (! flickable.interactive && flickable.allowDragging) return
const futureVelocity = -flickable.height * pages
const currentVelocity = -flickable.verticalVelocity
const goFaster =
(futureVelocity < 0 && currentVelocity < futureVelocity / 2) ||
(futureVelocity > 0 && currentVelocity > futureVelocity / 2)
const normalDecel = flickable.flickDeceleration
const fastMultiply = pages && 8 / (1 - Math.log10(Math.abs(pages)))
const magicNumber = 2.5
flickable.flickDeceleration = Math.max(
goFaster ? normalDecel : -Infinity,
Math.abs(normalDecel * magicNumber * pages),
)
flickable.flick(
0, futureVelocity * magicNumber * (goFaster ? fastMultiply : 1),
)
flickable.flickDeceleration = normalDecel
}
function flickToTop(flickable) {
if (! flickable.interactive && flickable.allowDragging) return
if (flickable.visibleArea.yPosition < 0) return
flickable.contentY -= flickable.contentHeight
flickable.returnToBounds()
flickable.flick(0, -100) // Force the delegates to load
}
function flickToBottom(flickable) {
if (! flickable.interactive && flickable.allowDragging) return
if (flickable.visibleArea.yPosition < 0) return
flickable.contentY = flickTarget.contentHeight - flickTarget.height
flickable.returnToBounds()
flickable.flick(0, 100)
}
function urlExtension(url) {
return url.toString().split("/").slice(-1)[0].split("?")[0].split(".")
.slice(-1)[0].toLowerCase()
}
function sendFile(userId, roomId, path, onSuccess, onError) {
py.callClientCoro(userId, "send_file", [roomId, path], onSuccess, onError)
}