Display video thumbnails.

Crop thumbnail and pictures if distored.
This commit is contained in:
Julien Wadel 2023-04-13 18:08:21 +02:00
parent c8b80c4282
commit 3d7a9acc25
20 changed files with 555 additions and 387 deletions

View file

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Primary color for links in chat.
- Replace double click on avatar by a simple click for copying address into the SmartSearchBar.
- Bubble chat layout.
### Added
- VFS Encryption
@ -26,6 +27,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Emojis picker.
- Text edit in chat can now understand rich texts.
- Create thumbnails into memory instead of disk.
- Display video thumbnails.
- Crop thumbnail and pictures if distored.
### Removed
- Picture zoom on mouse over.

View file

@ -132,7 +132,7 @@ if( WIN32)
endif()
set(CMAKE_INCLUDE_CURRENT_DIR ON)#useful for config.h
set(QT5_PACKAGES Core Gui Quick Widgets QuickControls2 Svg LinguistTools Concurrent Network Test Qml)
set(QT5_PACKAGES Core Gui Quick Widgets QuickControls2 Svg LinguistTools Concurrent Network Test Qml Multimedia)
if(ENABLE_APP_OAUTH2)
list(APPEND QT5_PACKAGES NetworkAuth)

View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
width="80"
height="80"
viewBox="0 0 80 80"
xml:space="preserve"
id="svg8"
sodipodi:docname="thumbnail_video_custom.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview10"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="3.2539062"
inkscape:cx="117.55102"
inkscape:cy="54.088836"
inkscape:window-width="1920"
inkscape:window-height="1043"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg8" />
<defs
id="defs2">
</defs>
<g
style="opacity:1;fill:none;fill-rule:nonzero;stroke:none;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none"
transform="matrix(0.55555556,0,0,0.55555556,15.40659,15.406593)"
id="g6">
<path
d="M 45,0 C 20.147,0 0,20.147 0,45 0,69.853 20.147,90 45,90 69.853,90 90,69.853 90,45 90,20.147 69.853,0 45,0 Z M 62.251,46.633 37.789,60.756 C 36.531,61.482 34.96,60.575 34.96,59.123 V 30.877 c 0,-1.452 1.572,-2.36 2.829,-1.634 L 62.25,43.366 c 1.258,0.726 1.258,2.541 10e-4,3.267 z"
style="opacity:1;fill:#000000;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none"
stroke-linecap="round"
id="path4" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -164,6 +164,7 @@
<file>assets/images/stop_fullscreen_custom.svg</file>
<file>assets/images/timer_custom.svg</file>
<file>assets/images/tel_keypad_voicemail_custom.svg</file>
<file>assets/images/thumbnail_video_custom.svg</file>
<file>assets/images/tooltip_arrow_bottom_custom.svg</file>
<file>assets/images/tooltip_arrow_left_custom.svg</file>
<file>assets/images/tooltip_arrow_right_custom.svg</file>
@ -412,6 +413,7 @@
<file>ui/modules/Linphone/Styles/Dialog/OnlineInstallerDialogStyle.qml</file>
<file>ui/modules/Linphone/Styles/Dialog/SipAddressDialogStyle.qml</file>
<file>ui/modules/Linphone/Styles/Dialog/ZrtpTokenAuthenticationDialogStyle.qml</file>
<file>ui/modules/Linphone/Styles/File/FileViewStyle.qml</file>
<file>ui/modules/Linphone/Styles/History/HistoryStyle.qml</file>
<file>ui/modules/Linphone/Styles/Menus/SipAddressesMenuStyle.qml</file>
<file>ui/modules/Linphone/Styles/Menus/IncallMenuStyle.qml</file>

View file

