diff --git a/Linphone/view/App/Layout/LoginLayout.qml b/Linphone/view/App/Layout/LoginLayout.qml index d38f380b9..40d15af51 100644 --- a/Linphone/view/App/Layout/LoginLayout.qml +++ b/Linphone/view/App/Layout/LoginLayout.qml @@ -8,10 +8,11 @@ import QtQuick.Controls 2.2 as Control import Linphone -Item { +Rectangle { id: mainItem property alias titleContent : titleLayout.children property alias centerContent : centerLayout.children + color: DefaultStyle.grey_0 ColumnLayout { anchors.rightMargin: 40 * DefaultStyle.dp anchors.leftMargin: 119 * DefaultStyle.dp diff --git a/Linphone/view/App/Layout/MainLayout.qml b/Linphone/view/App/Layout/MainLayout.qml index 887b6aa47..0461ab735 100644 --- a/Linphone/view/App/Layout/MainLayout.qml +++ b/Linphone/view/App/Layout/MainLayout.qml @@ -80,248 +80,274 @@ Item { } } } - - RowLayout { + Item{ anchors.fill: parent - // spacing: 30 - anchors.topMargin: 18 * DefaultStyle.dp - VerticalTabBar { - id: tabbar - Layout.fillHeight: true - model: [ - {icon: AppIcons.phone, selectedIcon: AppIcons.phoneSelected, label: qsTr("Appels")}, - {icon: AppIcons.adressBook, selectedIcon: AppIcons.adressBookSelected, label: qsTr("Contacts")}, - {icon: AppIcons.chatTeardropText, selectedIcon: AppIcons.chatTeardropTextSelected, label: qsTr("Conversations")}, - {icon: AppIcons.usersThree, selectedIcon: AppIcons.usersThreeSelected, label: qsTr("Réunions")} - ] - } - ColumnLayout { - Layout.fillWidth: true - Layout.fillHeight: true - RowLayout { - id: topRow - Layout.leftMargin: 25 * DefaultStyle.dp - Layout.rightMargin: 41 * DefaultStyle.dp - SearchBar { - id: magicSearchBar + ColumnLayout{ + anchors.fill: parent + spacing: 0 + Rectangle{ + Layout.fillWidth: true + Layout.fillHeight: true + color: DefaultStyle.grey_0 + } + RowLayout{ + Layout.fillWidth: true + Layout.preferredHeight: tabbar.cornerRadius + spacing: 0 + Item{// Transparent corner + Layout.fillHeight: true + Layout.preferredWidth: tabbar.cornerRadius + } + Rectangle{ Layout.fillWidth: true - placeholderText: qsTr("Rechercher un contact, appeler ou envoyer un message...") - onTextChanged: { - if (text.length != 0) listPopup.open() - else listPopup.close() - } - Popup { - id: listPopup - width: magicSearchList.width + listPopup.rightPadding + listPopup.leftPadding - height: Math.min(magicSearchList.contentHeight, 300 * DefaultStyle.dp + topPadding + bottomPadding) - y: magicSearchBar.height - closePolicy: Popup.NoAutoClose - topPadding: 10 * DefaultStyle.dp - bottomPadding: 10 * DefaultStyle.dp - rightPadding: 10 * DefaultStyle.dp - leftPadding: 10 * DefaultStyle.dp - background: Item { - anchors.fill: parent - Rectangle { - id: popupBg - radius: 15 * DefaultStyle.dp - anchors.fill: parent - // width: magicSearchList.width + listPopup.rightPadding + listPopup.leftPadding - } - MultiEffect { - source: popupBg - anchors.fill: popupBg - shadowEnabled: true - shadowBlur: 1 - shadowColor: DefaultStyle.grey_1000 - shadowOpacity: 0.1 - } - } - Control.ScrollBar { - id: scrollbar - height: parent.height - anchors.right: parent.right - } - ContactsList { - id: magicSearchList - visible: magicSearchBar.text.length != 0 - height: Math.min(contentHeight, listPopup.height) - width: magicSearchBar.width - initialHeadersVisible: false - contactMenuVisible: false - model: MagicSearchProxy { - searchText: magicSearchBar.text.length === 0 ? "*" : magicSearchBar.text - aggregationFlag: LinphoneEnums.MagicSearchAggregation.Friend - } - Control.ScrollBar.vertical: scrollbar - header: ColumnLayout { - width: magicSearchList.width - RowLayout { - Layout.fillWidth: true - spacing: 10 * DefaultStyle.dp - Avatar { - Layout.preferredWidth: 45 * DefaultStyle.dp - Layout.preferredHeight: 45 * DefaultStyle.dp - address: sipAddr.text - } - ColumnLayout { - Text { - text: magicSearchBar.text - font { - pixelSize: 14 * DefaultStyle.dp - weight: 700 * DefaultStyle.dp - } - } - Text { - id: sipAddr - text: UtilsCpp.generateLinphoneSipAddress(magicSearchBar.text) - } - } - Item { - Layout.fillWidth: true - } - Button { - background: Item{} - Layout.preferredWidth: 24 * DefaultStyle.dp - Layout.preferredHeight: 24 * DefaultStyle.dp - contentItem: Image { - width: 24 * DefaultStyle.dp - height: 24 * DefaultStyle.dp - source: AppIcons.phone - } - onClicked: { - var callObj = UtilsCpp.createCall(sipAddr) - } - } - } - Button { - Layout.fillWidth: true - color: DefaultStyle.main2_200 - pressedColor: DefaultStyle.main2_400 - background: Rectangle { - anchors.fill: parent - color: DefaultStyle.main2_200 - } - contentItem: RowLayout { - spacing: 10 * DefaultStyle.dp - Image { - source: AppIcons.userPlus - } - Text { - text: qsTr("Ajouter ce contact") - font { - pixelSize: 14 * DefaultStyle.dp - weight: 700 * DefaultStyle.dp - capitalization: Font.AllUppercase - } - } - Item {Layout.fillWidth: true} - } - onClicked: { - var currentItem = mainStackLayout.children[mainStackLayout.currentIndex] - listPopup.close() - currentItem.createContact(magicSearchBar.text, sipAddr.text) - } - } - } - delegateButtons: Button { - Layout.preferredWidth: 24 * DefaultStyle.dp - Layout.preferredHeight: 24 * DefaultStyle.dp - background: Item{} - contentItem: Image { - anchors.fill: parent - width: 24 * DefaultStyle.dp - height: 24 * DefaultStyle.dp - source: AppIcons.phone - } - } - } - } - } - PopupButton { - id: avatarButton - AccountProxy{ - id: accountProxy - //property bool haveAvatar: defaultAccount && defaultAccount.core.pictureUri || false - } - background.visible: false - Layout.preferredWidth: 54 * DefaultStyle.dp - Layout.preferredHeight: width - contentItem: Avatar { - id: avatar - height: avatarButton.height - width: avatarButton.width - account: accountProxy.defaultAccount - } - popup.x: width - popup.width - popup.padding: 0 - popup.contentItem: ColumnLayout { - Accounts { - id: accounts - onAddAccountRequest: mainItem.addAccountRequest() - } - } - } - PopupButton { - id: settingsButton - Layout.preferredWidth: 24 * DefaultStyle.dp - Layout.preferredHeight: 24 * DefaultStyle.dp - popup.x: width - popup.width - popup.width: 271 * DefaultStyle.dp - popup.contentItem: ColumnLayout { - spacing: 20 * DefaultStyle.dp - IconLabelButton { - Layout.preferredHeight: 32 * DefaultStyle.dp - iconSize: 32 * DefaultStyle.dp - text: qsTr("Mon compte") - iconSource: AppIcons.manageProfile - onClicked: console.log("TODO : manage profile") - } - IconLabelButton { - Layout.preferredHeight: 32 * DefaultStyle.dp - iconSize: 32 * DefaultStyle.dp - text: qsTr("Paramètres") - iconSource: AppIcons.settings - } - IconLabelButton { - Layout.preferredHeight: 32 * DefaultStyle.dp - iconSize: 32 * DefaultStyle.dp - text: qsTr("Enregistrements") - iconSource: AppIcons.micro - } - IconLabelButton { - Layout.preferredHeight: 32 * DefaultStyle.dp - iconSize: 32 * DefaultStyle.dp - text: qsTr("Aide") - iconSource: AppIcons.question - } - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 1 * DefaultStyle.dp - color: DefaultStyle.main2_400 - } - IconLabelButton { - Layout.preferredHeight: 32 * DefaultStyle.dp - iconSize: 32 * DefaultStyle.dp - text: qsTr("Ajouter un compte") - iconSource: AppIcons.plusCircle - onClicked: mainItem.addAccountRequest() - } - } + Layout.fillHeight: true + color: DefaultStyle.grey_0 } } - StackLayout { - // width: parent.width - // height: parent.height - id: mainStackLayout - currentIndex: tabbar.currentIndex - - CallPage { - id: callPage + } + + RowLayout { + anchors.fill: parent + // spacing: 30 + anchors.topMargin: 18 * DefaultStyle.dp + VerticalTabBar { + id: tabbar + Layout.fillHeight: true + model: [ + {icon: AppIcons.phone, selectedIcon: AppIcons.phoneSelected, label: qsTr("Appels")}, + {icon: AppIcons.adressBook, selectedIcon: AppIcons.adressBookSelected, label: qsTr("Contacts")}, + {icon: AppIcons.chatTeardropText, selectedIcon: AppIcons.chatTeardropTextSelected, label: qsTr("Conversations")}, + {icon: AppIcons.usersThree, selectedIcon: AppIcons.usersThreeSelected, label: qsTr("Réunions")} + ] + } + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + RowLayout { + id: topRow + Layout.leftMargin: 25 * DefaultStyle.dp + Layout.rightMargin: 41 * DefaultStyle.dp + SearchBar { + id: magicSearchBar + Layout.fillWidth: true + placeholderText: qsTr("Rechercher un contact, appeler ou envoyer un message...") + onTextChanged: { + if (text.length != 0) listPopup.open() + else listPopup.close() + } + Popup { + id: listPopup + width: magicSearchList.width + listPopup.rightPadding + listPopup.leftPadding + height: Math.min(magicSearchList.contentHeight, 300 * DefaultStyle.dp + topPadding + bottomPadding) + y: magicSearchBar.height + closePolicy: Popup.NoAutoClose + topPadding: 10 * DefaultStyle.dp + bottomPadding: 10 * DefaultStyle.dp + rightPadding: 10 * DefaultStyle.dp + leftPadding: 10 * DefaultStyle.dp + background: Item { + anchors.fill: parent + Rectangle { + id: popupBg + radius: 15 * DefaultStyle.dp + anchors.fill: parent + // width: magicSearchList.width + listPopup.rightPadding + listPopup.leftPadding + } + MultiEffect { + source: popupBg + anchors.fill: popupBg + shadowEnabled: true + shadowBlur: 1 + shadowColor: DefaultStyle.grey_1000 + shadowOpacity: 0.1 + } + } + Control.ScrollBar { + id: scrollbar + height: parent.height + anchors.right: parent.right + } + ContactsList { + id: magicSearchList + visible: magicSearchBar.text.length != 0 + height: Math.min(contentHeight, listPopup.height) + width: magicSearchBar.width + initialHeadersVisible: false + contactMenuVisible: false + model: MagicSearchProxy { + searchText: magicSearchBar.text.length === 0 ? "*" : magicSearchBar.text + aggregationFlag: LinphoneEnums.MagicSearchAggregation.Friend + } + Control.ScrollBar.vertical: scrollbar + header: ColumnLayout { + width: magicSearchList.width + RowLayout { + Layout.fillWidth: true + spacing: 10 * DefaultStyle.dp + Avatar { + Layout.preferredWidth: 45 * DefaultStyle.dp + Layout.preferredHeight: 45 * DefaultStyle.dp + address: sipAddr.text + } + ColumnLayout { + Text { + text: magicSearchBar.text + font { + pixelSize: 14 * DefaultStyle.dp + weight: 700 * DefaultStyle.dp + } + } + Text { + id: sipAddr + text: UtilsCpp.generateLinphoneSipAddress(magicSearchBar.text) + } + } + Item { + Layout.fillWidth: true + } + Button { + background: Item{} + Layout.preferredWidth: 24 * DefaultStyle.dp + Layout.preferredHeight: 24 * DefaultStyle.dp + contentItem: Image { + width: 24 * DefaultStyle.dp + height: 24 * DefaultStyle.dp + source: AppIcons.phone + } + onClicked: { + var callObj = UtilsCpp.createCall(sipAddr) + } + } + } + Button { + Layout.fillWidth: true + color: DefaultStyle.main2_200 + pressedColor: DefaultStyle.main2_400 + background: Rectangle { + anchors.fill: parent + color: DefaultStyle.main2_200 + } + contentItem: RowLayout { + spacing: 10 * DefaultStyle.dp + Image { + source: AppIcons.userPlus + } + Text { + text: qsTr("Ajouter ce contact") + font { + pixelSize: 14 * DefaultStyle.dp + weight: 700 * DefaultStyle.dp + capitalization: Font.AllUppercase + } + } + Item {Layout.fillWidth: true} + } + onClicked: { + var currentItem = mainStackLayout.children[mainStackLayout.currentIndex] + listPopup.close() + currentItem.createContact(magicSearchBar.text, sipAddr.text) + } + } + } + delegateButtons: Button { + Layout.preferredWidth: 24 * DefaultStyle.dp + Layout.preferredHeight: 24 * DefaultStyle.dp + background: Item{} + contentItem: Image { + anchors.fill: parent + width: 24 * DefaultStyle.dp + height: 24 * DefaultStyle.dp + source: AppIcons.phone + } + } + } + } + } + PopupButton { + id: avatarButton + AccountProxy{ + id: accountProxy + //property bool haveAvatar: defaultAccount && defaultAccount.core.pictureUri || false + } + background.visible: false + Layout.preferredWidth: 54 * DefaultStyle.dp + Layout.preferredHeight: width + contentItem: Avatar { + id: avatar + height: avatarButton.height + width: avatarButton.width + account: accountProxy.defaultAccount + } + popup.x: width - popup.width + popup.padding: 0 + popup.contentItem: ColumnLayout { + Accounts { + id: accounts + onAddAccountRequest: mainItem.addAccountRequest() + } + } + } + PopupButton { + id: settingsButton + Layout.preferredWidth: 24 * DefaultStyle.dp + Layout.preferredHeight: 24 * DefaultStyle.dp + popup.x: width - popup.width + popup.width: 271 * DefaultStyle.dp + popup.contentItem: ColumnLayout { + spacing: 20 * DefaultStyle.dp + IconLabelButton { + Layout.preferredHeight: 32 * DefaultStyle.dp + iconSize: 32 * DefaultStyle.dp + text: qsTr("Mon compte") + iconSource: AppIcons.manageProfile + onClicked: console.log("TODO : manage profile") + } + IconLabelButton { + Layout.preferredHeight: 32 * DefaultStyle.dp + iconSize: 32 * DefaultStyle.dp + text: qsTr("Paramètres") + iconSource: AppIcons.settings + } + IconLabelButton { + Layout.preferredHeight: 32 * DefaultStyle.dp + iconSize: 32 * DefaultStyle.dp + text: qsTr("Enregistrements") + iconSource: AppIcons.micro + } + IconLabelButton { + Layout.preferredHeight: 32 * DefaultStyle.dp + iconSize: 32 * DefaultStyle.dp + text: qsTr("Aide") + iconSource: AppIcons.question + } + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 * DefaultStyle.dp + color: DefaultStyle.main2_400 + } + IconLabelButton { + Layout.preferredHeight: 32 * DefaultStyle.dp + iconSize: 32 * DefaultStyle.dp + text: qsTr("Ajouter un compte") + iconSource: AppIcons.plusCircle + onClicked: mainItem.addAccountRequest() + } + } + } + } + StackLayout { + // width: parent.width + // height: parent.height + id: mainStackLayout + currentIndex: tabbar.currentIndex + + CallPage { + id: callPage + } + ContactPage{} + //ConversationPage{} + //MeetingPage{} } - ContactPage{} - //ConversationPage{} - //MeetingPage{} } } } diff --git a/Linphone/view/App/Main.qml b/Linphone/view/App/Main.qml index a1171ea50..71e3259d8 100644 --- a/Linphone/view/App/Main.qml +++ b/Linphone/view/App/Main.qml @@ -14,7 +14,7 @@ Window { // TODO : handle this bool when security mode is implemented property bool firstConnection: true - + color: "transparent" function goToNewCall() { mainWindowStackView.replace(mainPage, StackView.Immediate) mainWindowStackView.currentItem.goToNewCall() diff --git a/Linphone/view/CMakeLists.txt b/Linphone/view/CMakeLists.txt index de7cd270f..61e676e82 100644 --- a/Linphone/view/CMakeLists.txt +++ b/Linphone/view/CMakeLists.txt @@ -67,6 +67,7 @@ list(APPEND _LINPHONEAPP_QML_FILES view/Page/Main/CallPage.qml view/Page/Main/ContactPage.qml + view/Tool/utils.js # Prototypes view/Prototype/PhoneNumberPrototype.qml view/Prototype/AccountsPrototype.qml diff --git a/Linphone/view/Item/VerticalTabBar.qml b/Linphone/view/Item/VerticalTabBar.qml index af3f7e0bc..2f40a76ec 100644 --- a/Linphone/view/Item/VerticalTabBar.qml +++ b/Linphone/view/Item/VerticalTabBar.qml @@ -11,6 +11,7 @@ Control.TabBar { topPadding: 20 * DefaultStyle.dp property var model + readonly property alias cornerRadius: bottomLeftCorner.radius contentItem: ListView { model: mainItem.contentModel @@ -32,6 +33,7 @@ Control.TabBar { id: background anchors.fill: parent Rectangle { + id: bottomLeftCorner anchors.fill: parent color: DefaultStyle.main1_500_main radius: 25 * DefaultStyle.dp diff --git a/Linphone/view/Tool/utils.js b/Linphone/view/Tool/utils.js new file mode 100644 index 000000000..520f54ca5 --- /dev/null +++ b/Linphone/view/Tool/utils.js @@ -0,0 +1,734 @@ +/* + * Copyright (c) 2010-2024 Belledonne Communications SARL. + * + * This file is part of linphone-desktop + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +// ============================================================================= +// Contains many common helpers. +// ============================================================================= + +.pragma library + +.import QtQuick as QtQuick +.import Linphone as Linphone + +// ============================================================================= +// Constants. +// ============================================================================= + + +var SCHEME_REGEX = new RegExp('^[^:]+:') + +// ============================================================================= +// QML helpers. +// ============================================================================= + +function buildCommonDialogUri (component) { + return 'qrc:/ui/modules/Common/Dialog/' + component + '.qml' +} +function buildCommonUri (component, subComponent) { + return 'qrc:/ui/modules/Common/'+subComponent+'/' + component + '.qml' +} + +function buildLinphoneDialogUri (component) { + return 'qrc:/ui/modules/Linphone/Dialog/' + component + '.qml' +} + +function buildAppDialogUri (component) { + return 'qrc:/ui/views/App/Dialog/' + component + '.qml' +} + +// ----------------------------------------------------------------------------- + +// Destroy timeout. +function clearTimeout (timer) { + timer.stop() // NECESSARY. + timer.destroy() +} + +// ----------------------------------------------------------------------------- + +function createObject (source, parent, options) { + if (options && options.isString) { + var object = Qt.createQmlObject(source, parent) + + var properties = options && options.properties + if (properties) { + for (var key in properties) { + object[key] = properties[key] + } + } + + return object + } + + var component = Qt.createComponent(source) + if (component.status !== QtQuick.Component.Ready) { + console.debug('Component not ready.') + if (component.status === QtQuick.Component.Error) { + console.debug('Error: ' + component.errorString()) + } + return // Error. + } + + var object = component.createObject(parent, (options && options.properties) || {}) + if (!object) { + console.debug('Error: unable to create dynamic object.') + } + + return object +} + +// ----------------------------------------------------------------------------- + +function getSystemPathFromUri (uri) { + var str = uri.toString() + if (startsWith(str, 'file://')) { + str = str.substring(7) + + // Absolute path. + if (str.charAt(0) === '/') { + return runOnWindows() ? str.substring(1) : str + } + } + + return str +} + +function getUriFromSystemPath (path) { + if (path.startsWith('file://')) { + return path + } + + if (runOnWindows()) { + return 'file://' + (/^\w:/.exec(path) ? '/' : '') + path + } + + return 'file://' + path +} + +// ----------------------------------------------------------------------------- + +// Returns the top (root) parent of one object. +function getTopParent (object, useFakeParent) { + function _getTopParent (object, useFakeParent) { + return (useFakeParent && object.$parent) || object.parent + } + + var parent = _getTopParent(object, useFakeParent) + var p + while ((p = _getTopParent(parent, useFakeParent)) != null) { + parent = p + } + + return parent +} + +// ----------------------------------------------------------------------------- + +// Load by default a window in the ui/views folder. +// If options.isString is equals to true, a marshalling component can +// be used. +// +// Supported options: isString, exitHandler, properties. +// +// If exitHandler is used, window must implement exitStatus signal. +function openWindow (window, parent, options, fullscreen) { + var object = createObject(window, parent, options) + + object.closing.connect(object.destroy.bind(object)) + + if (options && options.exitHandler) { + object.exitStatus.connect( + // Bind to access parent properties. + options.exitHandler.bind(parent) + ) + } + if( runOnWindows()){ + object.show() // Needed for Windows : Show the window in all case. Allow to graphically locate the window before going to fullscreen. + if(fullscreen) + object.showFullScreen()// Should be equivalent to changing visibility + }else if(fullscreen) + object.showFullScreen()// Should be equivalent to changing visibility + else + object.show() + return object +} + +// ----------------------------------------------------------------------------- + +function resolveImageUri (name) { + return name + //? 'image://internal/' + removeScheme(Qt.resolvedUrl('/assets/images/' + name + '.svg')) + ? 'image://internal/' + name + : '' +} + +// ----------------------------------------------------------------------------- + + +function runOnWindows () { + var os = Qt.platform.os + return os === 'windows' || os === 'winrt' +} + +// ----------------------------------------------------------------------------- + +// Test if a point is in a item. +// +// `source` is the item that generated the point. +// `target` is the item to test. +// `point` is the point to test. +function pointIsInItem (source, target, point) { + point = source.mapToItem(target.parent, point.x, point.y) + + return ( + point.x >= target.x && + point.y >= target.y && + point.x < target.x + target.width && + point.y < target.y + target.height + ) +} + +// ----------------------------------------------------------------------------- + +// Test the type of a qml object. +// Warning: this function is probably not portable +// on new versions of Qt. +// +// So, if you want to use it on a specific `className`, please to add +// a test in `test_qmlTypeof_data` of `utils.spec.qml`. +function qmlTypeof (object, className) { + var str = object.toString() + + return ( + str.indexOf(className + '(') == 0 || + str.indexOf(className + '_QML') == 0 + ) +} + +// ----------------------------------------------------------------------------- + +function removeScheme (url) { + return url.toString().replace(SCHEME_REGEX, '') +} + +// ----------------------------------------------------------------------------- + +// A copy of `Window.setTimeout` from js. +// delay is in milliseconds. +function setTimeout (parent, delay, cb) { + var timer = new (function (parent) { + return Qt.createQmlObject('import QtQml 2.2; Timer { }', parent) + })(parent) + + timer.interval = delay + timer.repeat = false + timer.triggered.connect(cb) + timer.start() + + return timer +} + +// ============================================================================= +// GENERIC. +// ============================================================================= + +function _computeOptimizedCb (func, context) { + return context + ? (function () { + return func.apply(context, arguments) + }) : func +} + +function _indexFinder (array, cb, context) { + var length = array.length + + for (var i = 0; i < length; i++) { + if (cb( (array.getAt ? array.getAt(i) : array[i]), i, array)) { + return i + } + } + + return -1 +} + +function _keyFinder (obj, cb, context) { + var keys = Object.keys(obj) + var length = keys.length + + for (var i = 0; i < length; i++) { + var key = keys[i] + if (cb(obj[key], key, obj)) { + return key + } + } +} + +// ----------------------------------------------------------------------------- + +// Basic assert function. +function assert (condition, message) { + if (!condition) { + throw new Error('Assert: ' + message) + } +} + +// ----------------------------------------------------------------------------- + +function basename (str) { + if (!str) { + return '' + } + + if (runOnWindows()) { + str = str.replace(/\\/g, '/') + } + + var str2 = str + var length = str2.length - 1 + + if (str2[length] === '/') { + str2 = str2.substring(0, length) + } + + return str2.slice(str2.lastIndexOf('/') + 1) +} + +// ----------------------------------------------------------------------------- + +function capitalizeFirstLetter (str) { + return str.charAt(0).toUpperCase() + str.slice(1) +} + +// ----------------------------------------------------------------------------- + +function dirname (str) { + if (!str) { + return '' + } + + if (runOnWindows()) { + str = str.replace(/\\/g, '/') + } + + var str2 = str + var length = str2.length - 1 + + if (str2[length] === '/') { + str2 = str2.substring(0, length) + } + + return str2.slice(0, str2.lastIndexOf('/') + 1) +} + +// ----------------------------------------------------------------------------- + +function extractProperties (obj, pattern) { + if (!pattern) { + return {} + } + + var obj2 = {} + pattern.forEach(function (property) { + obj2[property] = obj[property] + }) + + return obj2 +} + +// ----------------------------------------------------------------------------- + +// Returns an array from an `object` or `array` argument. +function ensureArray (obj) { + if (isArray(obj)) { + return obj + } + + var keys = Object.keys(obj) + var length = keys.length + var values = Array(length) + + for (var i = 0; i < length; i++) { + values[i] = obj[keys[i]] + } + + return values +} + +// ----------------------------------------------------------------------------- + +function escapeQuotes (str) { + return str != null + ? str.replace(/([^'\\]*(?:\\.[^'\\]*)*)'/g, '$1\\\'') + : '' +} + +function decode(str){ + return decodeURIComponent(escape(str.replace(/%([0-9A-Fa-f]{2})/g, function() { + return String.fromCharCode(parseInt(arguments[1], 16)); + }))); +} +// ----------------------------------------------------------------------------- + +// Get the first matching value in a array or object. +// The matching value is obtained if `cb` returns true. +function find (obj, cb, context) { + cb = _computeOptimizedCb(cb, context) + + var finder = isArray(obj) ? _indexFinder : _keyFinder + var key = finder(obj, cb, context) + + return key != null && key !== -1 ? obj[key] : null +} + +// ----------------------------------------------------------------------------- + +function findIndex (array, cb, context) { + cb = _computeOptimizedCb(cb, context) + + var key = _indexFinder(array, cb, context) + return key != null ? key : -1 +} + +// ----------------------------------------------------------------------------- +function formatElapsedTime (seconds) { + seconds = parseInt(seconds, 10) +//s, m, h, d, W, M, Y +//1, 60, 3600, 86400, 604800, 2592000, 31104000 + var y = Math.floor(seconds / 31104000) + if(y > 0) + return y+ ' years' + var M = Math.floor(seconds / 2592000) + if(M > 0) + return M+' months' + var w = Math.floor(seconds / 604800) + if(w>0) + return w+' week'; + var d = Math.floor(seconds / 86400) + if(d>0) + return d+' days' + var h = Math.floor(seconds / 3600) + var m = Math.floor((seconds - h * 3600) / 60) + var s = seconds - h * 3600 - m * 60 + + if (h < 10 && h > 0) { + h = '0' + h + } + + if (m < 10) { + m = '0' + m + } + + if (s < 10) { + s = '0' + s + } + + return (h === 0 ? '' : h + ':') + m + ':' + s +} + +function formatDuration (seconds) { + seconds = parseInt(seconds, 10) +//s, m, h, d, W, M, Y +//1, 60, 3600, 86400, 604800, 2592000, 31104000 + var y = Math.floor(seconds / 31104000) + if(y > 0) + //: '%1 year' + return qsTr('formatYears', '', y).arg(y) + var M = Math.floor(seconds / 2592000) + if(M > 0) + //: '%1 month' + return qsTr('formatMonths', '', M).arg(M) + var w = Math.floor(seconds / 604800) + if(w>0) + //: '%1 week' + return qsTr('formatWeeks', '', w).arg(w) + var d = Math.floor(seconds / 86400) + if(d>0) + //: '%1 day' + return qsTr('formatDays', '', d).arg(d) + var h = Math.floor(seconds / 3600) + var m = Math.floor((seconds - h * 3600) / 60) + var s = seconds - h * 3600 - m * 60 + + //: '%1 hour' + return (h > 0 ? qsTr('formatHours', '', h).arg(h): '') + //: '%1 minute' + + (m > 0 ? (h > 0 ? ', ' : '') +qsTr('formatMinutes', '', m).arg(m): '') + //: '%1 second' + + (s > 0 ? (h> 0 || m > 0 ? ', ' : '') +qsTr('formatSeconds', '', s).arg(s): '') +} + +function buildDate(date, time){ + var dateTime = new Date() + dateTime.setFullYear(date.getFullYear(), date.getMonth(), date.getDate()) + dateTime.setHours(time.getHours()) + dateTime.setMinutes(time.getMinutes()) + dateTime.setSeconds(time.getSeconds()) + return dateTime +} + +function equalDate(date1, date2){ + return date1.getFullYear() == date2.getFullYear() && date1.getMonth() == date2.getMonth() && date1.getDate() == date2.getDate() +} + +function fromUTC(date){ + return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), + date.getUTCDate(), date.getUTCHours(), + date.getUTCMinutes(), date.getUTCSeconds())); +} +// ----------------------------------------------------------------------------- + +function formatSize (size) { + var units = ['KB', 'MB', 'GB', 'TB'] + var unit = 'B' + + if (size == null) { + size = 0 + } + + var length = units.length + for (var i = 0; size >= 1024 && i < length; i++) { + unit = units[i] + size /= 1024 + } + + return parseFloat(size.toFixed(2)) + unit +} + +// ----------------------------------------------------------------------------- + +// Generate a random number in the [min, max[ interval. +// Uniform distrib. +function genRandomNumber (min, max) { + return Math.random() * (max - min) + min +} + +function genRandomColor(){ + return '#'+ Math.floor(Math.random()*255).toString(16) + +Math.floor(Math.random()*255).toString(16) + +Math.floor(Math.random()*255).toString(16) +} + +// ----------------------------------------------------------------------------- + +// Generate a random number between a set of intervals. +// The `intervals` param must be orderer like this: +// `[ [ 1, 4 ], [ 8, 16 ], [ 22, 25 ] ]` +function genRandomNumberBetweenIntervals (intervals) { + if (intervals.length === 1) { + return genRandomNumber(intervals[0][0], intervals[0][1]) + } + + // Compute the intervals size. + var size = 0 + intervals.forEach(function (interval) { + size += interval[1] - interval[0] + }) + + // Generate a value in the interval: `[0, size[` + var n = genRandomNumber(0, size) + + // Map the value in the right interval. + n += intervals[0][0] + for (var i = 0; i < intervals.length - 1; i++) { + if (n < intervals[i][1]) { + break + } + + n += intervals[i + 1][0] - intervals[i][1] + } + + return n +} + +// ----------------------------------------------------------------------------- + +// Returns the extension of a filename. +function getExtension (str) { + var index = str.lastIndexOf('.') + + if (index === -1) { + return '' + } + + return str.slice(index + 1) +} + +// ----------------------------------------------------------------------------- + +// Test if a value is included in an array or object. +function includes (obj, value, startIndex) { + obj = ensureArray(obj) + if (startIndex == null) { + startIndex = 0 + } + var length = obj.length + + for (var i = startIndex; i < length; i++) { + if ( + value == obj[i] || + // Check `NaN`. + (value !== value && obj[i] !== obj[i]) + ) { + return true + } + } + return false +} + +// ----------------------------------------------------------------------------- + +function isArray (array) { + return (array instanceof Array) +} + +// ----------------------------------------------------------------------------- + +function isFunction (func) { + return typeof func === 'function' +} + +// ----------------------------------------------------------------------------- + +function isInteger (integer) { + return integer === parseInt(integer, 10) +} + +// ----------------------------------------------------------------------------- + +function isObject (object) { + return object !== null && typeof object === 'object' +} + +// ----------------------------------------------------------------------------- + +function isString (string) { + return typeof string === 'string' || string instanceof String +} + +// ----------------------------------------------------------------------------- + +// Convert a snake_case string to a lowerCamelCase string. +function snakeToCamel (s) { + return s.replace(/(\_\w)/g, function (matches) { + return matches[1].toUpperCase() + }) +} + +// ----------------------------------------------------------------------------- + +// Test if a string starts by a given string. +function startsWith (str, searchStr) { + if (searchStr == null) { + searchStr = '' + } + + return str.slice(0, searchStr.length) === searchStr +} + +// ----------------------------------------------------------------------------- + +// Invoke a `cb` function with each value of the interval: `[0, n[`. +// Return a mapped array created with the returned values of `cb`. +function times (n, cb, context) { + var arr = Array(Math.max(0, n)) + cb = _computeOptimizedCb(cb, context, 1) + + for (var i = 0; i < n; i++) { + arr[i] = cb(i) + } + + return arr +} + +// ----------------------------------------------------------------------------- + +function unscapeHtml (str) { + return str.replace(/&/g, '&') + .replace(//g, '\u2063>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + +// ----------------------------------------------------------------------------- + +function write (fileName, text) { + // TODO: Deal with async. + var request = new XMLHttpRequest() + request.open('PUT', getUriFromSystemPath(fileName), false) + request.send(text) +} + +function computeAvatarSize (container, maxSize, ratio) { + var height = container.height * ratio + var width = container.width * ratio + var size = height < maxSize && height > 0 ? height : maxSize + return size < width ? size : width +} + +// ----------------------------------------------------------------------------- + +function openCodecOnlineInstallerDialog (window, codecInfo, cb) { + var VideoCodecsModel = Linphone.VideoCodecsModel + window.attachVirtualWindow(buildCommonDialogUri('ConfirmDialog'), { + descriptionText: qsTr('downloadCodecDescription') + .replace('%1', codecInfo.mime) + .replace('%2', codecInfo.encoderDescription) + }, function (status) { + if (status) { + window.attachVirtualWindow(buildLinphoneDialogUri('OnlineInstallerDialog'), { + downloadUrl: codecInfo.downloadUrl, + extract: true, + installFolder: VideoCodecsModel.codecsFolder, + installName: codecInfo.installName, + mime: codecInfo.mime, + checksum: codecInfo.checksum + }, function (status) { + if (status) { + VideoCodecsModel.reload() + } + if (cb) { + cb(window) + } + }) + } + else if (cb) { + cb(window) + } + }) +} + +function printObject(o) { + var out = ''; + for (var p in o) { + out += p + ': ' + o[p] + '\n'; + } + if(!o) + return 'Empty' + else + return out; +} + +function infoDialog(window, message) { + window.attachVirtualWindow(buildCommonDialogUri('ConfirmDialog'), { + buttonTexts : ['',qsTr('okButton')], + descriptionText: message, + showButtonOnly: 1 + }, function (status) {}) +}