You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

658 lines
18 KiB

/*****************************************************************************
* Copyright (C) 2020 VLC authors and VideoLAN
*
* 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 2 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
*****************************************************************************/
#include "addonsmodel.hpp"
#include "util/locallistbasemodel.hpp"
#include "medialibrary/mlhelper.hpp"
#include "playlist/media.hpp"
#include "playlist/playlist_controller.hpp"
#include <memory>
#include <QPixmap>
#include <QQmlFile>
#include <vlc_media_source.h>
#include <vlc_addons.h>
#include <vlc_cxx_helpers.hpp>
#include <vlc_addons.h>
#define CHECK_ADDON_TYPE_MATCH(type) \
static_assert(static_cast<addon_type_t>(AddonsModel::Type::TYPE_ ## type) == ADDON_##type)
CHECK_ADDON_TYPE_MATCH(UNKNOWN);
CHECK_ADDON_TYPE_MATCH(PLAYLIST_PARSER);
CHECK_ADDON_TYPE_MATCH(SERVICE_DISCOVERY);
CHECK_ADDON_TYPE_MATCH(SKIN2);
CHECK_ADDON_TYPE_MATCH(PLUGIN);
CHECK_ADDON_TYPE_MATCH(INTERFACE);
CHECK_ADDON_TYPE_MATCH(META);
CHECK_ADDON_TYPE_MATCH(OTHER);
#undef CHECK_ADDON_TYPE_MATCH
#define CHECK_ADDON_STATE_MATCH(type) \
static_assert(static_cast<addon_state_t>(AddonsModel::State::STATE_ ## type) == ADDON_##type)
CHECK_ADDON_STATE_MATCH(INSTALLING);
CHECK_ADDON_STATE_MATCH(INSTALLED);
CHECK_ADDON_STATE_MATCH(UNINSTALLING);
#undef CHECK_ADDON_STATE_MATCH
namespace {
using AddonPtr = ::vlc::vlc_shared_data_ptr<addon_entry_t,
&addon_entry_Hold, &addon_entry_Release>;
class AddonItem {
public:
//
AddonItem( AddonPtr addon )
{
vlc_mutex_locker locker{&addon->lock};
name = qfu( addon->psz_name );
summary = qfu( addon->psz_summary ).trimmed();
description = qfu( addon->psz_description ).trimmed();
author = qfu( addon->psz_author );
sourceUrl = QUrl( addon->psz_source_uri );
entry = addon;
uuid = QByteArray( reinterpret_cast<const char*>(addon->uuid), sizeof( addon_uuid_t ));
if ( addon->psz_image_data ) {
char *cDir = config_GetUserDir( VLC_CACHE_DIR );
if (likely(cDir != nullptr))
{
QDir dir( cDir );
free(cDir);
dir.mkdir("art");
dir.cd("art");
dir.mkdir("qt-addon-covers");
dir.cd("qt-addon-covers");
char* uuidStr = addons_uuid_to_psz( &addon->uuid );
QString filename = QString("addon_thumbnail_%1.png").arg(uuidStr);
free(uuidStr);
QString absoluteFilePath = dir.absoluteFilePath(filename);
if ( !QFileInfo::exists( absoluteFilePath )) {
QPixmap pixmap;
pixmap.loadFromData( QByteArray::fromBase64( QByteArray( addon->psz_image_data ) ),
0,
Qt::AutoColor
);
pixmap.save(absoluteFilePath);
}
artworkUrl = QUrl::fromLocalFile( absoluteFilePath );
}
}
else if ( addon->e_flags & ADDON_BROKEN )
artworkUrl = QUrl( ":/addons/addon_broken.svg" );
else
artworkUrl = QUrl( ":/addons/addon_default.svg" );
}
public:
QByteArray uuid;
QString name;
QString summary;
QString description;
QString author;
QUrl sourceUrl;
QUrl artworkUrl;
AddonPtr entry;
};
} //namespace
using AddonItemPtr = std::shared_ptr<AddonItem> ;
using AddonItemList = std::vector<AddonItemPtr> ;
// ListCache specialisation
template<>
bool ListCache<AddonItemPtr>::compareItems(const AddonItemPtr& a, const AddonItemPtr& b)
{
//just compare the pointers here
return a->uuid == b->uuid;
}
// AddonsModelPrivate
class AddonsModelPrivate
: public LocalListBaseModelPrivate<AddonItemPtr>
{
public:
Q_DECLARE_PUBLIC(AddonsModel)
public: //ctor/dtor
AddonsModelPrivate(AddonsModel* pub)
: LocalListBaseModelPrivate<AddonItemPtr>(pub)
{
}
~AddonsModelPrivate()
{
if ( m_manager )
addons_manager_Delete( m_manager );
}
public:
const AddonItem* getItemForRow(int row) const
{
const AddonItemPtr* ref = item(row);
if (ref)
return ref->get();
return nullptr;
}
bool gatherRepository(const char* uri)
{
Q_Q(AddonsModel);
if (!m_manager)
return false;
m_loading = true;
emit q->loadingChanged();
addons_manager_Gather( m_manager, uri);
return true;
}
public: //BaseModelPrivateT implementation
bool initializeModel() override;
LocalListCacheLoader<AddonItemPtr>::ItemCompare getSortFunction() const override
{
if (m_sortOrder == Qt::SortOrder::DescendingOrder)
return [](const AddonItemPtr& a, const AddonItemPtr& b){
return QString::compare(a->name, b->name) > 0;
};
else
return [](const AddonItemPtr& a, const AddonItemPtr& b) {
return QString::compare(a->name, b->name) < 0;
};
}
public: //discovery callbacks
void addonFound( AddonPtr addon );
void addonChanged( AddonPtr addon );
void discoveryEnded();
public: //LocalListCacheLoader implementation
//return the data matching the pattern
AddonItemList getModelData(const QString& pattern) const override
{
if (pattern.isEmpty()
&& m_typeFilter == AddonsModel::Type::TYPE_NONE
&& m_stateFilter == AddonsModel::State::STATE_NONE)
return m_items;
AddonItemList items;
std::copy_if(
m_items.cbegin(), m_items.cend(),
std::back_inserter(items),
[&pattern, filter = m_typeFilter, state = m_stateFilter](const AddonItemPtr& item) {
if (state != AddonsModel::State::STATE_NONE
&& static_cast<AddonsModel::State>(item->entry->e_state) != state)
return false;
if (filter != AddonsModel::Type::TYPE_NONE
&& static_cast<AddonsModel::Type>(item->entry->e_type) != filter)
return false;
return item->name.contains(pattern, Qt::CaseInsensitive);
});
return items;
}
public: // data
MainCtx* m_ctx = nullptr;
addons_manager_t* m_manager = nullptr;
AddonItemList m_items;
AddonsModel::Type m_typeFilter = AddonsModel::Type::TYPE_NONE;
AddonsModel::State m_stateFilter = AddonsModel::State::STATE_NONE;
};
AddonsModel::AddonsModel( QObject* parent )
: BaseModel( new AddonsModelPrivate(this), parent )
{
}
QVariant AddonsModel::data( const QModelIndex& index, int role ) const
{
Q_D(const AddonsModel);
if (!d->m_ctx)
return {};
const AddonItem* item = d->getItemForRow(index.row());
if (!item)
return {};
switch ( role )
{
case Qt::DisplayRole:
case Role::NAME:
return item->name;
case Role::AUTHOR:
return item->author;
case Role::SUMMARY:
return item->summary;
case Role::DESCRIPTION:
return item->description;
case Role::DOWNLOADS:
{
vlc_mutex_locker locker{&item->entry->lock};
return QVariant::fromValue( item->entry->i_downloads );
}
case Role::SCORE:
{
vlc_mutex_locker locker{&item->entry->lock};
return item->entry->i_score;
}
case Role::STATE:
{
vlc_mutex_locker locker{&item->entry->lock};
return item->entry->e_state;
}
case Role::ARTWORK:
{
vlc_mutex_locker locker{&item->entry->lock};
return item->artworkUrl;
}
case Role::TYPE:
{
vlc_mutex_locker locker{&item->entry->lock};
return QVariant::fromValue(item->entry->e_type);
}
case Role::LINK:
{
vlc_mutex_locker locker{&item->entry->lock};
return QString{ item->entry->psz_source_uri };
}
case Role::ADDON_VERSION:
{
vlc_mutex_locker locker{&item->entry->lock};
return qfu(item->entry->psz_version);
}
case Role::UUID:
return item->uuid;
case Role::DOWNLOAD_COUNT:
{
vlc_mutex_locker locker{&item->entry->lock};
return QVariant::fromValue(item->entry->i_downloads);
}
case Role::FILENAME:
{
vlc_mutex_locker locker{&item->entry->lock};
QList<QString> list;
addon_file_t *p_file;
ARRAY_FOREACH( p_file, item->entry->files )
list << qfu( p_file->psz_filename );
return QVariant{ list };
}
case Role::BROKEN:
{
vlc_mutex_locker locker{&item->entry->lock};
return item->entry->e_flags & ADDON_BROKEN;
}
case Role::MANAGEABLE:
{
vlc_mutex_locker locker{&item->entry->lock};
return item->entry->e_flags & ADDON_MANAGEABLE;
}
case Role::UPDATABLE:
{
vlc_mutex_locker locker{&item->entry->lock};
return item->entry->e_flags & ADDON_UPDATABLE;
}
case Qt::ToolTipRole:
{
vlc_mutex_locker locker{&item->entry->lock};
if ( !( item->entry->e_flags & ADDON_MANAGEABLE ) )
{
return qtr("This addon has been installed manually. VLC can't manage it by itself.");
}
return QVariant{};
}
case Qt::DecorationRole:
return QPixmap{QQmlFile::urlToLocalFileOrQrc(item->artworkUrl)};
default:
return {};
}
}
bool AddonsModel::setData(const QModelIndex& index, const QVariant &value, int role)
{
if ( role == Role::STATE )
{
auto i_value = value.value<AddonsModel::State>();
if ( i_value == AddonsModel::State::STATE_INSTALLING )
{
installService(index.row());
}
else if ( i_value == AddonsModel::State::STATE_UNINSTALLING )
{
removeService(index.row());
}
}
return true;
}
Qt::ItemFlags AddonsModel::flags( const QModelIndex &index ) const
{
Q_D(const AddonsModel);
Qt::ItemFlags qtFlags = BaseModel::flags(index);
const AddonItem* item = d->getItemForRow(index.row());
if (!item)
return qtFlags;
{
vlc_mutex_locker locker{&item->entry->lock};
addon_state_t addonState = item->entry->e_state;
if (addonState == ADDON_INSTALLING || addonState == ADDON_UNINSTALLING)
qtFlags &= ~Qt::ItemIsEnabled;
}
qtFlags |= Qt::ItemIsEditable;
return qtFlags;
}
QHash<int, QByteArray> AddonsModel::roleNames() const
{
return {
{ Role::NAME, "name" },
{ Role::AUTHOR, "author"},
{ Role::SUMMARY, "summary" },
{ Role::DESCRIPTION, "description" },
{ Role::DOWNLOADS, "downloads" },
{ Role::SCORE, "score" },
{ Role::STATE, "state" },
{ Role::ARTWORK, "artwork" },
{ Role::TYPE, "type" },
{ Role::LINK, "link" },
{ Role::FILENAME, "filename" },
{ Role::ADDON_VERSION, "version" },
{ Role::UUID, "uuid" },
{ Role::DOWNLOAD_COUNT, "downloadCount" },
{ Role::BROKEN, "broken" },
{ Role::MANAGEABLE, "manageable" },
{ Role::UPDATABLE, "updatable" },
};
}
void AddonsModel::installService(int idx)
{
Q_D(AddonsModel);
const AddonItem* item = d->getItemForRow(idx);
if (!item)
return;
addon_uuid_t uuid;
assert(sizeof(uuid) == item->uuid.size());
memcpy( uuid, item->uuid.constData(), sizeof( uuid ) );
addons_manager_Install( d->m_manager, uuid );
}
void AddonsModel::removeService(int idx)
{
Q_D(AddonsModel);
const AddonItem* item = d->getItemForRow(idx);
if (!item)
return;
addon_uuid_t uuid;
assert(sizeof(uuid) == item->uuid.size());
memcpy( uuid, item->uuid.constData(), sizeof( uuid ) );
addons_manager_Remove( d->m_manager, uuid );
}
void AddonsModel::loadFromDefaultRepository()
{
Q_D(AddonsModel);
d->gatherRepository("repo://");
}
void AddonsModel::loadFromExternalRepository(QUrl uri)
{
Q_D(AddonsModel);
d->gatherRepository(uri.toEncoded().constData());
}
void AddonsModel::setCtx(MainCtx* ctx)
{
Q_D(AddonsModel);
if (ctx == d->m_ctx)
return;
assert(ctx);
d->m_ctx = ctx;
d->initializeModel();
emit ctxChanged();
}
MainCtx* AddonsModel::getCtx() const
{
Q_D(const AddonsModel);
return d->m_ctx;
}
AddonsModel::Type AddonsModel::getTypeFilter() const
{
Q_D(const AddonsModel);
return d->m_typeFilter;
}
void AddonsModel::setTypeFilter(AddonsModel::Type type)
{
Q_D(AddonsModel);
if (type == d->m_typeFilter)
return;
d->m_typeFilter = type;
d->m_revision++;
invalidateCache();
emit typeFilterChanged();
}
AddonsModel::State AddonsModel::getStateFilter() const
{
Q_D(const AddonsModel);
return d->m_stateFilter;
}
void AddonsModel::setStateFilter(AddonsModel::State state)
{
Q_D(AddonsModel);
if (state == d->m_stateFilter)
return;
d->m_stateFilter = state;
d->m_revision++;
invalidateCache();
emit stateFilterChanged();
}
QString AddonsModel::getLabelForType(AddonsModel::Type type)
{
switch ( type )
{
case Type::TYPE_SKIN2 :
return qtr( "Skins" );
case Type::TYPE_PLAYLIST_PARSER:
return qtr("Playlist parsers");
case Type::TYPE_SERVICE_DISCOVERY:
return qtr("Service Discovery");
case Type::TYPE_INTERFACE:
return qtr("Interfaces");
case Type::TYPE_META:
return qtr("Art and meta fetchers");
case Type::TYPE_EXTENSION:
return qtr("Extensions");
default:
return qtr("Unknown");
}
}
QColor AddonsModel::getColorForType(AddonsModel::Type type)
{
QColor color;
switch( type )
{
case Type::TYPE_EXTENSION:
color = QColor(0xDB, 0xC5, 0x40);
break;
case Type::TYPE_PLAYLIST_PARSER:
color = QColor(0x36, 0xBB, 0x59);
break;
case Type::TYPE_SERVICE_DISCOVERY:
color = QColor(0xDB, 0x52, 0x40);
break;
case Type::TYPE_SKIN2:
color = QColor(0x8B, 0xD6, 0xFC);
break;
case Type::TYPE_INTERFACE:
color = QColor(0x00, 0x13, 0x85);
break;
case Type::TYPE_META:
color = QColor(0xCD, 0x23, 0xBF);
break;
case Type::TYPE_PLUGIN:
case Type::TYPE_UNKNOWN:
case Type::TYPE_OTHER:
default:
break;
}
return color;
}
QString AddonsModel::getIconForType(AddonsModel::Type type)
{
switch( type )
{
case AddonsModel::Type::TYPE_EXTENSION:
return QStringLiteral("qrc:///addons/addon_yellow.svg");
case AddonsModel::Type::TYPE_PLAYLIST_PARSER:
return QStringLiteral("qrc:///addons/addon_green.svg");
case AddonsModel::Type::TYPE_SERVICE_DISCOVERY:
return QStringLiteral("qrc:///addons/addon_red.svg");
case AddonsModel::Type::TYPE_SKIN2:
return QStringLiteral("qrc:///addons/addon_cyan.svg");
case AddonsModel::Type::TYPE_INTERFACE:
return QStringLiteral("qrc:///addons/addon_blue.svg");
case AddonsModel::Type::TYPE_META:
return QStringLiteral("qrc:///addons/addon_magenta.svg");
default:
return QStringLiteral("qrc:///addons/addon_default.svg");
}
vlc_assert_unreachable();
}
int AddonsModel::getMaxScore()
{
return ADDON_MAX_SCORE;
}
static void addonFoundCallback( addons_manager_t *manager, addon_entry_t *entry )
{
AddonsModelPrivate* d = (AddonsModelPrivate*) manager->owner.sys;
QMetaObject::invokeMethod( d->q_func(), [d, entryPtr = AddonPtr(entry)]()
{
d->addonFound( std::move( entryPtr ) );
}, Qt::QueuedConnection);
}
static void addonsDiscoveryEndedCallback( addons_manager_t *manager )
{
AddonsModelPrivate* d = (AddonsModelPrivate*) manager->owner.sys;
QMetaObject::invokeMethod( d->q_func(), [d]()
{
d->discoveryEnded();
}, Qt::QueuedConnection);
}
static void addonChangedCallback( addons_manager_t *manager, addon_entry_t *entry )
{
AddonsModelPrivate* d = (AddonsModelPrivate*) manager->owner.sys;
QMetaObject::invokeMethod( d->q_func(), [d, entryPtr = AddonPtr(entry)]()
{
d->addonChanged( std::move( entryPtr ) );
}, Qt::QueuedConnection);
}
bool AddonsModelPrivate::initializeModel()
{
Q_Q(AddonsModel);
if (m_qmlInitializing || !m_ctx)
return false;
if ( m_manager )
addons_manager_Delete( m_manager );
struct addons_manager_owner owner =
{
this,
addonFoundCallback,
addonsDiscoveryEndedCallback,
addonChangedCallback,
};
m_manager = addons_manager_New( VLC_OBJECT( m_ctx->getIntf() ), &owner );
assert( m_manager );
emit q->loadingChanged();
addons_manager_LoadCatalog( m_manager );
m_revision++;
m_loading = false;
emit q->loadingChanged();
return true;
}
void AddonsModelPrivate::addonFound( AddonPtr addon )
{
m_items.emplace_back(std::make_shared<AddonItem>(addon));
}
void AddonsModelPrivate::addonChanged( AddonPtr addon )
{
AddonItemPtr sdItem = std::make_shared<AddonItem>(addon);
m_cache->updateItem(std::move(sdItem));
m_revision++;
invalidateCache();
}
void AddonsModelPrivate::discoveryEnded()
{
Q_Q(AddonsModel);
assert( m_loading );
m_loading = false;
emit q->loadingChanged();
m_revision++;
invalidateCache();
}