@ -35,8 +35,17 @@ ExternalImageProvider::ExternalImageProvider () : QQuickImageProvider(
) {
}
QImage ExternalImageProvider::requestImage (const QString &id, QSize *size, const QSize &) {
QImage image(Utils::getImage(QUrl::fromPercentEncoding(id.toUtf8())));
*size = image.size();
return image;
QImage ExternalImageProvider::requestImage (const QString &id, QSize *size, const QSize &requestedSize) {
QImage image(Utils::getImage(QUrl::fromPercentEncoding(id.toUtf8())));
double requestedFactor = 1.0;
double factor = image.width() / (double)image.height();
if(requestedSize.isValid())
requestedFactor = requestedSize.width() / (double)requestedSize.height();
if(factor <0.2){// too height
image = image.copy(0,0, image.width(), image.width() / requestedFactor);
}else if( factor > 5){// too large
image = image.copy(0,0, image.height() * requestedFactor, image.height());
}
*size = image.size();
return image;
}

View file

@ -22,6 +22,7 @@
#include <QQmlApplicationEngine>
#include <QImageReader>
#include <QPainter>
#include <QMediaPlayer>
#include "app/App.hpp"
#include "utils/Utils.hpp"
@ -30,6 +31,75 @@
#include "components/core/CoreManager.hpp"
#include "utils/QExifImageHeader.hpp"
#include <QVideoSurfaceFormat>
VideoFrameGrabber::VideoFrameGrabber( QObject *parent)
: QAbstractVideoSurface(parent){
}
QList<QVideoFrame::PixelFormat> VideoFrameGrabber::supportedPixelFormats(QAbstractVideoBuffer::HandleType handleType) const {
Q_UNUSED(handleType);
return QList<QVideoFrame::PixelFormat>()
<< QVideoFrame::Format_ARGB32
<< QVideoFrame::Format_ARGB32_Premultiplied
<< QVideoFrame::Format_RGB32
<< QVideoFrame::Format_RGB24
<< QVideoFrame::Format_RGB565
<< QVideoFrame::Format_RGB555
<< QVideoFrame::Format_ARGB8565_Premultiplied
<< QVideoFrame::Format_BGRA32
<< QVideoFrame::Format_BGRA32_Premultiplied
<< QVideoFrame::Format_BGR32
<< QVideoFrame::Format_BGR24
<< QVideoFrame::Format_BGR565
<< QVideoFrame::Format_BGR555
<< QVideoFrame::Format_BGRA5658_Premultiplied
<< QVideoFrame::Format_AYUV444
<< QVideoFrame::Format_AYUV444_Premultiplied
<< QVideoFrame::Format_YUV444
<< QVideoFrame::Format_YUV420P
<< QVideoFrame::Format_YV12
<< QVideoFrame::Format_UYVY
<< QVideoFrame::Format_YUYV
<< QVideoFrame::Format_NV12
<< QVideoFrame::Format_NV21
<< QVideoFrame::Format_IMC1
<< QVideoFrame::Format_IMC2
<< QVideoFrame::Format_IMC3
<< QVideoFrame::Format_IMC4
<< QVideoFrame::Format_Y8
<< QVideoFrame::Format_Y16
<< QVideoFrame::Format_Jpeg
<< QVideoFrame::Format_CameraRaw
<< QVideoFrame::Format_AdobeDng;
}
bool VideoFrameGrabber::isFormatSupported(const QVideoSurfaceFormat &format) const {
const QImage::Format imageFormat = QVideoFrame::imageFormatFromPixelFormat(format.pixelFormat());
const QSize size = format.frameSize();
return imageFormat != QImage::Format_Invalid
&& !size.isEmpty()
&& format.handleType() == QAbstractVideoBuffer::NoHandle;
}
bool VideoFrameGrabber::start(const QVideoSurfaceFormat &format){
return QAbstractVideoSurface::start(format);
}
void VideoFrameGrabber::stop() {
QAbstractVideoSurface::stop();
}
bool VideoFrameGrabber::present(const QVideoFrame &frame){
if (frame.isValid()) {
emit frameAvailable(frame.image());
return true;
}else
return false;
}
// =============================================================================
ImageModel::ImageModel (const QString& id, const QString& path, const QString& description, QObject * parent) : QObject(parent) {
@ -88,6 +158,25 @@ QImage ImageModel::createThumbnail(const QString& path){
QByteArray format = reader.format();
if(!format.isEmpty())
originalImage = QImage(path, format);
else if(Utils::isVideo(path)){
QMediaPlayer player;
player.setMedia(QUrl::fromLocalFile(path));
player.setPosition(player.duration() / 2);
VideoFrameGrabber grabber;
player.setVideoOutput(&grabber);
QObject * context = new QObject();
QObject::connect(&grabber, &VideoFrameGrabber::frameAvailable, context, [&context,&originalImage, &player](QImage frame) mutable{
originalImage = frame.copy();
player.stop();
context->deleteLater();// This will destroy context and initializer
context = nullptr;
}, Qt::DirectConnection);
player.play();
do{
qApp->processEvents();
}while(player.state() != QMediaPlayer::State::StoppedState);
if(context) context->deleteLater();
}
}
if (!originalImage.isNull()){
int rotation = 0;
@ -101,26 +190,27 @@ QImage ImageModel::createThumbnail(const QString& path){
painter.drawImage(0, 0, originalImage);
//--------------------
double factor = image.width() / (double)image.height();
if(factor < 0.2 || factor > 5){
qInfo() << QStringLiteral("Cannot create thumbnails because size factor (%1) is too low/much of: `%2`.").arg(factor).arg(path);
}else {
thumbnail = image.scaled(
Qt::AspectRatioMode aspectRatio = Qt::KeepAspectRatio;
if(factor < 0.2 || factor > 5)
aspectRatio = Qt::KeepAspectRatioByExpanding;
thumbnail = image.scaled(
Constants::ThumbnailImageFileWidth, Constants::ThumbnailImageFileHeight,
Qt::KeepAspectRatio, Qt::SmoothTransformation
aspectRatio , Qt::SmoothTransformation
);
if(aspectRatio == Qt::KeepAspectRatioByExpanding)
thumbnail = thumbnail.copy(0,0,Constants::ThumbnailImageFileWidth, Constants::ThumbnailImageFileHeight);
if (rotation != 0) {
QTransform transform;
if (rotation == 3 || rotation == 4)
transform.rotate(180);
else if (rotation == 5 || rotation == 6)
transform.rotate(90);
else if (rotation == 7 || rotation == 8)
transform.rotate(-90);
thumbnail = thumbnail.transformed(transform);
if (rotation == 2 || rotation == 4 || rotation == 5 || rotation == 7)
thumbnail = thumbnail.mirrored(true, false);
}
if (rotation != 0) {
QTransform transform;
if (rotation == 3 || rotation == 4)
transform.rotate(180);
else if (rotation == 5 || rotation == 6)
transform.rotate(90);
else if (rotation == 7 || rotation == 8)
transform.rotate(-90);
thumbnail = thumbnail.transformed(transform);
if (rotation == 2 || rotation == 4 || rotation == 5 || rotation == 7)
thumbnail = thumbnail.mirrored(true, false);
}
}
}

View file

@ -28,6 +28,25 @@
#include <QColor>
#include "utils/LinphoneEnums.hpp"
#include <QAbstractVideoSurface>
class VideoFrameGrabber : public QAbstractVideoSurface {
Q_OBJECT
public:
VideoFrameGrabber(QObject *parent = 0);
QList<QVideoFrame::PixelFormat> supportedPixelFormats(
QAbstractVideoBuffer::HandleType handleType = QAbstractVideoBuffer::NoHandle) const override;
bool isFormatSupported(const QVideoSurfaceFormat &format) const override;
bool start(const QVideoSurfaceFormat &format) override;
void stop() override;
bool present(const QVideoFrame &frame) override;
signals:
void frameAvailable(QImage frame);
};
class ImageModel : public QObject {
Q_OBJECT

View file

@ -50,6 +50,9 @@ ParticipantDeviceListModel::ParticipantDeviceListModel (CallModel * callModel, Q
}
}
ParticipantDeviceListModel::~ParticipantDeviceListModel(){
}
void ParticipantDeviceListModel::initConferenceModel(){
if(!mInitialized && mCallModel){
auto conferenceModel = mCallModel->getConferenceSharedModel();

View file

@ -38,7 +38,7 @@ class ParticipantDeviceListModel : public ProxyListModel {
public:
ParticipantDeviceListModel (std::shared_ptr<linphone::Participant> participant, QObject *parent = nullptr);
ParticipantDeviceListModel (CallModel * callModel, QObject *parent = nullptr);
~ParticipantDeviceListModel();
void initConferenceModel();
void updateDevices(std::shared_ptr<linphone::Participant> participant);
void updateDevices(const std::list<std::shared_ptr<linphone::ParticipantDevice>>& devices, const bool& isMe);

View file

@ -650,6 +650,10 @@ bool Utils::isSupportedForDisplay(const QString& path){
return !QMimeDatabase().mimeTypeForFile(path).name().contains("application");// "for pdf : "application/pdf". Note : Make an exception when supported.
}
bool Utils::canHaveThumbnail(const QString& path){
return isImage(path) || isAnimatedImage(path) || isPdf(path) || isVideo(path);
}
bool Utils::isPhoneNumber(const QString& txt){
auto core = CoreManager::getInstance()->getCore();
if(!core)

View file

@ -71,6 +71,7 @@ public:
Q_INVOKABLE static bool isVideo(const QString& path);
Q_INVOKABLE static bool isPdf(const QString& path);
Q_INVOKABLE static bool isSupportedForDisplay(const QString& path);
Q_INVOKABLE static bool canHaveThumbnail(const QString& path);
Q_INVOKABLE static bool isPhoneNumber(const QString& txt);
Q_INVOKABLE static bool isUsername(const QString& txt); // Check with Regex
Q_INVOKABLE QSize getImageSize(const QString& url);

View file

@ -47,10 +47,6 @@ Loader{
property int fitWidth: layout.fitWidth + ChatCalendarMessageStyle.leftMargin+ChatCalendarMessageStyle.rightMargin
anchors.fill: parent
anchors.leftMargin: ChatCalendarMessageStyle.widthMargin
anchors.rightMargin: ChatCalendarMessageStyle.widthMargin
anchors.topMargin: ChatCalendarMessageStyle.topMargin
anchors.bottomMargin: ChatCalendarMessageStyle.bottomMargin
clip: false

View file

@ -23,10 +23,10 @@ Loader{// Use of Loader because of Repeater (items cannot be loaded dynamically)
id: mainItem
property ChatMessageModel chatMessageModel: null
property int availableWidth //const
property int fileWidth: ChatStyle.entry.message.file.height * 4 / 3 + 2*ChatStyle.entry.message.file.margins
property int fileWidth: FileViewStyle.height * 4 / 3 + 2*ChatStyle.entry.message.file.margins
// Readonly
property int bestWidth: Math.min(availableWidth, Math.max(filesBestWidth, conferencesBestWidth, textsBestWidth, voicesBestWidth))
property int bestWidth: Math.min(availableWidth, Math.max(filesCount*filesBestWidth, conferencesBestWidth, textsBestWidth, voicesBestWidth))
property int filesBestWidth: 0
property int filesCount: 0
property int conferencesCount: 0
@ -49,11 +49,12 @@ Loader{// Use of Loader because of Repeater (items cannot be loaded dynamically)
property int fileBackgroundRadius: ChatStyle.entry.message.file.extension.radius
active: chatMessageModel
sourceComponent: Component{
Column{
id: mainComponent
spacing: 0
padding: 10
function updateFilesBestWidth(){
var newBestWidth = 0
var count = 0
@ -63,13 +64,10 @@ Loader{// Use of Loader because of Repeater (items cannot be loaded dynamically)
var a = item.fitWidth
if(a) {
++count
newBestWidth = Math.max(newBestWidth,a)
newBestWidth = Math.max(newBestWidth,a+2*ChatStyle.entry.message.file.margins)
}
}
}
if(count > 1){
newBestWidth = Math.max(newBestWidth, mainItem.fileWidth*count)
}
mainItem.filesCount = count
mainItem.filesBestWidth = newBestWidth
}
@ -87,7 +85,7 @@ Loader{// Use of Loader because of Repeater (items cannot be loaded dynamically)
}
ListView {
id: messagesVoicesList
width: parent.width
width: parent.width-2*mainComponent.padding
visible: count > 0
spacing: 0
clip: false
@ -115,7 +113,7 @@ Loader{// Use of Loader because of Repeater (items cannot be loaded dynamically)
// CONFERENCE
ListView {
id: messagesConferencesList
width: parent.width
width: parent.width-2*mainComponent.padding
visible: count > 0
spacing: 0
clip: false
@ -151,14 +149,14 @@ Loader{// Use of Loader because of Repeater (items cannot be loaded dynamically)
id: messageFilesList
property alias count: repeater.count
visible: count > 0
clip: false
clip: false
width: parent.width-2*mainComponent.padding
property int availableSection: mainItem.availableWidth / mainItem.fileWidth
property int bestFitSection: mainItem.bestWidth / mainItem.fileWidth
property int availableSection: mainItem.availableWidth / mainItem.filesBestWidth
property int bestFitSection: mainItem.bestWidth / mainItem.filesBestWidth
columns: Math.max(1, Math.min(availableSection , bestFitSection))
columnSpacing: 0
rowSpacing: 0
width: parent.width
rowSpacing: ChatStyle.entry.message.file.spacing
Repeater{
id: repeater
model: ContentProxyModel{
@ -166,6 +164,12 @@ Loader{// Use of Loader because of Repeater (items cannot be loaded dynamically)
chatMessageModel: mainItem.chatMessageModel
}
ChatFileMessage{
Layout.fillHeight: true
Layout.fillWidth: true
Layout.preferredHeight: fitHeight
Layout.preferredWidth: fitWidth
Layout.maximumWidth: fitWidth
Layout.maximumHeight: fitHeight
contentModel: $modelData
onIsHoveringChanged: mainItem.isFileHoveringChanged(isHovering)
borderWidth: mainItem.fileBorderWidth
@ -178,7 +182,7 @@ Loader{// Use of Loader because of Repeater (items cannot be loaded dynamically)
// TEXTS
ListView {
id: messagesTextsList
width: parent.width
width: parent.width-2*mainComponent.padding
visible: count > 0
spacing: 0
clip: false
@ -192,11 +196,14 @@ Loader{// Use of Loader because of Repeater (items cannot be loaded dynamically)
function updateBestWidth(){
var newWidth = mainComponent.updateListBestWidth(messagesTextsList)
mainItem.textsCount = newWidth[0]
mainItem.textsBestWidth = newWidth[1]
// Padding is takken account because it is used for the whole bubble.
// We add 1 pixel to avoid implicit new line computation (Guess : float computation from Qt)
mainItem.textsBestWidth = newWidth[1] + 2*mainComponent.padding + 1
}
Component.onCompleted: messagesTextsList.updateBestWidth()
delegate:
ChatTextMessage {
width: parent.width
contentModel: $modelData
onLastTextSelectedChanged: mainItem.lastTextSelectedChanged(lastTextSelected)
color: mainItem.useTextColor

View file

@ -13,270 +13,35 @@ import UtilsCpp 1.0
// =============================================================================
// TODO : into Loader
Row {
Item {
id:mainRow
property ChatMessageModel chatMessageModel: contentModel && contentModel.chatMessageModel
property ContentModel contentModel
property bool isOutgoing : chatMessageModel && ( chatMessageModel.isOutgoing || chatMessageModel.state == LinphoneEnums.ChatMessageStateIdle);
property int fitHeight: mainRow.isAnimatedImage ? ChatStyle.entry.message.file.heightbetter : ChatStyle.entry.message.file.height
property int fitWidth: fitHeight * 4 / 3 + 2*ChatStyle.entry.message.file.margins
property int borderWidth : 0
property color backgroundColor: ChatStyle.entry.message.file.extension.background.colorModel.color
property int backgroundRadius: ChatStyle.entry.message.file.extension.radius
/*
property int fitWidth: visible
? Math.max( Math.max((thumbnailProvider.sourceComponent == extension
? thumbnailProvider.item.fitWidth
: 0)
, thumbnailProvider.width + 3*ChatStyle.entry.message.file.margins)
, Math.max(ChatStyle.entry.message.file.width, ChatStyle.entry.message.outgoing.areaSize))
: 0
property int fitHeight: visible ? rectangle.height : 0
*/
property bool isAnimatedImage : mainRow.contentModel && mainRow.contentModel.wasDownloaded && UtilsCpp.isAnimatedImage(mainRow.contentModel.filePath)
property bool haveThumbnail: mainRow.contentModel && mainRow.contentModel.thumbnail
property bool isHovering: thumbnailProvider.state == 'hovered'
property int fitHeight: fileView.fitHeight
property int fitWidth: fileView.fitWidth
property alias borderWidth: fileView.borderWidth
property alias backgroundColor: fileView.backgroundColor
property alias backgroundRadius: fileView.backgroundRadius
property alias isHovering: fileView.isHovering
signal copyAllDone()
signal copySelectionDone()
signal forwardClicked()
height: fitHeight
width: fitWidth
visible: true
// ---------------------------------------------------------------------------
// File message.
// ---------------------------------------------------------------------------
Item{
width: mainRow.width
height:rectangle.height
Rectangle {
id: rectangle
color: 'transparent'
anchors.fill: parent
radius: ChatStyle.entry.message.radius
Rectangle {
id: rectangle
readonly property bool isError: chatMessageModel && Utils.includes([
LinphoneEnums.ChatMessageStateFileTransferError,
LinphoneEnums.ChatMessageStateNotDelivered,
], chatMessageModel.state)
readonly property bool isUploaded: chatMessageModel && chatMessageModel.state == LinphoneEnums.ChatMessageStateDelivered
readonly property bool isDelivered: chatMessageModel && chatMessageModel.state == LinphoneEnums.ChatMessageStateDeliveredToUser
readonly property bool isRead: chatMessageModel && chatMessageModel.state == LinphoneEnums.ChatMessageStateDisplayed
readonly property bool isTransferring: chatMessageModel && (chatMessageModel.state == LinphoneEnums.ChatMessageStateFileTransferInProgress || chatMessageModel.state == LinphoneEnums.ChatMessageStateInProgress )
property string thumbnail : mainRow.contentModel ? mainRow.contentModel.thumbnail : ''
color: 'transparent'
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
height: 2*ChatStyle.entry.message.file.margins + (mainRow.isAnimatedImage
? ChatStyle.entry.message.file.heightbetter
: thumbnailProvider.sourceComponent == extension
? ChatStyle.entry.message.file.height
: ChatStyle.entry.message.file.height
)
radius: ChatStyle.entry.message.radius
// ---------------------------------------------------------------------
// Thumbnail or extension.
// ---------------------------------------------------------------------
Component {
id: thumbnailImage
Image {
id: thumbnailImageSource
property real scaleAnimatorTo : ChatStyle.entry.message.file.animation.thumbnailTo
anchors.centerIn: parent
mipmap: SettingsModel.mipmapEnabled
source: mainRow.contentModel.thumbnail
autoTransform: true
fillMode: Image.PreserveAspectFit
height: ChatStyle.entry.message.file.height
width: height*4/3
Component.onCompleted: mainRow.fitHeight = height
Loader{
anchors.fill: parent
sourceComponent: Image{// Better quality on zoom
mipmap: SettingsModel.mipmapEnabled
source:'image://external/'+mainRow.contentModel.filePath
autoTransform: true
fillMode: Image.PreserveAspectFit
visible: status == Image.Ready
}
asynchronous: true
active: thumbnailProvider.state == 'hovered'
}
}
}
Component {
id: animatedImage
AnimatedImage {
id: animatedImageSource
property real scaleAnimatorTo : ChatStyle.entry.message.file.animation.to
mipmap: SettingsModel.mipmapEnabled
source: 'file:/'+mainRow.contentModel.filePath
autoTransform: true
fillMode: Image.PreserveAspectFit
height: ChatStyle.entry.message.file.heightbetter
width: height*4/3
Component.onCompleted: mainRow.fitHeight = height
}
}
Component {
id: extension
Rectangle {
property int fitWidth: Math.max(downloadText.implicitWidth, Math.max(fileName.visible ? fileName.implicitWidth : 0, fileIcon.iconSize)) + 20
//property int fitHeight: fileIcon.iconSize + (fileName.visible ? fileName.implicitHeight + ChatStyle.entry.message.file.spacing : 0 )
// + (downloadText.visible? downloadText.implicitHeight + ChatStyle.entry.message.file.spacing : 0) + 2*ChatStyle.entry.message.file.margins
property real scaleAnimatorTo : ChatStyle.entry.message.file.animation.to
anchors.centerIn: parent
height: ChatStyle.entry.message.file.height
width: height*4/3
color: mainRow.backgroundColor
radius: mainRow.backgroundRadius
border.width: mainRow.borderWidth
border.color: ChatStyle.entry.message.file.extension.background.borderColorModel.color
ColumnLayout{
anchors.fill: parent
anchors.topMargin: ChatStyle.entry.message.file.margins
anchors.bottomMargin: ChatStyle.entry.message.file.margins
spacing: ChatStyle.entry.message.file.spacing
Icon{
id: fileIcon
Layout.alignment: Qt.AlignCenter
icon: extensionText.text != '' ? ChatStyle.entry.message.file.extension.icon : ChatStyle.entry.message.file.extension.unknownIcon
iconSize: ChatStyle.entry.message.file.extension.iconSize
Layout.fillHeight: true
Layout.fillWidth: true
Layout.preferredHeight: iconSize
Layout.preferredWidth: iconSize
Text {
id: extensionText
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: ChatStyle.entry.message.file.spacing
width: parent.width - 2*ChatStyle.entry.message.file.spacing
color: ChatStyle.entry.message.file.extension.text.colorModel.color
font.bold: true
font.pointSize: ChatStyle.entry.message.file.extension.text.pointSize
clip: true
text: (mainRow.contentModel?Utils.getExtension(mainRow.contentModel.name).toUpperCase():'')
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
RoundProgressBar {
id: progressBar
anchors.centerIn: parent
property int fileSize: mainRow.contentModel ? mainRow.contentModel.fileSize : 0
to: 100
value: mainRow.contentModel ? (fileSize>0 ? Math.floor(100 * mainRow.contentModel.fileOffset / fileSize) : 0) : to
visible: rectangle.isTransferring && value != 0
/* Change format? Current is %
text: if(mainRow.contentModel){
var fileSize = Utils.formatSize(mainRow.contentModel.fileSize)
return progressBar.visible
? Utils.formatSize(mainRow.contentModel.fileOffset) + '/' + fileSize
: fileSize
}else
return ''
*/
}
}
Text {
id: fileName
Layout.fillWidth: true
Layout.fillHeight: true
visible: mainRow.contentModel && !mainRow.isAnimatedImage && !mainRow.haveThumbnail
color: ChatStyle.entry.message.file.extension.text.colorModel.color
font.pointSize: ChatStyle.entry.message.file.name.pointSize
wrapMode: Text.WrapAnywhere
horizontalAlignment: Text.AlignHCenter
maximumLineCount: 2
text: (mainRow.contentModel ? mainRow.contentModel.name : '')
}
Text{
id: downloadText
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredHeight: visible ? contentHeight : 0
//: 'Cancel' : Message link to cancel a transfer (upload/download)
text: mainRow.contentModel ? rectangle.isTransferring ? qsTr('fileTransferCancel')
//: 'Download' : Message link to download a file
: qsTr('fileTransferDownload') +' ('+Utils.formatSize(mainRow.contentModel.fileSize)+')'
: ''
font.underline: true
font.pointSize: ChatStyle.entry.message.file.download.pointSize
color:ChatStyle.entry.message.file.extension.text.colorModel.color
visible: (mainRow.contentModel? (!mainItem.isOutgoing && !mainRow.contentModel.wasDownloaded) || rectangle.isTransferring : false)
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
}
}
Loader {
id: thumbnailProvider
anchors.centerIn: parent
sourceComponent: (mainRow.contentModel ?
(mainRow.isAnimatedImage ? animatedImage
: (mainRow.haveThumbnail ? thumbnailImage : extension )
) : undefined)
states: State {
name: 'hovered'
}
}
}
MouseArea {
function handleMouseMove (mouse) {
thumbnailProvider.state = Utils.pointIsInItem(this, thumbnailProvider, mouse)
? 'hovered'
: ''
}
FileView{
id: fileView
anchors.fill: parent
visible: true
onClicked: {
if(rectangle.isTransferring)
mainRow.contentModel.cancelDownloadFile()
else if( !mainRow.contentModel.wasDownloaded) {
thumbnailProvider.state = ''
mainRow.contentModel.downloadFile()
}else if (Utils.pointIsInItem(this, thumbnailProvider, mouse)) {
if(SettingsModel.isVfsEncrypted){
window.attachVirtualWindow(Utils.buildCommonDialogUri('FileViewDialog'), {
contentModel: mainRow.contentModel,
}, function (status) {
})
}else
mainRow.contentModel.openFile()
} else if (mainRow.contentModel ) {
thumbnailProvider.state = ''
mainRow.contentModel.openFile(true)// Show directory
} else {
thumbnailProvider.state = ''
mainRow.contentModel.downloadFile()
}
}
onExited: thumbnailProvider.state = ''
onMouseXChanged: handleMouseMove.call(this, mouse)
onMouseYChanged: handleMouseMove.call(this, mouse)
contentModel: mainRow.contentModel
thumbnail: mainRow.contentModel.thumbnail
name: mainRow.contentModel && mainRow.contentModel.name
filePath: mainRow.contentModel && mainRow.contentModel.filePath
isTransferring: mainRow.chatMessageModel && (mainRow.chatMessageModel.state == LinphoneEnums.ChatMessageStateFileTransferInProgress || mainRow.chatMessageModel.state == LinphoneEnums.ChatMessageStateInProgress )
}
}
}
}

View file

@ -49,12 +49,10 @@ Item{
width: height * ChatFilePreviewStyle.filePreview.format
anchors.verticalCenter: parent ? parent.verticalCenter : ScrollableListView.verticalCenter
anchors.verticalCenterOffset: 7
contentModel: $modelData
thumbnail: $modelData.thumbnail
name: $modelData.name
animationScale: 1.1
onClickOnFile: {
$modelData.openFile()
}
ActionButton{
anchors.bottom: parent.top
anchors.bottomMargin: -height/2

View file

@ -23,8 +23,8 @@ TextEdit {
property ContentModel contentModel
property string lastTextSelected : ''
property font customFont : SettingsModel.textMessageFont
property int fitHeight: contentHeight + padding + 8
property int fitWidth: implicitWidth + 2 // add 2 because there is a bug on border that lead to not fit text exactly
property int fitHeight: contentHeight
property int fitWidth: implicitWidth
signal rightClicked()
@ -33,9 +33,8 @@ TextEdit {
height: fitHeight
width: parent && parent.width || 1
visible: contentModel// && contentModel.isText()
visible: contentModel
clip: false
padding: ChatStyle.entry.message.padding
textMargin: 0
readOnly: true
selectByMouse: true
@ -70,7 +69,6 @@ TextEdit {
}
deselect()
}
MouseArea {
id: mouseArea
property bool keepLastSelection: false

View file

@ -7,6 +7,7 @@ import Linphone 1.0
import LinphoneEnums 1.0
import Linphone.Styles 1.0
import Utils 1.0
import UtilsCpp 1.0
import Units 1.0
import ColorsList 1.0
@ -15,118 +16,249 @@ import ColorsList 1.0
Item {
id: mainItem
property string thumbnail
property string name
property ContentModel contentModel
property string thumbnail: contentModel && contentModel.thumbnail
property string name: contentModel && contentModel.name
property string filePath: contentModel && contentModel.filePath
property int fileHeight: FileViewStyle.height
property bool active: true
property real animationScale : ChatStyle.entry.message.file.animation.to
property real animationScale : FileViewStyle.animation.to
property alias imageScale: thumbnailProvider.scale
property int fitHeight: mainItem.isAnimatedImage ? FileViewStyle.heightbetter : FileViewStyle.height
property int fitWidth: fitHeight*4/3
property bool isAnimatedImage : contentModel && contentModel.wasDownloaded && UtilsCpp.isAnimatedImage(filePath)
property bool haveThumbnail: contentModel && UtilsCpp.canHaveThumbnail(filePath)
property int borderWidth : 0
property color backgroundColor: FileViewStyle.extension.background.colorModel.color
property int backgroundRadius: FileViewStyle.extension.radius
property bool isTransferring
property bool isHovering: thumbnailProvider.state == 'hovered'
signal clickOnFile()
// ---------------------------------------------------------------------
// Thumbnail or extension.
// ---------------------------------------------------------------------
MouseArea {
function handleMouseMove (mouse) {
thumbnailProvider.state = Utils.pointIsInItem(this, thumbnailProvider, mouse)
? 'hovered'
: ''
}
anchors.fill: parent
visible: true
onClicked: {
if(mainItem.isTransferring)
mainItem.contentModel.cancelDownloadFile()
else if( !mainItem.contentModel.wasDownloaded) {
thumbnailProvider.state = ''
mainItem.contentModel.downloadFile()
}else if (Utils.pointIsInItem(this, thumbnailProvider, mouse)) {
if(SettingsModel.isVfsEncrypted){
window.attachVirtualWindow(Utils.buildCommonDialogUri('FileViewDialog'), {
contentModel: mainItem.contentModel,
}, function (status) {
})
}else
mainItem.contentModel.openFile()
} else if (mainItem.contentModel ) {
thumbnailProvider.state = ''
mainItem.contentModel.openFile(true)// Show directory
} else {
thumbnailProvider.state = ''
mainItem.contentModel.downloadFile()
}
mainItem.clickOnFile()
}
onExited: thumbnailProvider.state = ''
onMouseXChanged: handleMouseMove.call(this, mouse)
onMouseYChanged: handleMouseMove.call(this, mouse)
}
// ---------------------------------------------------------------------
// Thumbnail
// ---------------------------------------------------------------------
Component {
id: thumbnailImage
Image {
id: thumbnailImageSource
property real scaleAnimatorTo : FileViewStyle.animation.thumbnailTo
property bool isVideo: UtilsCpp.isVideo(mainItem.filePath)
mipmap: SettingsModel.mipmapEnabled
source: mainItem.thumbnail
autoTransform: true
fillMode: Image.PreserveAspectFit
anchors.fill: parent
Loader{
anchors.fill: parent
sourceComponent: Image{// Better quality on zoom
mipmap: SettingsModel.mipmapEnabled
source: !thumbnailImageSource.isVideo ? 'image://external/'+mainItem.filePath : ''
autoTransform: true
fillMode: Image.PreserveAspectFit
visible: status == Image.Ready
}
asynchronous: true
active: !thumbnailImageSource.isVideo && thumbnailProvider.state == 'hovered'
}
ActionButton{
id: thumbnailVideoButton
anchors.centerIn: parent
visible: thumbnailImageSource.isVideo
isCustom: true
backgroundRadius: width
colorSet: FileViewStyle.thumbnailVideoIcon
onClicked:{
window.attachVirtualWindow(Utils.buildCommonDialogUri('FileViewDialog'), {
contentModel: mainItem.contentModel,
}, function (status) {
})
}
}
}
}
Component {
id: animatedImage
AnimatedImage {
id: animatedImageSource
property real scaleAnimatorTo : FileViewStyle.animation.to
mipmap: SettingsModel.mipmapEnabled
source: 'file:/'+mainItem.filePath
autoTransform: true
fillMode: Image.PreserveAspectFit
anchors.fill: parent
}
}
// ---------------------------------------------------------------------
// Extension
// ---------------------------------------------------------------------
Component {
id: extension
Rectangle {
color: ChatStyle.entry.message.file.extension.background.colorModel.color
Text {
property real scaleAnimatorTo : FileViewStyle.animation.to
anchors.fill: parent
color: mainItem.backgroundColor
radius: mainItem.backgroundRadius
border.width: mainItem.borderWidth
border.color: FileViewStyle.extension.background.borderColorModel.color
ColumnLayout{
anchors.fill: parent
color: ChatStyle.entry.message.file.extension.text.colorModel.color
font.bold: true
elide: Text.ElideRight
text: Utils.getExtension(mainItem.name).toUpperCase()
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
anchors.topMargin: FileViewStyle.margins
anchors.bottomMargin: FileViewStyle.margins
spacing: FileViewStyle.spacing
Icon{
id: fileIcon
Layout.alignment: Qt.AlignCenter
icon: extensionText.text != '' ? FileViewStyle.extension.icon : FileViewStyle.extension.unknownIcon
iconSize: FileViewStyle.extension.iconSize
Layout.fillHeight: true
Layout.fillWidth: true
Layout.preferredHeight: iconSize
Layout.preferredWidth: iconSize
Text {
id: extensionText
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: FileViewStyle.spacing
width: FileViewStyle.extension.internalSize
onWidthChanged: extensionMetrics.font.pointSize = FileViewStyle.extension.text.pointSize // reset metrics
color: FileViewStyle.extension.text.colorModel.color
font.bold: true
font.pointSize: extensionMetrics.font.pointSize
clip: true
text: (mainItem.contentModel?Utils.getExtension(mainItem.name).toUpperCase():'')
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
TextMetrics{
id: extensionMetrics
text: extensionText.text
font.pointSize: FileViewStyle.extension.text.pointSize
onWidthChanged: if(width > extensionText.width) --font.pointSize
Component.onCompleted: if(width > extensionText.width) --font.pointSize
}
}
RoundProgressBar {
id: progressBar
anchors.centerIn: parent
property int fileSize: mainItem.contentModel ? mainItem.contentModel.fileSize : 0
to: 100
value: mainItem.contentModel ? (fileSize>0 ? Math.floor(100 * mainItem.contentModel.fileOffset / fileSize) : 0) : to
visible: mainItem.isTransferring && value != 0
/* Change format? Current is %
text: if(mainRow.contentModel){
var fileSize = Utils.formatSize(mainRow.contentModel.fileSize)
return progressBar.visible
? Utils.formatSize(mainRow.contentModel.fileOffset) + '/' + fileSize
: fileSize
}else
return ''
*/
}
}
Text {
id: fileName
Layout.fillWidth: true
Layout.fillHeight: true
visible: mainItem.contentModel && !mainItem.isAnimatedImage
color: FileViewStyle.extension.text.colorModel.color
font.pointSize: FileViewStyle.name.pointSize
wrapMode: Text.WrapAnywhere
horizontalAlignment: Text.AlignHCenter
maximumLineCount: 2
text: mainItem.name
}
Text{
id: downloadText
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredHeight: visible ? contentHeight : 0
//: 'Cancel' : Message link to cancel a transfer (upload/download)
text: mainItem.contentModel ? mainItem.isTransferring ? qsTr('fileTransferCancel')
//: 'Download' : Message link to download a file
: qsTr('fileTransferDownload') +' ('+Utils.formatSize(mainItem.contentModel.fileSize)+')'
: ''
font.underline: true
font.pointSize: FileViewStyle.download.pointSize
color:FileViewStyle.extension.text.colorModel.color
visible: (mainItem.contentModel? (!mainItem.isOutgoing && !mainItem.contentModel.wasDownloaded) || mainItem.isTransferring : false)
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
}
}
Loader {
id: thumbnailProvider
anchors.fill: parent
sourceComponent: (mainItem.active ? (mainItem.thumbnail ? thumbnailImage : extension ): undefined)
ScaleAnimator {
id: thumbnailProviderAnimator
target: mainItem
duration: ChatStyle.entry.message.file.animation.duration
easing.type: Easing.InOutQuad
from: 1.0
}
sourceComponent: (mainItem.contentModel ?
(mainItem.isAnimatedImage ? animatedImage
: (mainItem.haveThumbnail ? thumbnailImage : extension )
) : undefined)
states: State {
name: 'hovered'
}
transitions: [
Transition {
from: ''
to: 'hovered'
ScriptAction {
script: {
if (thumbnailProviderAnimator.running) {
thumbnailProviderAnimator.running = false
}
mainItem.z = Constants.zPopup
thumbnailProviderAnimator.to = mainItem.animationScale
thumbnailProviderAnimator.running = true
}
}
},
Transition {
from: 'hovered'
to: ''
ScriptAction {
script: {
if (thumbnailProviderAnimator.running) {
thumbnailProviderAnimator.running = false
}
thumbnailProviderAnimator.to = 1.0
thumbnailProviderAnimator.running = true
mainItem.z = 0
}
}
}
]
}
MouseArea {
function handleMouseMove (mouse) {
thumbnailProvider.state = Utils.pointIsInItem(this, thumbnailProvider, mouse)
? 'hovered'
: ''
}
Loader {
id: waitingProvider
anchors.fill: parent
onClicked: {
clickOnFile()
thumbnailProvider.state = ''
sourceComponent: thumbnailProvider.sourceComponent == thumbnailImage && thumbnailProvider.item.status != Image.Ready
? extension
: undefined
states: State {
name: 'hovered'
}
onExited: thumbnailProvider.state = ''
onMouseXChanged: handleMouseMove.call(this, mouse)
onMouseYChanged: handleMouseMove.call(this, mouse)
}
}

View file

@ -201,7 +201,17 @@ QtObject {
property var outgoingColor: ColorsList.addImageColor(sectionName+'_download_out', icon, 'g')
property var incomingColor: ColorsList.addImageColor(sectionName+'_download_in', icon, 'q')
}
property QtObject thumbnailVideoIcon: QtObject {
property int iconSize: 40
property string name : 'play'
property string icon : 'thumbnail_video_custom'
property var backgroundNormalColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_n', icon, 'wr_n_b_bg')
property var backgroundHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_h', icon, 'wr_h_b_bg')
property var backgroundPressedColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_p', icon, 'wr_p_b_bg')
property var foregroundNormalColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_n', icon, 'wr_n_b_fg')
property var foregroundHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_h', icon, 'wr_h_b_fg')
property var foregroundPressedColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_p', icon, 'wr_p_b_fg')
}
property QtObject animation: QtObject {
property int duration: 300
property real to: 1.7
@ -212,6 +222,7 @@ QtObject {
property string icon: 'file_extension_custom'
property string unknownIcon: 'file_unknown_custom'
property int iconSize: 60
property int internalSize: 37
property int radius: 0
property QtObject background: QtObject {

View file

@ -0,0 +1,82 @@
pragma Singleton
import QtQml 2.2
import Units 1.0
import ColorsList 1.0
// =============================================================================
QtObject {
property string sectionName : 'FileView'
property int height: 120
property int heightbetter: 200
property int iconSize: 18
property int margins: 8
property int spacing: 8
property int width: 100
property QtObject name: QtObject{
property int pointSize: Units.dp * 7
}
property QtObject download: QtObject{
property string icon: 'download_custom'
property int height: 20
property int pointSize: Units.dp * 8
property int iconSize: 30
property var outgoingColor: ColorsList.addImageColor(sectionName+'_download_out', icon, 'g')
property var incomingColor: ColorsList.addImageColor(sectionName+'_download_in', icon, 'q')
}
property QtObject thumbnailVideoIcon: QtObject {
property int iconSize: 40
property string name : 'play'
property string icon : 'thumbnail_video_custom'
property var backgroundNormalColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_n', icon, 'wr_n_b_bg')
property var backgroundHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_h', icon, 'wr_h_b_bg')
property var backgroundPressedColor : ColorsList.addImageColor(sectionName+'_'+name+'_bg_p', icon, 'wr_p_b_bg')
property var foregroundNormalColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_n', icon, 'wr_n_b_fg')
property var foregroundHoveredColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_h', icon, 'wr_h_b_fg')
property var foregroundPressedColor : ColorsList.addImageColor(sectionName+'_'+name+'_fg_p', icon, 'wr_p_b_fg')
}
property QtObject animation: QtObject {
property int duration: 300
property real to: 1.7
property real thumbnailTo: 2
}
property QtObject extension: QtObject {
property string icon: 'file_extension_custom'
property string unknownIcon: 'file_unknown_custom'
property int iconSize: 60
property int internalSize: 37
property int radius: 0
property QtObject background: QtObject {
property var colorModel: ColorsList.add(sectionName+'_file_extension_bg', 'q')
property var borderColorModel: ColorsList.add(sectionName+'_file_extension_border', 'extension_file_border')
}
property QtObject text: QtObject {
property var colorModel: ColorsList.add(sectionName+'_file_extension_text', 'd')
property int pointSize: Units.dp * 9
}
}
property QtObject status: QtObject {
property int spacing: 4
property QtObject bar: QtObject {
property int height: 6
property int radius: 3
property QtObject background: QtObject {
property var colorModel: ColorsList.add(sectionName+'_file_statusbar_bg', 'f')
}
property QtObject contentItem: QtObject {
property var colorModel: ColorsList.add(sectionName+'_file_statusbar_content', 'p')
}
}
}
}

View file

@ -37,6 +37,7 @@ singleton OnlineInstallerDialogStyle 1.0 Dialog/OnlineInstallerDialogS
singleton SipAddressDialogStyle 1.0 Dialog/SipAddressDialogStyle.qml
singleton ZrtpTokenAuthenticationDialogStyle 1.0 Dialog/ZrtpTokenAuthenticationDialogStyle.qml
singleton FileViewStyle 1.0 File/FileViewStyle.qml
singleton HistoryStyle 1.0 History/HistoryStyle.qml