Commit 90c35eb2 authored by Ronan Abhamon's avatar Ronan Abhamon

feat(app): calls in progress

parent af60c28e
<?xml version="1.0" encoding="UTF-8"?>
<svg width="40px" height="40px" viewBox="0 0 40 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 41 (35326) - http://www.bohemiancoding.com/sketch -->
<title>call_in_sign</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="call_in_sign">
<polygon fill="#DC4100" points="0 0 40 0 0 40"></polygon>
<path d="M11.243716,8.71501414 L11.1871835,8.71501414 C8.47215269,8.71501414 5.21235367,10.3895588 4.11804501,12.8152126 L4.51732144,15.956733 L8.01512006,15.956733 L8.2626641,12.830113 L14.1683578,12.830113 L14.41529,15.956733 L17.9130886,15.956733 L18.3123651,12.8149535 C17.2174446,10.3886518 13.9583798,8.71501414 11.243716,8.71501414 Z" fill="#FFFFFF" transform="translate(11.215205, 12.335874) rotate(-135.000000) translate(-11.215205, -12.335874) "></path>
</g>
</g>
</svg>
...@@ -40,15 +40,15 @@ ...@@ -40,15 +40,15 @@
</message> </message>
<message> <message>
<source>resumeCall</source> <source>resumeCall</source>
<translation type="unfinished"></translation> <translation>RESUME CALL</translation>
</message> </message>
<message> <message>
<source>transferCall</source> <source>transferCall</source>
<translation type="unfinished"></translation> <translation>TRANSFER CALL</translation>
</message> </message>
<message> <message>
<source>pauseCall</source> <source>pauseCall</source>
<translation type="unfinished"></translation> <translation>PAUSE CALL</translation>
</message> </message>
</context> </context>
<context> <context>
......
...@@ -32,15 +32,15 @@ ...@@ -32,15 +32,15 @@
</message> </message>
<message> <message>
<source>resumeCall</source> <source>resumeCall</source>
<translation type="unfinished"></translation> <translation>REPRENDRE APPEL</translation>
</message> </message>
<message> <message>
<source>transferCall</source> <source>transferCall</source>
<translation type="unfinished"></translation> <translation>TRANSFERER APPEL</translation>
</message> </message>
<message> <message>
<source>pauseCall</source> <source>pauseCall</source>
<translation type="unfinished"></translation> <translation>PAUSE</translation>
</message> </message>
</context> </context>
<context> <context>
......
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
<file>assets/images/call_quality_2.svg</file> <file>assets/images/call_quality_2.svg</file>
<file>assets/images/call_quality_3.svg</file> <file>assets/images/call_quality_3.svg</file>
<file>assets/images/call_sign_connected.svg</file> <file>assets/images/call_sign_connected.svg</file>
<file>assets/images/call_sign_ended.svg</file>
<file>assets/images/call_sign_incoming.svg</file> <file>assets/images/call_sign_incoming.svg</file>
<file>assets/images/call_sign_outgoing.svg</file> <file>assets/images/call_sign_outgoing.svg</file>
<file>assets/images/call_sign_paused.svg</file> <file>assets/images/call_sign_paused.svg</file>
......
...@@ -62,6 +62,14 @@ App::App (int &argc, char **argv) : QApplication(argc, argv) { ...@@ -62,6 +62,14 @@ App::App (int &argc, char **argv) : QApplication(argc, argv) {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
QQuickWindow *App::getCallsWindow () const {
QQuickWindow *window = m_engine.rootContext()->contextProperty("CallsWindow").value<QQuickWindow *>();
if (!window)
qFatal("Unable to get calls window.");
return window;
}
bool App::hasFocus () const { bool App::hasFocus () const {
QQmlApplicationEngine &engine = const_cast<QQmlApplicationEngine &>(m_engine); QQmlApplicationEngine &engine = const_cast<QQmlApplicationEngine &>(m_engine);
const QQuickWindow *root = qobject_cast<QQuickWindow *>(engine.rootObjects().at(0)); const QQuickWindow *root = qobject_cast<QQuickWindow *>(engine.rootObjects().at(0));
...@@ -182,7 +190,6 @@ void App::addContextProperties () { ...@@ -182,7 +190,6 @@ void App::addContextProperties () {
qInfo() << "Adding context properties..."; qInfo() << "Adding context properties...";
QQmlContext *context = m_engine.rootContext(); QQmlContext *context = m_engine.rootContext();
// TODO: Avoid context properties. Use qmlRegister...
QQmlComponent component(&m_engine, QUrl(QML_VIEW_CALL_WINDOW)); QQmlComponent component(&m_engine, QUrl(QML_VIEW_CALL_WINDOW));
if (component.isError()) { if (component.isError()) {
qWarning() << component.errors(); qWarning() << component.errors();
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
#include <QApplication> #include <QApplication>
#include <QQmlApplicationEngine> #include <QQmlApplicationEngine>
#include <QQmlFileSelector> #include <QQmlFileSelector>
#include <QQuickWindow>
#include <QSystemTrayIcon> #include <QSystemTrayIcon>
#include "../components/notifier/Notifier.hpp" #include "../components/notifier/Notifier.hpp"
...@@ -25,6 +26,8 @@ public: ...@@ -25,6 +26,8 @@ public:
return m_notifier; return m_notifier;
} }
QQuickWindow *getCallsWindow () const;
bool hasFocus () const; bool hasFocus () const;
Q_INVOKABLE QString locale () const { Q_INVOKABLE QString locale () const {
......
#include <QDebug> #include <QDebug>
#include <QTimer>
#include "../../app/App.hpp" #include "../../app/App.hpp"
#include "../core/CoreManager.hpp" #include "../core/CoreManager.hpp"
#include "CallsListModel.hpp" #include "CallsListModel.hpp"
/* Delay before removing call in ms. */
#define DELAY_BEFORE_REMOVE_CALL 3000
using namespace std; using namespace std;
// ============================================================================= // =============================================================================
...@@ -16,17 +20,16 @@ CallsListModel::CallsListModel (QObject *parent) : QAbstractListModel(parent) { ...@@ -16,17 +20,16 @@ CallsListModel::CallsListModel (QObject *parent) : QAbstractListModel(parent) {
this, [this](const shared_ptr<linphone::Call> &linphone_call, linphone::CallState state) { this, [this](const shared_ptr<linphone::Call> &linphone_call, linphone::CallState state) {
switch (state) { switch (state) {
case linphone::CallStateIncomingReceived: case linphone::CallStateIncomingReceived:
addCall(linphone_call);
break;
case linphone::CallStateOutgoingInit: case linphone::CallStateOutgoingInit:
addCall(linphone_call); addCall(linphone_call);
break; break;
case linphone::CallStateEnd: case linphone::CallStateEnd:
case linphone::CallStateError: case linphone::CallStateError:
removeCall(linphone_call); removeCall(linphone_call);
break; break;
default:
default:
break; break;
} }
} }
...@@ -80,11 +83,13 @@ bool CallsListModel::removeRows (int row, int count, const QModelIndex &parent) ...@@ -80,11 +83,13 @@ bool CallsListModel::removeRows (int row, int count, const QModelIndex &parent)
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
void CallsListModel::addCall (const shared_ptr<linphone::Call> &linphone_call) { void CallsListModel::addCall (const shared_ptr<linphone::Call> &linphone_call) {
App::getInstance()->getCallsWindow()->show();
CallModel *call = new CallModel(linphone_call); CallModel *call = new CallModel(linphone_call);
App::getInstance()->getEngine()->setObjectOwnership(call, QQmlEngine::CppOwnership); App::getInstance()->getEngine()->setObjectOwnership(call, QQmlEngine::CppOwnership);
linphone_call->setData("call-model", *call); linphone_call->setData("call-model", *call);
int row = rowCount(); int row = m_list.count();
beginInsertRows(QModelIndex(), row, row); beginInsertRows(QModelIndex(), row, row);
m_list << call; m_list << call;
...@@ -92,12 +97,20 @@ void CallsListModel::addCall (const shared_ptr<linphone::Call> &linphone_call) { ...@@ -92,12 +97,20 @@ void CallsListModel::addCall (const shared_ptr<linphone::Call> &linphone_call) {
} }
void CallsListModel::removeCall (const shared_ptr<linphone::Call> &linphone_call) { void CallsListModel::removeCall (const shared_ptr<linphone::Call> &linphone_call) {
CallModel &call = linphone_call->getData<CallModel>("call-model"); CallModel *call = &linphone_call->getData<CallModel>("call-model");
linphone_call->unsetData("call-model"); linphone_call->unsetData("call-model");
qInfo() << "Removing call:" << &call; // TODO: It will be (maybe) necessary to use a single scheduled function in the future.
QTimer::singleShot(
DELAY_BEFORE_REMOVE_CALL, this, [this, call]() {
qInfo() << "Removing call:" << call;
int index = m_list.indexOf(call);
if (index == -1 || !removeRow(index))
qWarning() << "Unable to remove call:" << call;
int index = m_list.indexOf(&call); if (m_list.empty())
if (index == -1 || !removeRow(index)) App::getInstance()->getCallsWindow()->close();
qWarning() << "Unable to remove call:" << &call; }
);
} }
...@@ -520,7 +520,7 @@ void ChatModel::removeEntry (ChatEntryData &pair) { ...@@ -520,7 +520,7 @@ void ChatModel::removeEntry (ChatEntryData &pair) {
} }
void ChatModel::insertMessageAtEnd (const shared_ptr<linphone::ChatMessage> &message) { void ChatModel::insertMessageAtEnd (const shared_ptr<linphone::ChatMessage> &message) {
int row = rowCount(); int row = m_entries.count();
beginInsertRows(QModelIndex(), row, row); beginInsertRows(QModelIndex(), row, row);
......
...@@ -92,7 +92,7 @@ ContactModel *ContactsListModel::addContact (VcardModel *vcard) { ...@@ -92,7 +92,7 @@ ContactModel *ContactsListModel::addContact (VcardModel *vcard) {
return nullptr; return nullptr;
} }
int row = rowCount(); int row = m_list.count();
beginInsertRows(QModelIndex(), row, row); beginInsertRows(QModelIndex(), row, row);
addContact(contact); addContact(contact);
......
...@@ -6,75 +6,75 @@ import Common.Styles 1.0 ...@@ -6,75 +6,75 @@ import Common.Styles 1.0
// ============================================================================= // =============================================================================
BusyIndicator { BusyIndicator {
id: busyIndicator id: busyIndicator
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
readonly property int _rotation: 360 readonly property int _rotation: 360
readonly property int _size: width < height ? width : height readonly property int _size: width < height ? width : height
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
contentItem: Item { contentItem: Item {
x: parent.width / 2 - width / 2 x: parent.width / 2 - width / 2
y: parent.height / 2 - height / 2 y: parent.height / 2 - height / 2
height: _size height: _size
width: _size width: _size
Item { Item {
id: item id: item
height: parent.height height: parent.height
width: parent.width width: parent.width
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// Animation. // Animation.
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
RotationAnimator { RotationAnimator {
duration: BusyIndicatorStyle.duration duration: BusyIndicatorStyle.duration
loops: Animation.Infinite loops: Animation.Infinite
running: busyIndicator.visible && busyIndicator.running running: busyIndicator.visible && busyIndicator.running
target: item target: item
from: 0 from: 0
to: busyIndicator._rotation to: busyIndicator._rotation
} }
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// Items to draw. // Items to draw.
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
Repeater { Repeater {
id: repeater id: repeater
model: BusyIndicatorStyle.nSpheres model: BusyIndicatorStyle.nSpheres
Rectangle { Rectangle {
x: item.width / 2 - width / 2 x: item.width / 2 - width / 2
y: item.height / 2 - height / 2 y: item.height / 2 - height / 2
height: item.height / 3 height: item.height / 3
width: item.width / 3 width: item.width / 3
color: BusyIndicatorStyle.color color: BusyIndicatorStyle.color
radius: (width > height ? width : height) / 2 radius: (width > height ? width : height) / 2
transform: [ transform: [
Translate { Translate {
y: busyIndicator._size / 2 y: busyIndicator._size / 2
}, },
Rotation { Rotation {
angle: index / repeater.count * busyIndicator._rotation angle: index / repeater.count * busyIndicator._rotation
origin { origin {
x: width / 2 x: width / 2
y: height / 2 y: height / 2
} }
} }
] ]
} }
} }
} }
} }
} }
...@@ -6,7 +6,7 @@ import Common 1.0 ...@@ -6,7 +6,7 @@ import Common 1.0
// ============================================================================= // =============================================================================
QtObject { QtObject {
property color color: Colors.i property color color: Colors.i
property int duration: 1250 property int duration: 1250
property int nSpheres: 6 property int nSpheres: 6
} }
...@@ -7,172 +7,207 @@ import Linphone.Styles 1.0 ...@@ -7,172 +7,207 @@ import Linphone.Styles 1.0
// ============================================================================= // =============================================================================
ListView { ListView {
id: calls id: calls
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
property var _mapStatusToParams property var _mapStatusToParams
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
signal entrySelected (var entry) signal entrySelected (var entry)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function _getSignIcon (call) { function _getSignIcon (call) {
if (call) { if (call) {
var string = _mapStatusToParams[call.status].string return 'call_sign_' + _mapStatusToParams[call.status].string
return string ? 'call_sign_' + string : '' }
}
return ''
return '' }
}
function _getParams (call) {
function _getParams (call) { if (call) {
if (call) { return _mapStatusToParams[call.status]
return _mapStatusToParams[call.status] }
} }
}
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
boundsBehavior: Flickable.StopAtBounds
boundsBehavior: Flickable.StopAtBounds clip: true
clip: true spacing: 0
spacing: 0
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
Component.onCompleted: {
Component.onCompleted: { _mapStatusToParams = {}
_mapStatusToParams = {}
_mapStatusToParams[CallModel.CallStatusConnected] = {
_mapStatusToParams[CallModel.CallStatusConnected] = { actions: [{
actions: [{ name: qsTr('resumeCall'),
name: qsTr('resumeCall'), handler: (function (call) { call.pausedByUser = false })
handler: (function (call) { call.pausedByUser = false }) }, {
}, { name: qsTr('transferCall'),
name: qsTr('transferCall'), handler: (function (call) { call.transferCall() })
handler: (function (call) { call.transferCall() }) }, {
}, { name: qsTr('terminateCall'),
name: qsTr('terminateCall'), handler: (function (call) { call.terminateCall() })
handler: (function (call) { call.terminateCall() }) }],
}], component: callActions,
component: callActions, string: 'connected'
string: 'connected' }
}
_mapStatusToParams[CallModel.CallStatusEnded] = {
_mapStatusToParams[CallModel.CallStatusEnded] = {} string: 'ended'
}
_mapStatusToParams[CallModel.CallStatusIncoming] = {
actions: [{ _mapStatusToParams[CallModel.CallStatusIncoming] = {
name: qsTr('acceptAudioCall'), actions: [{
handler: (function (call) { call.acceptAudioCall() }) name: qsTr('acceptAudioCall'),
}, { handler: (function (call) { call.acceptAudioCall() })
name: qsTr('acceptVideoCall'), }, {
handler: (function (call) { call.acceptVideoCall() }) name: qsTr('acceptVideoCall'),
}, { handler: (function (call) { call.acceptVideoCall() })
name: qsTr('terminateCall'), }, {
handler: (function (call) { call.terminateCall() }) name: qsTr('terminateCall'),
}], handler: (function (call) { call.terminateCall() })
component: callActions, }],
string: 'incoming' component: callActions,
} string: 'incoming'
}
_mapStatusToParams[CallModel.CallStatusOutgoing] = {
component: callAction, _mapStatusToParams[CallModel.CallStatusOutgoing] = {
handler: (function (call) { call.terminateCall() }), component: callAction,
icon: 'hangup', handler: (function (call) { call.terminateCall() }),
string: 'outgoing' icon: 'hangup',
} string: 'outgoing'
}
_mapStatusToParams[CallModel.CallStatusPaused] = {
actions: [{ _mapStatusToParams[CallModel.CallStatusPaused] = {
name: qsTr('pauseCall'), actions: [{
handler: (function (call) { call.pausedByUser = true }) name: qsTr('pauseCall'),
}, { handler: (function (call) { call.pausedByUser = true })
name: qsTr('transferCall'), }, {
handler: (function (call) { call.transferCall() }) name: qsTr('transferCall'),
}, { handler: (function (call) { call.transferCall() })
name: qsTr('terminateCall'), }, {
handler: (function (call) { call.terminateCall() }) name: qsTr('terminateCall'),
}], handler: (function (call) { call.terminateCall() })
component: callActions, }],
string: 'paused' component: callActions,
} string: 'paused'
} }
}
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
Component {
id: callAction Component {
id: callAction
ActionButton {
icon: params.icon ActionButton {
iconSize: CallsStyle.entry.iconActionSize icon: params.icon
iconSize: CallsStyle.entry.iconActionSize
onClicked: params.handler(call)
} onClicked: params.handler(call)
} }
}
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
Component {
id: callActions Component {
id: callActions
ActionButton {
id: button ActionButton {
id: button
icon: 'burger_menu'
iconSize: CallsStyle.entry.iconMenuSize icon: calls.currentIndex === callId && call.status !== CallModel.CallStatusEnded
? 'burger_menu_light'
onClicked: menu.showMenu() : 'burger_menu'
iconSize: CallsStyle.entry.iconMenuSize
DropDownMenu {
id: menu onClicked: menu.showMenu()
implicitWidth: actionMenu.width DropDownMenu {
launcher: button id: menu
relativeTo: callControls
relativeX: callControls.width implicitWidth: actionMenu.width
launcher: button
ActionMenu { relativeTo: callControls
id: actionMenu relativeX: callControls.width
entryHeight: CallsStyle.entry.height ActionMenu {
entryWidth: CallsStyle.entry.width id: actionMenu
Repeater { entryHeight: CallsStyle.entry.height
model: params.actions entryWidth: CallsStyle.entry.width
ActionMenuEntry { Repeater {
entryName: modelData.name model: params.actions
onClicked: { ActionMenuEntry {
menu.hideMenu() entryName: modelData.name
params.actions[index].handler(call)
} onClicked: {
} menu.hideMenu()
} params.actions[index].handler(call)
} }
} }
} }
} }
}
// --------------------------------------------------------------------------- }
}
delegate: CallControls {
id: _callControls // ---------------------------------------------------------------------------
signIcon: _getSignIcon($call) delegate: CallControls {
sipAddress: $call.sipAddress id: _callControls
width: parent.width
function useColorStatus () {
Loader { return calls.currentIndex === index && $call.status !== CallModel.CallStatusEnded
property var call: $call }
property var callControls: _callControls
property var params: _getParams($call) color: useColorStatus()
? CallsStyle.entry.color.selected
anchors.centerIn: parent : CallsStyle.entry.color.normal
sourceComponent: params.component sipAddressColor: useColorStatus()
} ? CallsStyle.entry.sipAddressColor.selected
} : CallsStyle.entry.sipAddressColor.normal
usernameColor: useColorStatus()
? CallsStyle.entry.usernameColor.selected
: CallsStyle.entry.usernameColor.normal
signIcon: _getSignIcon($call)
sipAddress: $call.sipAddress
width: parent.width
Loader {
property int callId: index
property var call: $call
property var callControls: _callControls
property var params: _getParams($call)
anchors.centerIn: parent
sourceComponent: params.component
}
SequentialAnimation on color {
loops: CallsStyle.entry.endCallAnimation.loops
running: $call.status === CallModel.CallStatusEnded
ColorAnimation {
duration: CallsStyle.entry.endCallAnimation.duration
from: CallsStyle.entry.color.normal
to: CallsStyle.entry.endCallAnimation.blinkColor
}
ColorAnimation {
duration: CallsStyle.entry.endCallAnimation.duration
from: CallsStyle.entry.endCallAnimation.blinkColor
to: CallsStyle.entry.color.normal
}
}
}
} }
...@@ -6,10 +6,10 @@ import Common 1.0 ...@@ -6,10 +6,10 @@ import Common 1.0
// ============================================================================= // =============================================================================
QtObject { QtObject {
property color color: Colors.e property color color: Colors.e
property int height: 60 property int height: 60
property int leftMargin: 12 property int leftMargin: 12
property int rightMargin: 12 property int rightMargin: 12
property int signSize: 40 property int signSize: 40
property int width: 240 property int width: 240
} }
...@@ -6,10 +6,31 @@ import Common 1.0 ...@@ -6,10 +6,31 @@ import Common 1.0
// ============================================================================= // =============================================================================
QtObject { QtObject {
property QtObject entry: QtObject { property QtObject entry: QtObject {
property int iconActionSize: 30 property int iconActionSize: 30
property int iconMenuSize: 17 property int iconMenuSize: 17
property int height: 30 property int height: 30
property int width: 200 property int width: 200
}
property QtObject color: QtObject {
property color normal: Colors.e
property color selected: Colors.j
}
property QtObject endCallAnimation: QtObject {
property color blinkColor: Colors.i
property int duration: 300
property int loops: 3
}
property QtObject sipAddressColor: QtObject {
property color normal: Colors.w
property color selected: Colors.k
}
property QtObject usernameColor: QtObject {
property color normal: Colors.j
property color selected: Colors.k
}
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment