mirror of
https://github.com/OpenMW/openmw.git
synced 2025-06-19 08:41:35 +00:00
Preliminary asset reloading
This commit is contained in:
parent
b73ed5ccac
commit
d31ed83b54
19 changed files with 123 additions and 17 deletions
|
@ -269,11 +269,11 @@ void CSMDoc::Document::createBase()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CSMDoc::Document::Document (const VFS::Manager* vfs, const Files::ConfigurationManager& configuration,
|
CSMDoc::Document::Document (VFS::Manager* vfs, const Files::ConfigurationManager& configuration,
|
||||||
const std::vector< boost::filesystem::path >& files, bool new_,
|
const std::vector< boost::filesystem::path >& files, bool new_,
|
||||||
const boost::filesystem::path& savePath, const boost::filesystem::path& resDir,
|
const boost::filesystem::path& savePath, const boost::filesystem::path& resDir,
|
||||||
const Fallback::Map* fallback,
|
const Fallback::Map* fallback,
|
||||||
ToUTF8::FromType encoding, const CSMWorld::ResourcesManager& resourcesManager,
|
ToUTF8::FromType encoding, CSMWorld::ResourcesManager& resourcesManager,
|
||||||
const std::vector<std::string>& blacklistedScripts)
|
const std::vector<std::string>& blacklistedScripts)
|
||||||
: mVFS(vfs), mSavePath (savePath), mContentFiles (files), mNew (new_), mData (encoding, resourcesManager, fallback, resDir),
|
: mVFS(vfs), mSavePath (savePath), mContentFiles (files), mNew (new_), mData (encoding, resourcesManager, fallback, resDir),
|
||||||
mTools (*this, encoding),
|
mTools (*this, encoding),
|
||||||
|
|
|
@ -59,7 +59,7 @@ namespace CSMDoc
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
const VFS::Manager* mVFS;
|
VFS::Manager* mVFS;
|
||||||
boost::filesystem::path mSavePath;
|
boost::filesystem::path mSavePath;
|
||||||
std::vector<boost::filesystem::path> mContentFiles;
|
std::vector<boost::filesystem::path> mContentFiles;
|
||||||
bool mNew;
|
bool mNew;
|
||||||
|
@ -102,11 +102,11 @@ namespace CSMDoc
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
Document (const VFS::Manager* vfs, const Files::ConfigurationManager& configuration,
|
Document (VFS::Manager* vfs, const Files::ConfigurationManager& configuration,
|
||||||
const std::vector< boost::filesystem::path >& files, bool new_,
|
const std::vector< boost::filesystem::path >& files, bool new_,
|
||||||
const boost::filesystem::path& savePath, const boost::filesystem::path& resDir,
|
const boost::filesystem::path& savePath, const boost::filesystem::path& resDir,
|
||||||
const Fallback::Map* fallback,
|
const Fallback::Map* fallback,
|
||||||
ToUTF8::FromType encoding, const CSMWorld::ResourcesManager& resourcesManager,
|
ToUTF8::FromType encoding, CSMWorld::ResourcesManager& resourcesManager,
|
||||||
const std::vector<std::string>& blacklistedScripts);
|
const std::vector<std::string>& blacklistedScripts);
|
||||||
|
|
||||||
~Document();
|
~Document();
|
||||||
|
|
|
@ -127,7 +127,7 @@ void CSMDoc::DocumentManager::documentNotLoaded (Document *document, const std::
|
||||||
removeDocument (document);
|
removeDocument (document);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CSMDoc::DocumentManager::setVFS(const VFS::Manager *vfs)
|
void CSMDoc::DocumentManager::setVFS(VFS::Manager *vfs)
|
||||||
{
|
{
|
||||||
mResourcesManager.setVFS(vfs);
|
mResourcesManager.setVFS(vfs);
|
||||||
mVFS = vfs;
|
mVFS = vfs;
|
||||||
|
|
|
@ -41,7 +41,7 @@ namespace CSMDoc
|
||||||
ToUTF8::FromType mEncoding;
|
ToUTF8::FromType mEncoding;
|
||||||
CSMWorld::ResourcesManager mResourcesManager;
|
CSMWorld::ResourcesManager mResourcesManager;
|
||||||
std::vector<std::string> mBlacklistedScripts;
|
std::vector<std::string> mBlacklistedScripts;
|
||||||
const VFS::Manager* mVFS;
|
VFS::Manager* mVFS;
|
||||||
|
|
||||||
DocumentManager (const DocumentManager&);
|
DocumentManager (const DocumentManager&);
|
||||||
DocumentManager& operator= (const DocumentManager&);
|
DocumentManager& operator= (const DocumentManager&);
|
||||||
|
@ -74,7 +74,7 @@ namespace CSMDoc
|
||||||
|
|
||||||
void setBlacklistedScripts (const std::vector<std::string>& scriptIds);
|
void setBlacklistedScripts (const std::vector<std::string>& scriptIds);
|
||||||
|
|
||||||
void setVFS(const VFS::Manager* vfs);
|
void setVFS(VFS::Manager* vfs);
|
||||||
|
|
||||||
bool isEmpty();
|
bool isEmpty();
|
||||||
|
|
||||||
|
|
|
@ -259,6 +259,7 @@ void CSMPrefs::State::declare()
|
||||||
declareShortcut ("document-character-topicinfos", "Open Topic Info List", QKeySequence());
|
declareShortcut ("document-character-topicinfos", "Open Topic Info List", QKeySequence());
|
||||||
declareShortcut ("document-character-journalinfos", "Open Journal Info List", QKeySequence());
|
declareShortcut ("document-character-journalinfos", "Open Journal Info List", QKeySequence());
|
||||||
declareShortcut ("document-character-bodyparts", "Open Body Part List", QKeySequence());
|
declareShortcut ("document-character-bodyparts", "Open Body Part List", QKeySequence());
|
||||||
|
declareShortcut ("document-assets-reload", "Reload Assets", QKeySequence(Qt::Key_F5));
|
||||||
declareShortcut ("document-assets-sounds", "Open Sound Asset List", QKeySequence());
|
declareShortcut ("document-assets-sounds", "Open Sound Asset List", QKeySequence());
|
||||||
declareShortcut ("document-assets-soundgens", "Open Sound Generator List", QKeySequence());
|
declareShortcut ("document-assets-soundgens", "Open Sound Generator List", QKeySequence());
|
||||||
declareShortcut ("document-assets-meshes", "Open Mesh Asset List", QKeySequence());
|
declareShortcut ("document-assets-meshes", "Open Mesh Asset List", QKeySequence());
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
#include <components/esm/cellref.hpp>
|
#include <components/esm/cellref.hpp>
|
||||||
|
|
||||||
#include <components/resource/scenemanager.hpp>
|
#include <components/resource/scenemanager.hpp>
|
||||||
|
#include <components/vfs/manager.hpp>
|
||||||
|
|
||||||
#include "idtable.hpp"
|
#include "idtable.hpp"
|
||||||
#include "idtree.hpp"
|
#include "idtree.hpp"
|
||||||
|
@ -61,7 +62,7 @@ int CSMWorld::Data::count (RecordBase::State state, const CollectionBase& collec
|
||||||
return number;
|
return number;
|
||||||
}
|
}
|
||||||
|
|
||||||
CSMWorld::Data::Data (ToUTF8::FromType encoding, const ResourcesManager& resourcesManager, const Fallback::Map* fallback, const boost::filesystem::path& resDir)
|
CSMWorld::Data::Data (ToUTF8::FromType encoding, ResourcesManager& resourcesManager, const Fallback::Map* fallback, const boost::filesystem::path& resDir)
|
||||||
: mEncoder (encoding), mPathgrids (mCells), mRefs (mCells),
|
: mEncoder (encoding), mPathgrids (mCells), mRefs (mCells),
|
||||||
mResourcesManager (resourcesManager), mFallbackMap(fallback),
|
mResourcesManager (resourcesManager), mFallbackMap(fallback),
|
||||||
mReader (0), mDialogue (0), mReaderIndex(1), mResourceSystem(new Resource::ResourceSystem(resourcesManager.getVFS()))
|
mReader (0), mDialogue (0), mReaderIndex(1), mResourceSystem(new Resource::ResourceSystem(resourcesManager.getVFS()))
|
||||||
|
@ -1215,6 +1216,36 @@ std::vector<std::string> CSMWorld::Data::getIds (bool listDeleted) const
|
||||||
return ids;
|
return ids;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CSMWorld::Data::assetsChanged()
|
||||||
|
{
|
||||||
|
VFS::Manager* vfs = mResourcesManager.getVFS();
|
||||||
|
vfs->rebuildIndex();
|
||||||
|
|
||||||
|
ResourceTable* meshTable = static_cast<ResourceTable*>(getTableModel(UniversalId::Type_Meshes));
|
||||||
|
ResourceTable* iconTable = static_cast<ResourceTable*>(getTableModel(UniversalId::Type_Icons));
|
||||||
|
ResourceTable* musicTable = static_cast<ResourceTable*>(getTableModel(UniversalId::Type_Musics));
|
||||||
|
ResourceTable* soundResTable = static_cast<ResourceTable*>(getTableModel(UniversalId::Type_SoundsRes));
|
||||||
|
ResourceTable* texTable = static_cast<ResourceTable*>(getTableModel(UniversalId::Type_Textures));
|
||||||
|
ResourceTable* vidTable = static_cast<ResourceTable*>(getTableModel(UniversalId::Type_Videos));
|
||||||
|
|
||||||
|
meshTable->beginReset();
|
||||||
|
iconTable->beginReset();
|
||||||
|
musicTable->beginReset();
|
||||||
|
soundResTable->beginReset();
|
||||||
|
texTable->beginReset();
|
||||||
|
vidTable->beginReset();
|
||||||
|
|
||||||
|
// Trigger recreation
|
||||||
|
mResourcesManager.recreateResources();
|
||||||
|
|
||||||
|
meshTable->endReset();
|
||||||
|
iconTable->endReset();
|
||||||
|
musicTable->endReset();
|
||||||
|
soundResTable->endReset();
|
||||||
|
texTable->endReset();
|
||||||
|
vidTable->endReset();
|
||||||
|
}
|
||||||
|
|
||||||
void CSMWorld::Data::dataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight)
|
void CSMWorld::Data::dataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight)
|
||||||
{
|
{
|
||||||
if (topLeft.column()<=0)
|
if (topLeft.column()<=0)
|
||||||
|
|
|
@ -108,7 +108,7 @@ namespace CSMWorld
|
||||||
RefCollection mRefs;
|
RefCollection mRefs;
|
||||||
IdCollection<ESM::Filter> mFilters;
|
IdCollection<ESM::Filter> mFilters;
|
||||||
Collection<MetaData> mMetaData;
|
Collection<MetaData> mMetaData;
|
||||||
const ResourcesManager& mResourcesManager;
|
ResourcesManager& mResourcesManager;
|
||||||
const Fallback::Map* mFallbackMap;
|
const Fallback::Map* mFallbackMap;
|
||||||
std::vector<QAbstractItemModel *> mModels;
|
std::vector<QAbstractItemModel *> mModels;
|
||||||
std::map<UniversalId::Type, QAbstractItemModel *> mModelIndex;
|
std::map<UniversalId::Type, QAbstractItemModel *> mModelIndex;
|
||||||
|
@ -140,7 +140,7 @@ namespace CSMWorld
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
Data (ToUTF8::FromType encoding, const ResourcesManager& resourcesManager, const Fallback::Map* fallback, const boost::filesystem::path& resDir);
|
Data (ToUTF8::FromType encoding, ResourcesManager& resourcesManager, const Fallback::Map* fallback, const boost::filesystem::path& resDir);
|
||||||
|
|
||||||
virtual ~Data();
|
virtual ~Data();
|
||||||
|
|
||||||
|
@ -306,6 +306,8 @@ namespace CSMWorld
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
|
|
||||||
|
void assetsChanged();
|
||||||
|
|
||||||
void dataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight);
|
void dataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight);
|
||||||
|
|
||||||
void rowsChanged (const QModelIndex& parent, int start, int end);
|
void rowsChanged (const QModelIndex& parent, int start, int end);
|
||||||
|
|
|
@ -12,6 +12,14 @@ CSMWorld::Resources::Resources (const VFS::Manager* vfs, const std::string& base
|
||||||
const char * const *extensions)
|
const char * const *extensions)
|
||||||
: mBaseDirectory (baseDirectory), mType (type)
|
: mBaseDirectory (baseDirectory), mType (type)
|
||||||
{
|
{
|
||||||
|
recreate(vfs, extensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSMWorld::Resources::recreate(const VFS::Manager* vfs, const char * const *extensions)
|
||||||
|
{
|
||||||
|
mFiles.clear();
|
||||||
|
mIndex.clear();
|
||||||
|
|
||||||
int baseSize = mBaseDirectory.size();
|
int baseSize = mBaseDirectory.size();
|
||||||
|
|
||||||
const std::map<std::string, VFS::File*>& index = vfs->getIndex();
|
const std::map<std::string, VFS::File*>& index = vfs->getIndex();
|
||||||
|
|
|
@ -27,6 +27,8 @@ namespace CSMWorld
|
||||||
Resources (const VFS::Manager* vfs, const std::string& baseDirectory, UniversalId::Type type,
|
Resources (const VFS::Manager* vfs, const std::string& baseDirectory, UniversalId::Type type,
|
||||||
const char * const *extensions = 0);
|
const char * const *extensions = 0);
|
||||||
|
|
||||||
|
void recreate(const VFS::Manager* vfs, const char * const *extensions = 0);
|
||||||
|
|
||||||
int getSize() const;
|
int getSize() const;
|
||||||
|
|
||||||
std::string getId (int index) const;
|
std::string getId (int index) const;
|
||||||
|
|
|
@ -14,7 +14,7 @@ void CSMWorld::ResourcesManager::addResources (const Resources& resources)
|
||||||
resources));
|
resources));
|
||||||
}
|
}
|
||||||
|
|
||||||
void CSMWorld::ResourcesManager::setVFS(const VFS::Manager *vfs)
|
void CSMWorld::ResourcesManager::setVFS(VFS::Manager *vfs)
|
||||||
{
|
{
|
||||||
mVFS = vfs;
|
mVFS = vfs;
|
||||||
mResources.clear();
|
mResources.clear();
|
||||||
|
@ -31,11 +31,26 @@ void CSMWorld::ResourcesManager::setVFS(const VFS::Manager *vfs)
|
||||||
addResources (Resources (vfs, "videos", UniversalId::Type_Video));
|
addResources (Resources (vfs, "videos", UniversalId::Type_Video));
|
||||||
}
|
}
|
||||||
|
|
||||||
const VFS::Manager* CSMWorld::ResourcesManager::getVFS() const
|
VFS::Manager* CSMWorld::ResourcesManager::getVFS() const
|
||||||
{
|
{
|
||||||
return mVFS;
|
return mVFS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CSMWorld::ResourcesManager::recreateResources()
|
||||||
|
{
|
||||||
|
// TODO make this shared with setVFS function
|
||||||
|
static const char * const sMeshTypes[] = { "nif", "osg", "osgt", "osgb", "osgx", "osg2", 0 };
|
||||||
|
|
||||||
|
std::map<UniversalId::Type, Resources>::iterator it = mResources.begin();
|
||||||
|
for ( ; it != mResources.end(); ++it)
|
||||||
|
{
|
||||||
|
if (it->first == UniversalId::Type_Mesh)
|
||||||
|
it->second.recreate(mVFS, sMeshTypes);
|
||||||
|
else
|
||||||
|
it->second.recreate(mVFS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const CSMWorld::Resources& CSMWorld::ResourcesManager::get (UniversalId::Type type) const
|
const CSMWorld::Resources& CSMWorld::ResourcesManager::get (UniversalId::Type type) const
|
||||||
{
|
{
|
||||||
std::map<UniversalId::Type, Resources>::const_iterator iter = mResources.find (type);
|
std::map<UniversalId::Type, Resources>::const_iterator iter = mResources.find (type);
|
||||||
|
|
|
@ -16,7 +16,7 @@ namespace CSMWorld
|
||||||
class ResourcesManager
|
class ResourcesManager
|
||||||
{
|
{
|
||||||
std::map<UniversalId::Type, Resources> mResources;
|
std::map<UniversalId::Type, Resources> mResources;
|
||||||
const VFS::Manager* mVFS;
|
VFS::Manager* mVFS;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
|
@ -26,9 +26,11 @@ namespace CSMWorld
|
||||||
|
|
||||||
ResourcesManager();
|
ResourcesManager();
|
||||||
|
|
||||||
const VFS::Manager* getVFS() const;
|
VFS::Manager* getVFS() const;
|
||||||
|
|
||||||
void setVFS(const VFS::Manager* vfs);
|
void setVFS(VFS::Manager* vfs);
|
||||||
|
|
||||||
|
void recreateResources();
|
||||||
|
|
||||||
const Resources& get (UniversalId::Type type) const;
|
const Resources& get (UniversalId::Type type) const;
|
||||||
};
|
};
|
||||||
|
|
|
@ -154,3 +154,13 @@ int CSMWorld::ResourceTable::getColumnId (int column) const
|
||||||
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CSMWorld::ResourceTable::beginReset()
|
||||||
|
{
|
||||||
|
beginResetModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSMWorld::ResourceTable::endReset()
|
||||||
|
{
|
||||||
|
endResetModel();
|
||||||
|
}
|
||||||
|
|
|
@ -53,6 +53,11 @@ namespace CSMWorld
|
||||||
virtual bool isDeleted (const std::string& id) const;
|
virtual bool isDeleted (const std::string& id) const;
|
||||||
|
|
||||||
virtual int getColumnId (int column) const;
|
virtual int getColumnId (int column) const;
|
||||||
|
|
||||||
|
/// Signal Qt that the data is about to change.
|
||||||
|
void beginReset();
|
||||||
|
/// Signal Qt that the data has been changed.
|
||||||
|
void endReset();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -284,6 +284,13 @@ void CSVDoc::View::setupAssetsMenu()
|
||||||
{
|
{
|
||||||
QMenu *assets = menuBar()->addMenu (tr ("Assets"));
|
QMenu *assets = menuBar()->addMenu (tr ("Assets"));
|
||||||
|
|
||||||
|
QAction *reload = new QAction (tr ("Reload"), this);
|
||||||
|
connect (reload, SIGNAL (triggered()), &mDocument->getData(), SLOT (assetsChanged()));
|
||||||
|
setupShortcut("document-assets-reload", reload);
|
||||||
|
assets->addAction (reload);
|
||||||
|
|
||||||
|
assets->addSeparator();
|
||||||
|
|
||||||
QAction *sounds = new QAction (tr ("Sounds"), this);
|
QAction *sounds = new QAction (tr ("Sounds"), this);
|
||||||
connect (sounds, SIGNAL (triggered()), this, SLOT (addSoundsSubView()));
|
connect (sounds, SIGNAL (triggered()), this, SLOT (addSoundsSubView()));
|
||||||
setupShortcut("document-assets-sounds", sounds);
|
setupShortcut("document-assets-sounds", sounds);
|
||||||
|
@ -889,6 +896,7 @@ void CSVDoc::View::addMetaDataSubView()
|
||||||
addSubView (CSMWorld::UniversalId (CSMWorld::UniversalId::Type_MetaData, "sys::meta"));
|
addSubView (CSMWorld::UniversalId (CSMWorld::UniversalId::Type_MetaData, "sys::meta"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void CSVDoc::View::abortOperation (int type)
|
void CSVDoc::View::abortOperation (int type)
|
||||||
{
|
{
|
||||||
mDocument->abortOperation (type);
|
mDocument->abortOperation (type);
|
||||||
|
|
|
@ -21,6 +21,9 @@ namespace VFS
|
||||||
public:
|
public:
|
||||||
virtual ~Archive() {}
|
virtual ~Archive() {}
|
||||||
|
|
||||||
|
/// Clears cached data for archives that may change.
|
||||||
|
virtual void resetIfNotStatic(){};
|
||||||
|
|
||||||
/// List all resources contained in this archive, and run the resource names through the given normalize function.
|
/// List all resources contained in this archive, and run the resource names through the given normalize function.
|
||||||
virtual void listResources(std::map<std::string, File*>& out, char (*normalize_function) (char)) = 0;
|
virtual void listResources(std::map<std::string, File*>& out, char (*normalize_function) (char)) = 0;
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,6 +12,12 @@ namespace VFS
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void FileSystemArchive::resetIfNotStatic()
|
||||||
|
{
|
||||||
|
mIndex.clear();
|
||||||
|
mBuiltIndex = false;
|
||||||
|
}
|
||||||
|
|
||||||
void FileSystemArchive::listResources(std::map<std::string, File *> &out, char (*normalize_function)(char))
|
void FileSystemArchive::listResources(std::map<std::string, File *> &out, char (*normalize_function)(char))
|
||||||
{
|
{
|
||||||
if (!mBuiltIndex)
|
if (!mBuiltIndex)
|
||||||
|
|
|
@ -23,6 +23,8 @@ namespace VFS
|
||||||
public:
|
public:
|
||||||
FileSystemArchive(const std::string& path);
|
FileSystemArchive(const std::string& path);
|
||||||
|
|
||||||
|
virtual void resetIfNotStatic();
|
||||||
|
|
||||||
virtual void listResources(std::map<std::string, File*>& out, char (*normalize_function) (char));
|
virtual void listResources(std::map<std::string, File*>& out, char (*normalize_function) (char));
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,14 @@ namespace VFS
|
||||||
(*it)->listResources(mIndex, mStrict ? &strict_normalize_char : &nonstrict_normalize_char);
|
(*it)->listResources(mIndex, mStrict ? &strict_normalize_char : &nonstrict_normalize_char);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Manager::rebuildIndex()
|
||||||
|
{
|
||||||
|
for (std::vector<Archive*>::const_iterator it = mArchives.begin(); it != mArchives.end(); ++it)
|
||||||
|
(*it)->resetIfNotStatic();
|
||||||
|
|
||||||
|
buildIndex();
|
||||||
|
}
|
||||||
|
|
||||||
Files::IStreamPtr Manager::get(const std::string &name) const
|
Files::IStreamPtr Manager::get(const std::string &name) const
|
||||||
{
|
{
|
||||||
std::string normalized = name;
|
std::string normalized = name;
|
||||||
|
|
|
@ -33,6 +33,9 @@ namespace VFS
|
||||||
/// Build the file index. Should be called when all archives have been registered.
|
/// Build the file index. Should be called when all archives have been registered.
|
||||||
void buildIndex();
|
void buildIndex();
|
||||||
|
|
||||||
|
/// Rebuild the file index. New/deleted files (actual files, not bsa's) will be reflected.
|
||||||
|
void rebuildIndex();
|
||||||
|
|
||||||
/// Does a file with this name exist?
|
/// Does a file with this name exist?
|
||||||
/// @note May be called from any thread once the index has been built.
|
/// @note May be called from any thread once the index has been built.
|
||||||
bool exists(const std::string& name) const;
|
bool exists(const std::string& name) const;
|
||||||
|
|
Loading…
Reference in a new issue