Lots of fixes, VFS now seems to work reliably. Still a lot of cleanup and shoring to do.
Decided to leave the VFS tab for now. Filtering likely doesn't work, except Show Only Open. Sorting works. Rewrites to replace dynamicRoles done for Mods and Plugins tables. Database.qml now reads "moddir", though installing extracted is still unimplemented so far. Launch still doesn't work. Ignore the "file -> test extract". It's a test for file extraction.
This commit is contained in:
parent
4bd0e980f3
commit
191cd5362a
21 changed files with 853 additions and 461 deletions
|
|
@ -144,11 +144,10 @@ QList< QPair< int, QString > > FuseAccessorArchive::activeArchives(QSqlDatabase
|
|||
|
||||
int FuseAccessorArchive::makeWritable(struct fuse_file_info *fi, const QString &origsource, const QString &path, const QByteArray &fullData, QIODeviceBase::OpenMode mode, const QByteArray &data, off_t offset)
|
||||
{
|
||||
FuseFHBase *fh = m_parent->makeWritableFromData(fullData, origsource, path, mode);
|
||||
FuseFHBase *fh = m_parent->makeWritableFromData(fi, fullData, origsource, path, mode);
|
||||
if( !fh )
|
||||
return -1;
|
||||
|
||||
fi->fh = reinterpret_cast<quint64>(fh);
|
||||
return fh->write(data, offset, fi);
|
||||
}
|
||||
|
||||
|
|
@ -223,7 +222,7 @@ int FuseAccessorArchive::getattr(const QString &path, struct stat *stbuf)
|
|||
}
|
||||
}
|
||||
|
||||
//qDebug() << "Not found.";
|
||||
//qDebug() << "Not found:" << path;
|
||||
m_statCache[path] = NULL;
|
||||
return -ENOENT;
|
||||
}
|
||||
|
|
@ -248,6 +247,12 @@ QStringList FuseAccessorArchive::readdir(const QString &path)
|
|||
if( !mapping.m_path.startsWith(lpath) )
|
||||
continue;
|
||||
|
||||
/* Avoid cases where a dir and filename share a starting:
|
||||
* eg: Data/f4ee, Data/f4ee.ini, etc.
|
||||
*/
|
||||
if( QDir::separator() != mapping.m_path.at(lpath.length()) )
|
||||
continue;
|
||||
|
||||
QStringList parts = mapping.m_path.mid(lplen).toLower().split( QDir::separator(), Qt::SkipEmptyParts );
|
||||
QString nent = parts.at(0);
|
||||
if( results.contains(nent) )
|
||||
|
|
@ -353,7 +358,7 @@ bool FuseAccessorArchive::slurpData(const QString &path, const QString &origpath
|
|||
m_mutex.unlock();
|
||||
|
||||
qDebug() << "Caching" << path << "which is" << data.length() << "bytes.";
|
||||
m_parent->cacheInsert(origpath, data);
|
||||
m_parent->cacheInsert(this, origpath, data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -362,7 +367,7 @@ QSharedPointer<QByteArray> FuseAccessorArchive::cacheGrab(const QString &path)
|
|||
{
|
||||
if(QSharedPointer<QByteArray> sptr = m_parent->cacheGrab(path))
|
||||
return sptr;
|
||||
return QSharedPointer<QByteArray>();
|
||||
return {};
|
||||
}
|
||||
|
||||
FuseFHArchive::FuseFHArchive(FuseAccessorBase *parent, const QString &path, const QString &newpath)
|
||||
|
|
@ -436,6 +441,7 @@ int FuseFHArchive::write(const QByteArray &data, off_t offset, struct fuse_file_
|
|||
QByteArray dcopy(entry->constData(), entry->length());
|
||||
int rval = accessor->makeWritable(fi, m_path, m_newpath, dcopy, m_mode, data, offset);
|
||||
release();
|
||||
qDebug() << "FuseFHArchive::write: is now writable. So long, pardner!" << rval;
|
||||
deleteLater();
|
||||
return rval;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ public:
|
|||
virtual ~FuseFHBase() {}
|
||||
|
||||
FuseAccessorBase *getAccessor() { return m_parent; }
|
||||
virtual const QString getResource() { return m_parent->getResource(); }
|
||||
const QString &getPath() { return m_path; }
|
||||
|
||||
virtual bool open(QIODeviceBase::OpenMode mode) = 0;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@
|
|||
#include <string.h>
|
||||
#include <sys/mount.h>
|
||||
|
||||
#include <QSqlError>
|
||||
#include <QSqlQuery>
|
||||
|
||||
#define CACHE_ENABLED true
|
||||
//#define FUSE_DEBUG 1
|
||||
|
||||
|
|
@ -125,6 +128,8 @@ int FuseInterface::pump()
|
|||
return ret;
|
||||
|
||||
fuse_session_process_buf(m_fuse_session, &fbuf);
|
||||
|
||||
free(fbuf.mem);
|
||||
} while(0 == ret);
|
||||
return ret;
|
||||
}
|
||||
|
|
@ -186,6 +191,8 @@ void FuseInterface::init()
|
|||
qDebug() << "Adding game data dir path:" << m_gameDataDir;
|
||||
append(vanilla);
|
||||
|
||||
readDeleted();
|
||||
|
||||
m_mutex.unlock();
|
||||
}
|
||||
|
||||
|
|
@ -197,6 +204,7 @@ void FuseInterface::clear()
|
|||
fab->deleteLater();
|
||||
}
|
||||
m_accessors.clear();
|
||||
m_deleted.clear();
|
||||
m_dircache.clear();
|
||||
for( struct stat *st : std::as_const(m_cache_stat) )
|
||||
free(st);
|
||||
|
|
@ -241,7 +249,22 @@ int FuseInterface::utilQPermsToMode(QFileDevice::Permissions mode)
|
|||
return smode;
|
||||
}
|
||||
*/
|
||||
FuseFHBase *FuseInterface::makeWritableFromSource(const QString &source, const QString &origsource, const QString &dest, QIODeviceBase::OpenMode mode)
|
||||
|
||||
/*
|
||||
* In FUSE3, the kernel treats fuse_file_info.fh as opaque state owned by the userspace daemon, but it’s
|
||||
* copied once at open time into the kernel’s internal struct file. After that, every subsequent operation
|
||||
* (read/write/release/etc.) gets a copy of that fh value from the kernel, not a live reference back into
|
||||
* userspace memory.
|
||||
*/
|
||||
FuseFHBase *FuseInterface::getFH(struct fuse_file_info *fi)
|
||||
{
|
||||
if( m_cow_redirects.contains(fi->fh) )
|
||||
return m_cow_redirects.value(fi->fh);
|
||||
|
||||
return reinterpret_cast<FuseFHBase *>(fi->fh);
|
||||
}
|
||||
|
||||
FuseFHBase *FuseInterface::makeWritableFromSource(fuse_file_info *fi, const QString &source, const QString &origsource, const QString &dest, QIODeviceBase::OpenMode mode)
|
||||
{
|
||||
m_mutex.lock();
|
||||
for( FuseAccessorBase *fab : std::as_const(m_accessors) )
|
||||
|
|
@ -251,6 +274,8 @@ FuseFHBase *FuseInterface::makeWritableFromSource(const QString &source, const Q
|
|||
|
||||
FuseAccessorSandbox *sandbox = reinterpret_cast<FuseAccessorSandbox *>(fab);
|
||||
FuseFHBase *fh = sandbox->makeWritableFromSource(source, origsource, dest, mode);
|
||||
m_cow_redirects.insert(fi->fh, fh);
|
||||
cacheInvalidate(dest);
|
||||
m_mutex.unlock();
|
||||
return fh;
|
||||
}
|
||||
|
|
@ -258,7 +283,7 @@ FuseFHBase *FuseInterface::makeWritableFromSource(const QString &source, const Q
|
|||
return NULL;
|
||||
}
|
||||
|
||||
FuseFHBase *FuseInterface::makeWritableFromData(const QByteArray &source, const QString &origsource, const QString &dest, QIODeviceBase::OpenMode mode)
|
||||
FuseFHBase *FuseInterface::makeWritableFromData(struct fuse_file_info *fi, const QByteArray &source, const QString &origsource, const QString &dest, QIODeviceBase::OpenMode mode)
|
||||
{
|
||||
m_mutex.lock();
|
||||
for( FuseAccessorBase *fab : std::as_const(m_accessors) )
|
||||
|
|
@ -268,11 +293,13 @@ FuseFHBase *FuseInterface::makeWritableFromData(const QByteArray &source, const
|
|||
|
||||
FuseAccessorSandbox *sandbox = reinterpret_cast<FuseAccessorSandbox *>(fab);
|
||||
FuseFHBase *fh = sandbox->makeWritableFromData(source, origsource, dest, mode);
|
||||
|
||||
m_cow_redirects.insert(fi->fh, fh);
|
||||
cacheInvalidate(dest);
|
||||
m_mutex.unlock();
|
||||
return fh;
|
||||
}
|
||||
m_mutex.unlock();
|
||||
qDebug() << "FuseInterface::makeWritableFromData: Couldn't do it.";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
@ -289,25 +316,101 @@ static QString humanSize(qint64 bytes)
|
|||
return QString::number(size, 'f', (i == 0 ? 0 : 2)) + ' ' + suffixes[i];
|
||||
}
|
||||
|
||||
void FuseInterface::cacheInsert(const QString &path, QByteArray &data)
|
||||
void FuseInterface::cacheInsert(FuseAccessorBase *fab, const QString &path, const QByteArray &data)
|
||||
{
|
||||
if( data.isEmpty() )
|
||||
if (data.isEmpty())
|
||||
return;
|
||||
|
||||
QSharedPointer<QByteArray> sharedData = QSharedPointer<QByteArray>::create(std::move(data));
|
||||
QSharedPointer<QByteArray> *val = new QSharedPointer<QByteArray>(std::move(sharedData));
|
||||
m_cache_data.insert(path, val, val->data()->size());
|
||||
QString qpath(path.toLower());
|
||||
auto val = QSharedPointer<QByteArray>::create(data);
|
||||
m_cache_data.insert(qpath, new QSharedPointer<QByteArray>(std::move(val)), val->size());
|
||||
qDebug() << " - m_cache_data now contains" << m_cache_data.size() << "entries, totalling" << humanSize(m_cache_data.totalCost()) << "bytes";
|
||||
|
||||
emit entryUpdated(path, "Archive", 1, 1);
|
||||
emit entryUpdated(qpath, fab->getType(), fab->getResource(), 1, 1);
|
||||
}
|
||||
|
||||
QSharedPointer<QByteArray> FuseInterface::cacheGrab(const QString &path)
|
||||
{
|
||||
if( !m_cache_data.contains(path) )
|
||||
if (auto ptr = m_cache_data.object(path.toLower()))
|
||||
return *ptr;
|
||||
return {};
|
||||
}
|
||||
|
||||
/*
|
||||
void FuseInterface::cacheInsert(FuseAccessorBase *fab, const QString &path, const QByteArray &data)
|
||||
{
|
||||
if( data.isEmpty() )
|
||||
return;
|
||||
|
||||
QString qpath(path.toLower());
|
||||
//QSharedPointer<QByteArray> val = QSharedPointer<QByteArray>::create(data);
|
||||
//m_cache_data.insert(qpath, new QSharedPointer<QByteArray>(val), val->size());
|
||||
m_cache_data.insert(qpath, new QByteArray(data), data.size());
|
||||
|
||||
qDebug() << " - m_cache_data now contains" << m_cache_data.size() << "entries, totalling" << humanSize(m_cache_data.totalCost()) << "bytes";
|
||||
|
||||
emit entryUpdated(qpath, fab->getType(), fab->getResource(), 1, 1);
|
||||
}
|
||||
|
||||
QByteArray FuseInterface::cacheGrab(const QString &path)
|
||||
{
|
||||
QString qpath(path.toLower());
|
||||
if( !m_cache_data.contains(qpath) )
|
||||
return {};
|
||||
auto *p = m_cache_data.object(path);
|
||||
return *p;
|
||||
|
||||
auto *data = m_cache_data.object(qpath);
|
||||
return data ? *data : QByteArray();
|
||||
}
|
||||
*/
|
||||
|
||||
void FuseInterface::cacheInvalidate(const QString &path)
|
||||
{
|
||||
QString qpath = path.toLower();
|
||||
//qDebug() << "FuseInterface::cacheInvalidate:" << path;
|
||||
|
||||
if( m_cache_data.contains(qpath) )
|
||||
m_cache_data.remove(qpath);
|
||||
|
||||
if( m_dircache.contains(qpath) )
|
||||
m_dircache.remove(qpath);
|
||||
|
||||
if( m_cache_stat.contains(qpath) ) {
|
||||
struct stat *st = m_cache_stat.take(qpath);
|
||||
free(st);
|
||||
}
|
||||
|
||||
if( m_cache_accessor.contains(qpath) )
|
||||
m_cache_accessor.remove(qpath);
|
||||
}
|
||||
|
||||
bool FuseInterface::markDeleted(const QString &path, bool isdeleted)
|
||||
{
|
||||
if( isdeleted )
|
||||
m_deleted.append(path.toLower());
|
||||
else
|
||||
m_deleted.removeAll(path.toLower());
|
||||
|
||||
QString qstr = QString("REPLACE INTO proxyOverrides(path, deleted)VALUES(?, %1)").arg(isdeleted ? 1 : 0);
|
||||
qDebug() << qstr << path;
|
||||
QSqlQuery q = QSqlQuery(qstr, m_database);
|
||||
q.bindValue(0, path);
|
||||
bool ret = q.exec();
|
||||
if( !ret )
|
||||
qDebug() << m_database.lastError();
|
||||
return ret;
|
||||
}
|
||||
|
||||
void FuseInterface::readDeleted()
|
||||
{
|
||||
m_deleted.clear();
|
||||
|
||||
QString qstr = QString("SELECT path, deleted FROM proxyOverrides WHERE deleted=1");
|
||||
qDebug() << qstr;
|
||||
QSqlQuery q = QSqlQuery(qstr, m_database);
|
||||
while( q.next() )
|
||||
{
|
||||
QString path = q.value(0).toString();
|
||||
m_deleted.append(path.toLower());
|
||||
}
|
||||
}
|
||||
|
||||
int FuseInterface::getattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi)
|
||||
|
|
@ -316,13 +419,17 @@ int FuseInterface::getattr(const char *path, struct stat *stbuf, struct fuse_fil
|
|||
|
||||
int ret;
|
||||
QString qpath(path);
|
||||
QString lpath = qpath.toLower();
|
||||
|
||||
if( CACHE_ENABLED && m_cache_stat.contains(qpath) )
|
||||
if( m_deleted.contains(lpath) )
|
||||
return -ENOENT;
|
||||
|
||||
if( CACHE_ENABLED && m_cache_stat.contains(lpath) )
|
||||
{
|
||||
if( !m_cache_stat[qpath] )
|
||||
if( !m_cache_stat[lpath] )
|
||||
return -ENOENT;
|
||||
|
||||
memcpy( stbuf, m_cache_stat[qpath], sizeof(struct stat) );
|
||||
memcpy( stbuf, m_cache_stat[lpath], sizeof(struct stat) );
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -332,13 +439,13 @@ int FuseInterface::getattr(const char *path, struct stat *stbuf, struct fuse_fil
|
|||
if( 0 == ret ) {
|
||||
struct stat *scache = (struct stat *)malloc(sizeof(struct stat));
|
||||
memcpy( scache, stbuf, sizeof(struct stat) );
|
||||
m_cache_stat.insert(qpath, scache);
|
||||
m_cache_accessor.insert(qpath, ent);
|
||||
m_cache_stat.insert(lpath, scache);
|
||||
//m_cache_accessor.insert(qpath, ent);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
m_cache_stat.insert(qpath, NULL);
|
||||
m_cache_stat.insert(lpath, NULL);
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
|
|
@ -362,16 +469,16 @@ int FuseInterface::create(const char *path, mode_t mode, struct fuse_file_info *
|
|||
break;
|
||||
}
|
||||
|
||||
if( CACHE_ENABLED && m_cache_stat.contains(qpath) )
|
||||
{
|
||||
struct stat *st = m_cache_stat.take(qpath);
|
||||
if( st )
|
||||
free(st);
|
||||
}
|
||||
if( CACHE_ENABLED )
|
||||
cacheInvalidate(qpath);
|
||||
|
||||
m_mutex.unlock();
|
||||
if( 0 == ret && sb )
|
||||
if( 0 == ret && sb ) {
|
||||
if( m_deleted.contains(qpath.toLower()) )
|
||||
markDeleted(qpath, false);
|
||||
|
||||
return open(path, fi);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
|
@ -393,16 +500,11 @@ int FuseInterface::mkdir(const char *path, mode_t mode)
|
|||
break;
|
||||
}
|
||||
|
||||
if( CACHE_ENABLED ) {
|
||||
if( m_cache_stat.contains(qpath) )
|
||||
{
|
||||
struct stat *st = m_cache_stat.take(qpath);
|
||||
if( st )
|
||||
free(st);
|
||||
}
|
||||
if( m_dircache.contains(qpath) )
|
||||
m_dircache.remove(qpath);
|
||||
}
|
||||
if( CACHE_ENABLED )
|
||||
cacheInvalidate(qpath);
|
||||
|
||||
if( 0 == ret && m_deleted.contains(qpath.toLower()) )
|
||||
markDeleted(qpath, false);
|
||||
|
||||
m_mutex.unlock();
|
||||
return ret;
|
||||
|
|
@ -410,12 +512,21 @@ int FuseInterface::mkdir(const char *path, mode_t mode)
|
|||
|
||||
int FuseInterface::unlink(const char *path)
|
||||
{
|
||||
int ret = 1;
|
||||
bool isFound = false;
|
||||
int ret = -ENOENT;
|
||||
QString qpath(path);
|
||||
|
||||
struct stat st;
|
||||
|
||||
m_mutex.lock();
|
||||
for( FuseAccessorBase *fab : std::as_const(m_accessors) )
|
||||
{
|
||||
int sret = fab->getattr(qpath, &st);
|
||||
if( 0 == sret )
|
||||
isFound = true;
|
||||
else
|
||||
continue;
|
||||
|
||||
if( FuseAccessorBase::FH_SANDBOX != fab->getType() )
|
||||
continue;
|
||||
|
||||
|
|
@ -424,15 +535,12 @@ int FuseInterface::unlink(const char *path)
|
|||
break;
|
||||
}
|
||||
|
||||
if( CACHE_ENABLED ) {
|
||||
if( m_cache_stat.contains(qpath) )
|
||||
{
|
||||
struct stat *st = m_cache_stat.take(qpath);
|
||||
if( st )
|
||||
free(st);
|
||||
}
|
||||
if( m_dircache.contains(qpath) )
|
||||
m_dircache.remove(qpath);
|
||||
if( CACHE_ENABLED )
|
||||
cacheInvalidate(qpath);
|
||||
|
||||
if( isFound ) {
|
||||
markDeleted(qpath, true);
|
||||
ret = 0;
|
||||
}
|
||||
|
||||
m_mutex.unlock();
|
||||
|
|
@ -455,16 +563,11 @@ int FuseInterface::rmdir(const char *path)
|
|||
break;
|
||||
}
|
||||
|
||||
if( CACHE_ENABLED ) {
|
||||
if( m_cache_stat.contains(qpath) )
|
||||
{
|
||||
struct stat *st = m_cache_stat.take(qpath);
|
||||
if( st )
|
||||
free(st);
|
||||
}
|
||||
if( m_dircache.contains(qpath) )
|
||||
m_dircache.remove(qpath);
|
||||
}
|
||||
if( CACHE_ENABLED )
|
||||
cacheInvalidate(qpath);
|
||||
|
||||
if( 0 == ret )
|
||||
markDeleted(qpath, true);
|
||||
|
||||
m_mutex.unlock();
|
||||
return ret;
|
||||
|
|
@ -495,12 +598,11 @@ int FuseInterface::truncate(const char *path, off_t offset, struct fuse_file_inf
|
|||
break;
|
||||
}
|
||||
|
||||
if( CACHE_ENABLED && m_cache_stat.contains(qpath) )
|
||||
{
|
||||
struct stat *st = m_cache_stat.take(qpath);
|
||||
if( st )
|
||||
free(st);
|
||||
}
|
||||
if( CACHE_ENABLED )
|
||||
cacheInvalidate(qpath);
|
||||
|
||||
if( 0 == ret && m_deleted.contains(qpath.toLower()) )
|
||||
markDeleted(qpath, false);
|
||||
|
||||
m_mutex.unlock();
|
||||
return ret;
|
||||
|
|
@ -516,11 +618,17 @@ int FuseInterface::readdir(const char *path, void *buf, fuse_fill_dir_t filler,
|
|||
Q_UNUSED(flags)
|
||||
|
||||
QString qpath(path);
|
||||
if( CACHE_ENABLED && m_dircache.contains(qpath) )
|
||||
QString lpath = qpath.toLower();
|
||||
if( CACHE_ENABLED && m_dircache.contains(lpath) )
|
||||
{
|
||||
QStringList ents = m_dircache[qpath];
|
||||
for( const QString &ent : std::as_const(ents) )
|
||||
QStringList ents = m_dircache[lpath];
|
||||
for( const QString &ent : std::as_const(ents) ) {
|
||||
QString qipath = QString("%1/%2").arg(qpath).arg(ent);
|
||||
if( m_deleted.contains(qipath.toLower()) )
|
||||
continue;
|
||||
|
||||
filler(buf, ent.toStdString().c_str(), NULL, 0, FUSE_FILL_DIR_DEFAULTS);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -530,6 +638,10 @@ int FuseInterface::readdir(const char *path, void *buf, fuse_fill_dir_t filler,
|
|||
QStringList dir = fab->readdir(QString(path));
|
||||
for( const QStringView nent : std::as_const(dir) )
|
||||
{
|
||||
QString qipath = QString("%1/%2").arg(qpath).arg(nent);
|
||||
if( m_deleted.contains(qipath.toLower()) )
|
||||
continue;
|
||||
|
||||
QString blorp = nent.toString();
|
||||
QString lc = blorp.toLower();
|
||||
if( lents.contains(lc) )
|
||||
|
|
@ -542,16 +654,16 @@ int FuseInterface::readdir(const char *path, void *buf, fuse_fill_dir_t filler,
|
|||
}
|
||||
|
||||
if( CACHE_ENABLED )
|
||||
m_dircache[qpath] = ents;
|
||||
m_dircache[lpath] = ents;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int FuseInterface::write(const char *path, const char *buf, size_t bufsz, off_t offset, struct fuse_file_info *fi)
|
||||
{
|
||||
Q_UNUSED(path)
|
||||
FuseFHBase *fhbase = reinterpret_cast<FuseFHBase *>(fi->fh);
|
||||
qDebug() << "FuseFHBase:" << fhbase;
|
||||
QString qpath(path);
|
||||
FuseFHBase *fhbase = getFH(fi);
|
||||
//qDebug() << "FuseInterface::write:" << fi << fhbase << path;
|
||||
|
||||
//FuseAccessorBase *fabase = fhbase->getAccessor();
|
||||
/*
|
||||
|
|
@ -561,11 +673,14 @@ int FuseInterface::write(const char *path, const char *buf, size_t bufsz, off_t
|
|||
|
||||
FuseFHBase *sfh = qobject_cast<FuseFHBase *>(fhbase);
|
||||
QByteArray data(buf, bufsz);
|
||||
if( CACHE_ENABLED )
|
||||
cacheInvalidate(qpath);
|
||||
return sfh->write(data, offset, fi);
|
||||
}
|
||||
|
||||
int FuseInterface::open(const char *path, struct fuse_file_info *fi)
|
||||
{
|
||||
bool checkDeleted = false;
|
||||
int fam = (fi->flags & O_ACCMODE);
|
||||
|
||||
QString qpath(path);
|
||||
|
|
@ -586,13 +701,19 @@ int FuseInterface::open(const char *path, struct fuse_file_info *fi)
|
|||
break;
|
||||
default:
|
||||
qmode = QIODeviceBase::ReadOnly;
|
||||
checkDeleted = true;
|
||||
}
|
||||
|
||||
if( !( O_CREAT & fi->flags ) )
|
||||
if( !( O_CREAT & fi->flags ) ) {
|
||||
qmode |= QIODeviceBase::ExistingOnly;
|
||||
checkDeleted = true;
|
||||
}
|
||||
|
||||
if( checkDeleted && m_deleted.contains(qpath.toLower()) )
|
||||
return -ENOENT;
|
||||
|
||||
// STATS:
|
||||
VFSStatEntry se { qpath, QDateTime::currentSecsSinceEpoch(), 0, 0 };
|
||||
VFSStatEntry se { qpath, QDateTime::currentSecsSinceEpoch(), 0, 0, 1 };
|
||||
|
||||
if( CACHE_ENABLED
|
||||
&& QIODeviceBase::ReadOnly & qmode
|
||||
|
|
@ -610,7 +731,7 @@ int FuseInterface::open(const char *path, struct fuse_file_info *fi)
|
|||
}
|
||||
m_stats[qpath].openCount++;
|
||||
m_stats[qpath].refCount++;
|
||||
emit entryUpdated(qpath, fab->getResource(), m_stats[qpath].openCount, m_stats[qpath].refCount);
|
||||
emit entryUpdated(qpath.toLower(), fab->getType(), fh->getResource(), m_stats[qpath].openCount, m_stats[qpath].refCount);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -632,12 +753,12 @@ int FuseInterface::open(const char *path, struct fuse_file_info *fi)
|
|||
m_open_handles.prepend(fh);
|
||||
|
||||
if( !m_stats.contains(qpath) ) {
|
||||
qDebug() << "Adding2" << qpath;
|
||||
qDebug() << "Adding2" << qpath << fh << qmode;
|
||||
m_stats.insert(qpath, se);
|
||||
}
|
||||
m_stats[qpath].openCount++;
|
||||
m_stats[qpath].refCount++;
|
||||
emit entryUpdated(qpath, fab->getResource(), m_stats[qpath].openCount, m_stats[qpath].refCount);
|
||||
emit entryUpdated(qpath.toLower(), fab->getType(), fh->getResource(), m_stats[qpath].openCount, m_stats[qpath].refCount);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -647,23 +768,31 @@ int FuseInterface::open(const char *path, struct fuse_file_info *fi)
|
|||
|
||||
int FuseInterface::release(const char *path, struct fuse_file_info *fi)
|
||||
{
|
||||
Q_UNUSED(path)
|
||||
//Q_UNUSED(path)
|
||||
|
||||
FuseFHBase *fhbase = reinterpret_cast<FuseFHBase *>(fi->fh);
|
||||
m_mutex.lock();
|
||||
FuseFHBase *fhbase = getFH(fi);
|
||||
qDebug() << "FuseInterface::release:" << fi << fhbase << path;
|
||||
|
||||
QString qpath = fhbase->getPath();
|
||||
VFSStatEntry se { qpath, QDateTime::currentSecsSinceEpoch(), 0, 0 };
|
||||
VFSStatEntry se { qpath, QDateTime::currentSecsSinceEpoch(), 0, 0, 1 };
|
||||
if( !m_stats.contains(qpath) )
|
||||
qDebug() << "Warning:" << qpath << "release, wasn't opened. Name was changed?";
|
||||
else {
|
||||
m_stats[qpath].refCount--;
|
||||
emit entryUpdated(qpath, fhbase->getAccessor()->getResource(), m_stats[qpath].openCount, m_stats[qpath].refCount);
|
||||
emit entryUpdated(qpath.toLower(), fhbase->getAccessor()->getType(), fhbase->getResource(), m_stats[qpath].openCount, m_stats[qpath].refCount);
|
||||
}
|
||||
|
||||
m_open_handles.removeOne(fhbase);
|
||||
|
||||
fhbase->release();
|
||||
fhbase->deleteLater();
|
||||
|
||||
if( m_cow_redirects.contains(fi->fh))
|
||||
m_cow_redirects.remove(fi->fh);
|
||||
|
||||
m_mutex.unlock();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -671,7 +800,7 @@ int FuseInterface::read(const char *path, char *buf, size_t size, off_t offset,
|
|||
{
|
||||
Q_UNUSED(path)
|
||||
|
||||
FuseFHBase *fhbase = reinterpret_cast<FuseFHBase *>(fi->fh);
|
||||
FuseFHBase *fhbase = getFH(fi);
|
||||
QByteArray data = fhbase->read(size, offset);
|
||||
if( data.isEmpty() )
|
||||
return 0;
|
||||
|
|
@ -684,7 +813,7 @@ off_t FuseInterface::lseek(const char *path, off_t off, int whence, struct fuse_
|
|||
{
|
||||
Q_UNUSED(path)
|
||||
|
||||
FuseFHBase *fhbase = reinterpret_cast<FuseFHBase *>(fi->fh);
|
||||
FuseFHBase *fhbase = getFH(fi);
|
||||
//FuseAccessorBase *fabase = fhbase->getAccessor();
|
||||
return fhbase->lseek(off, whence);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ struct VFSStatEntry {
|
|||
time_t lastAccess;
|
||||
quint32 openCount;
|
||||
quint32 refCount;
|
||||
qint8 type;
|
||||
};
|
||||
|
||||
class FuseAccessorBase;
|
||||
|
|
@ -52,13 +53,16 @@ class FuseInterface : public QObject
|
|||
QList< FuseAccessorBase * > m_accessors;
|
||||
QList< FuseFHBase * > m_open_handles;
|
||||
|
||||
QStringList m_deleted;
|
||||
QMap< QString, QStringList > m_dircache;
|
||||
QMap<QString, struct stat *> m_cache_stat;
|
||||
QMap<QString, FuseAccessorBase *> m_cache_accessor;
|
||||
QCache< QString, QSharedPointer<QByteArray> > m_cache_data;
|
||||
QCache<QString, QSharedPointer<QByteArray>> m_cache_data;
|
||||
QFileSystemWatcher m_watcher;
|
||||
QHash< QString, VFSStatEntry > m_stats;
|
||||
|
||||
QMap< qint64, FuseFHBase * > m_cow_redirects;
|
||||
|
||||
public:
|
||||
explicit FuseInterface(QSqlDatabase &database, int profileId, const QString &gameDataDir, const QString &sandboxDir, const QString &mountpoint, QObject *parent=nullptr);
|
||||
~FuseInterface();
|
||||
|
|
@ -76,11 +80,17 @@ public:
|
|||
int getProfileId() { return m_profileId; }
|
||||
|
||||
//int utilQPermsToMode(QFileDevice::Permissions perms);
|
||||
FuseFHBase *makeWritableFromSource(const QString &source, const QString &origsource, const QString &dest, QIODeviceBase::OpenMode mode);
|
||||
FuseFHBase *makeWritableFromData(const QByteArray &source, const QString &origsource, const QString &dest, QIODeviceBase::OpenMode mode);
|
||||
FuseFHBase *getFH(struct fuse_file_info *fi);
|
||||
FuseFHBase *makeWritableFromSource(struct fuse_file_info *fi, const QString &source, const QString &origsource, const QString &dest, QIODeviceBase::OpenMode mode);
|
||||
FuseFHBase *makeWritableFromData(struct fuse_file_info *fi, const QByteArray &source, const QString &origsource, const QString &dest, QIODeviceBase::OpenMode mode);
|
||||
|
||||
void cacheInsert(const QString &path, QByteArray &data);
|
||||
void cacheInsert(FuseAccessorBase *fab, const QString &path, const QByteArray &data);
|
||||
//QByteArray cacheGrab(const QString &path);
|
||||
QSharedPointer<QByteArray> cacheGrab(const QString &path);
|
||||
void cacheInvalidate(const QString &path);
|
||||
|
||||
bool markDeleted(const QString &path, bool isdeleted=true);
|
||||
void readDeleted();
|
||||
|
||||
protected:
|
||||
void init();
|
||||
|
|
@ -121,7 +131,7 @@ protected:
|
|||
static off_t f_lseek(const char *path, off_t off, int whence, struct fuse_file_info *fi);
|
||||
|
||||
signals:
|
||||
void entryUpdated(const QString &path, const QString &facility, quint32 count, quint8 refcount);
|
||||
void entryUpdated(const QString &path, qint8 type, const QString &facility, quint32 count, quint8 refcount);
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(FuseInterface *)
|
||||
|
|
|
|||
|
|
@ -114,21 +114,9 @@ QList<FuseProxyMapping> FuseAccessorProxy::proxyList(QSqlDatabase &database, int
|
|||
}
|
||||
|
||||
int FuseAccessorProxy::makeWritable(struct fuse_file_info *fi, const QString &path, QIODeviceBase::OpenMode mode, const QByteArray &data, off_t offset)
|
||||
{
|
||||
QString qpath = m_resource + QDir::separator() + resolvePath( m_resource, path );
|
||||
FuseFHBase *fh = m_parent->makeWritableFromSource(qpath, path, path, mode);
|
||||
if( !fh )
|
||||
return -1;
|
||||
|
||||
fi->fh = reinterpret_cast<quint64>(fh);
|
||||
return fh->write(data, offset, fi);
|
||||
}
|
||||
|
||||
/***********/
|
||||
|
||||
int FuseAccessorProxy::getattr(const QString &path, struct stat *stbuf)
|
||||
{
|
||||
if( !m_entries.isEmpty() ) {
|
||||
qDebug() << "FuseAccessorProxy::makeWritable: Looking for" << path;
|
||||
QString lpath = path.toLower();
|
||||
QStringList lparts = lpath.split(QDir::separator());
|
||||
for( const FuseProxyMapping &ent : std::as_const(m_entries) )
|
||||
|
|
@ -139,8 +127,55 @@ int FuseAccessorProxy::getattr(const QString &path, struct stat *stbuf)
|
|||
nomatch = true;
|
||||
}
|
||||
|
||||
if( nomatch )
|
||||
if( nomatch ) {
|
||||
//qDebug() << "FuseAccessorProxy::getattr: skipping" << ent.mapped;
|
||||
continue;
|
||||
}
|
||||
|
||||
if( lparts.length() < ent.lparts.length() )
|
||||
{
|
||||
qDebug() << "FuseAccessorProxy::getattr: Found dir" << lparts.join(QDir::separator()) << "in" << ent.lparts.join(QDir::separator());
|
||||
return -EACCES;
|
||||
}
|
||||
|
||||
QString qpath = ent.basedir + QDir::separator() + ent.actual;
|
||||
FuseFHBase *fh = m_parent->makeWritableFromSource(fi, qpath, path, path, mode);
|
||||
if( !fh )
|
||||
return -1;
|
||||
|
||||
return fh->write(data, offset, fi);
|
||||
}
|
||||
}
|
||||
|
||||
QString qpath = m_resource + QDir::separator() + resolvePath( m_resource, path );
|
||||
qDebug() << "FuseAccessorProxy::makeWritable:" << qpath;
|
||||
FuseFHBase *fh = m_parent->makeWritableFromSource(fi, qpath, path, path, mode);
|
||||
if( !fh )
|
||||
return -1;
|
||||
|
||||
return fh->write(data, offset, fi);
|
||||
}
|
||||
|
||||
/***********/
|
||||
|
||||
int FuseAccessorProxy::getattr(const QString &path, struct stat *stbuf)
|
||||
{
|
||||
if( !m_entries.isEmpty() ) {
|
||||
qDebug() << "FuseAccessorProxy::getattr: Looking for" << path;
|
||||
QString lpath = path.toLower();
|
||||
QStringList lparts = lpath.split(QDir::separator());
|
||||
for( const FuseProxyMapping &ent : std::as_const(m_entries) )
|
||||
{
|
||||
bool nomatch = false;
|
||||
for( int x=1; !nomatch && x < lparts.length() && x < ent.lparts.length(); x++ ) {
|
||||
if( lparts[x] != ent.lparts[x] )
|
||||
nomatch = true;
|
||||
}
|
||||
|
||||
if( nomatch ) {
|
||||
//qDebug() << "FuseAccessorProxy::getattr: skipping" << ent.mapped;
|
||||
continue;
|
||||
}
|
||||
|
||||
if( lparts.length() < ent.lparts.length() )
|
||||
{
|
||||
|
|
@ -179,13 +214,15 @@ QStringList FuseAccessorProxy::readdir(const QString &path)
|
|||
if( !ent.lmapped.startsWith(lpath) )
|
||||
continue;
|
||||
|
||||
qDebug() << "readdir:" << lpath << ent.lmapped << ent.actual;
|
||||
//qDebug() << "readdir:" << lpath << ent.lmapped << ent.actual;
|
||||
int trim = lpath.length();
|
||||
QStringList sent = ent.mapped.mid(trim).split(QDir::separator());
|
||||
|
||||
// Eg: lpath="/data" & ent.mapped="/data/meshes/..." -> sent[0] = "", sent[1] = "meshes"
|
||||
if( sent.at(0).isEmpty() )
|
||||
sent.removeFirst();
|
||||
if( !sent.at(0).isEmpty() )
|
||||
continue;
|
||||
|
||||
sent.removeFirst();
|
||||
//qDebug() << "sent:" << sent;
|
||||
|
||||
//results.append(m_entries[ent]);
|
||||
|
|
@ -225,7 +262,7 @@ FuseFHBase *FuseAccessorProxy::open(const QString &path, QIODeviceBase::OpenMode
|
|||
if( nomatch )
|
||||
continue;
|
||||
|
||||
if( lparts.length() < ent.lparts.length() )
|
||||
if( lparts.length() < ent.lparts.length() && QDir::separator() == ent.lparts.at(lparts.length()) )
|
||||
{
|
||||
qDebug() << "FuseAccessorArchive::open: Found dir" << lparts.join(QDir::separator()) << "in" << ent.lparts.join(QDir::separator());
|
||||
return NULL;
|
||||
|
|
@ -238,15 +275,17 @@ FuseFHBase *FuseAccessorProxy::open(const QString &path, QIODeviceBase::OpenMode
|
|||
}
|
||||
|
||||
QFileInfo fi( qpath );
|
||||
if( !fi.exists() && QIODeviceBase::ReadOnly & mode ) {
|
||||
if( !fi.exists() && !( mode & QIODeviceBase::ReadWrite ) && !( mode & QIODeviceBase::WriteOnly ) ) {
|
||||
//qDebug() << "FuseAccessorProxy::open:"<<qpath<<"doesn't exist";
|
||||
return NULL;
|
||||
}
|
||||
/*
|
||||
else if( fi.exists() && !(QIODeviceBase::ReadOnly & mode) )
|
||||
{
|
||||
qDebug() << "FuseAccessorProxy::open: ** Make writable:" << qpath << "--==>" << path << "--" << m_resource;
|
||||
return m_parent->makeWritableFromSource(qpath, path, path, mode);
|
||||
return m_parent->makeWritableFromSource(fi, qpath, path, path, mode);
|
||||
}
|
||||
*/
|
||||
|
||||
FuseFHProxy *f = new FuseFHProxy(this, path, qpath);
|
||||
if( !f->open(mode) )
|
||||
|
|
@ -272,6 +311,39 @@ FuseFHProxy::~FuseFHProxy()
|
|||
FuseFHProxy::release();
|
||||
}
|
||||
|
||||
const QString FuseFHProxy::getResource()
|
||||
{
|
||||
return m_file.fileName();
|
||||
|
||||
/*
|
||||
QList< FuseProxyMapping > entries = m_parent->entries();
|
||||
if( !m_entries.isEmpty() ) {
|
||||
qDebug() << "FuseAccessorProxy::getattr: Looking for" << path;
|
||||
QString lpath = path.toLower();
|
||||
QStringList lparts = lpath.split(QDir::separator());
|
||||
for( const FuseProxyMapping &ent : std::as_const(m_entries) )
|
||||
{
|
||||
bool nomatch = false;
|
||||
for( int x=1; !nomatch && x < lparts.length() && x < ent.lparts.length(); x++ ) {
|
||||
if( lparts[x] != ent.lparts[x] )
|
||||
nomatch = true;
|
||||
}
|
||||
|
||||
if( nomatch ) {
|
||||
//qDebug() << "FuseAccessorProxy::getattr: skipping" << ent.mapped;
|
||||
continue;
|
||||
}
|
||||
|
||||
QString qpath = ent.basedir + QDir::separator() + ent.actual;
|
||||
qDebug() << "FuseAccessorProxy::getattr:" << qpath;
|
||||
int ret = ::stat(qpath.toStdString().c_str(), stbuf);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
*/
|
||||
return m_parent->getResource();
|
||||
}
|
||||
|
||||
bool FuseFHProxy::open(QIODeviceBase::OpenMode mode)
|
||||
{
|
||||
m_mode = mode;
|
||||
|
|
@ -280,7 +352,7 @@ bool FuseFHProxy::open(QIODeviceBase::OpenMode mode)
|
|||
|
||||
QByteArray FuseFHProxy::read(size_t size, off_t offset)
|
||||
{
|
||||
if( offset >= 0 && !m_file.seek(offset) )
|
||||
if( !m_file.seek(offset) )
|
||||
return QByteArray();
|
||||
|
||||
return m_file.read(size);
|
||||
|
|
@ -296,8 +368,10 @@ off_t FuseFHProxy::lseek(off_t off, int whence)
|
|||
|
||||
int FuseFHProxy::write(const QByteArray &data, off_t offset, struct fuse_file_info *fi)
|
||||
{
|
||||
if( m_mode & QIODeviceBase::ReadOnly )
|
||||
if( !( m_mode & QIODeviceBase::ReadWrite ) && !( m_mode & QIODeviceBase::WriteOnly ) ) {
|
||||
qDebug() << "FuseFHProxy::write: Write attempted on ReadOnly handle:" << m_path;
|
||||
return -1;
|
||||
}
|
||||
|
||||
//FuseArchiveEntry &entry = m_entries[lpath];
|
||||
qDebug() << "FuseFHArchive::write: Upgrading" << m_path << "to writable...";
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ public:
|
|||
|
||||
// This expects the new entry to be created, then return "write" on the opened handle, otherwise return the appropriate "write" error code.
|
||||
int makeWritable(struct fuse_file_info *fi, const QString &path, QIODeviceBase::OpenMode mode, const QByteArray &data, off_t offset);
|
||||
|
||||
QList< FuseProxyMapping > &entries() { return m_entries; }
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -53,6 +55,8 @@ public:
|
|||
explicit FuseFHProxy(FuseAccessorProxy *parent, const QString &path, const QString &newpath);
|
||||
virtual ~FuseFHProxy();
|
||||
|
||||
const QString getResource() override;
|
||||
|
||||
bool open(QIODeviceBase::OpenMode mode) override;
|
||||
QByteArray read(size_t size, off_t offset) override;
|
||||
off_t lseek(off_t off, int whence) override;
|
||||
|
|
|
|||
|
|
@ -138,10 +138,11 @@ FuseFHBase *FuseAccessorSandbox::makeWritableFromSource(const QString &source, c
|
|||
return NULL;
|
||||
}
|
||||
|
||||
QStringList parts = dest.toLower().split(QDir::separator(), Qt::SkipEmptyParts);
|
||||
QString dfname = parts.takeLast();
|
||||
//QStringList parts = dest.toLower().split(QDir::separator(), Qt::SkipEmptyParts);
|
||||
//QString dfname = parts.takeLast();
|
||||
//ddir.mkpath( parts.join(QDir::separator()) );
|
||||
|
||||
QDir ddir(m_resource);
|
||||
ddir.mkpath( parts.join(QDir::separator()) );
|
||||
QFile::copy(source, qdest);
|
||||
|
||||
qDebug() << "FuseAccessorSandbox::makeWritableFromSource: cp" << source << qdest;
|
||||
|
|
@ -176,6 +177,7 @@ FuseFHBase *FuseAccessorSandbox::makeWritableFromData(const QByteArray &source,
|
|||
FuseFHSandbox *ent = new FuseFHSandbox(this, origpath, qdest);
|
||||
if( !ent->open(mode) )
|
||||
{
|
||||
qDebug() << "FuseAccessorSandbox::makeWritableFromData: FuseFHSandbox open failed";
|
||||
ent->deleteLater();
|
||||
return NULL;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,14 +128,24 @@ Item {
|
|||
if( !profileId )
|
||||
profileId = 1;
|
||||
|
||||
let q = conn.query("SELECT m.modId, m.name, m.nexusGameCode, m.nexusId, m.nexusFileId, m.author, m.version, m.website, m.description, m.groups, (SELECT COUNT(ps.modId) FROM profile_selections ps WHERE ps.profileId=? AND ps.modId=m.modId) AS enabled, m.installed, m.filename FROM mods m", [profileId]);
|
||||
let q = conn.query("SELECT m.modId, m.name, m.nexusGameCode, m.nexusId, m.nexusFileId, m.author, m.version, m.website, m.description, m.groups, (SELECT COUNT(ps.modId) FROM profile_selections ps WHERE ps.profileId=? AND ps.modId=m.modId) AS enabled, m.installed, m.filename, m.moddir FROM mods m", [profileId]);
|
||||
const results = q.toArray();
|
||||
q.destroy();
|
||||
|
||||
results.map( function(m) {
|
||||
if( m['groups'].length > 0 )
|
||||
m['groups'] = JSON.parse(m['groups']);
|
||||
else m['groups'] = [];
|
||||
try {
|
||||
m["modId"] = parseInt(""+m["modId"]);
|
||||
m["nexusId"] = parseInt(""+m["nexusId"]);
|
||||
m["nexusFileId"] = parseInt(""+m["nexusFileId"]);
|
||||
/*
|
||||
if( m['groups'].length > 0 ) {
|
||||
m['groups'] = JSON.parse(m['groups']);
|
||||
} else m['groups'] = "";
|
||||
*/
|
||||
} catch(e) {
|
||||
console.log(`getMods: Mod ${m['modId']} contains unparseable 'groups' data: `+m['groups']);
|
||||
m['groups'] = "";
|
||||
}
|
||||
} );
|
||||
|
||||
return results;
|
||||
|
|
@ -193,8 +203,8 @@ Item {
|
|||
const tgroups = JSON.stringify(modinfo['groups']);
|
||||
|
||||
conn.transaction();
|
||||
let q = conn.query("INSERT INTO mods (nexusGameCode, nexusId, nexusFileId, name, author, version, website, description, groups, installed, enabled, filename)VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
[modinfo['nexusGameCode'], modinfo['nexusId'], modinfo['nexusFileId'], modinfo['name'], modinfo['author'], modinfo['version'], modinfo['website'], modinfo['description'], tgroups, modinfo['installed'], modinfo['enabled'], modinfo['filename']]);
|
||||
let q = conn.query("INSERT INTO mods (nexusGameCode, nexusId, nexusFileId, name, author, version, website, description, groups, installed, enabled, filename, moddir)VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
[modinfo['nexusGameCode'], modinfo['nexusId'], modinfo['nexusFileId'], modinfo['name'], modinfo['author'], modinfo['version'], modinfo['website'], modinfo['description'], tgroups, modinfo['installed'], modinfo['enabled'], modinfo['filename'], modinfo['moddir']]);
|
||||
modinfo['modId'] = q.lastInsertId();
|
||||
conn.commit();
|
||||
|
||||
|
|
@ -219,13 +229,7 @@ Item {
|
|||
let q = conn.query("SELECT _rowId_ AS id, name FROM profiles");
|
||||
const results = q.toArray();
|
||||
q.destroy();
|
||||
/*
|
||||
results.map( function(m) {
|
||||
if( m['groups'].length > 0 )
|
||||
m['groups'] = JSON.parse(m['groups']);
|
||||
else m['groups'] = [];
|
||||
} );
|
||||
*/
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
|
|
@ -267,7 +271,7 @@ Item {
|
|||
{
|
||||
if( !checkConnection() ) return;
|
||||
|
||||
let qstr = "SELECT f.fileId, f.modId, f.relative, f.source, f.dest, f.priority, m.enabled FROM files f JOIN mods m ON m.modId=f.modId";
|
||||
let qstr = "SELECT f.fileId, f.modId, f.relative, f.source, f.dest, f.priority, m.enabled, m.name, m.description FROM files f JOIN mods m ON m.modId=f.modId";
|
||||
let token = ' WHERE ';
|
||||
let args = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ Item {
|
|||
signal deleteMod(variant mod)
|
||||
|
||||
signal writeRequested()
|
||||
function forceLayout() {
|
||||
modsList.forceLayout();
|
||||
}
|
||||
|
||||
SplitView {
|
||||
id: header
|
||||
|
|
@ -87,8 +90,13 @@ Item {
|
|||
height: row.height
|
||||
width: row.width
|
||||
|
||||
readonly property var modent: modsModel.get(index)
|
||||
readonly property int rowIndex: index
|
||||
required property int index
|
||||
required property var modelData
|
||||
//readonly property var modent: dragArea.modelData
|
||||
//readonly property var modent: modsModel.get(index)
|
||||
|
||||
//readonly property int rowIndex: index
|
||||
readonly property int rowIndex: dmodel.modelIndex(index).row
|
||||
//readonly property int modelIndex: dmodel.items.get(rowIndex).model.index
|
||||
|
||||
property bool held: false
|
||||
|
|
@ -160,7 +168,7 @@ Item {
|
|||
|
||||
clip: true
|
||||
|
||||
color: (dragArea.rowIndex % 2) === 0 ? Material.background : Qt.darker(Material.background, 1.20)
|
||||
color: (dragArea.index % 2) === 0 ? Material.background : Qt.darker(Material.background, 1.20)
|
||||
|
||||
Drag.active: dragArea.held
|
||||
Drag.source: dragArea
|
||||
|
|
@ -175,19 +183,19 @@ Item {
|
|||
id: switchEnabled
|
||||
y: height * 0.5
|
||||
width: header.widths ? header.widths[0] + 4 : 50
|
||||
enabled: dragArea.modent['installed']
|
||||
checked: dragArea.modent['enabled']
|
||||
enabled: modelData && modelData['installed']
|
||||
checked: modelData && modelData['enabled']
|
||||
onToggled: {
|
||||
if( !dragArea.modent['enabled'] )
|
||||
enableMod(dragArea.modent);
|
||||
if( !modelData['enabled'] )
|
||||
enableMod(modelData);
|
||||
else
|
||||
disableMod(dragArea.modent);
|
||||
disableMod(modelData);
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: dragArea.modent ? dragArea.modent['modId'] : '???'
|
||||
enabled: dragArea.modent['installed']
|
||||
text: modelData ? modelData['modId'] : '???'
|
||||
enabled: modelData && modelData['installed']
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 5
|
||||
width: header.widths ? header.widths[1] + 4 : 100
|
||||
|
|
@ -196,8 +204,8 @@ Item {
|
|||
maximumLineCount: 1
|
||||
}
|
||||
Label {
|
||||
text: dragArea.modent ? dragArea.modent['name'] : '???'
|
||||
enabled: dragArea.modent['installed']
|
||||
text: modelData ? modelData['name'] : '???'
|
||||
enabled: modelData && modelData['installed']
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 5
|
||||
width: header.widths ? header.widths[2] + 4 : 100
|
||||
|
|
@ -207,8 +215,8 @@ Item {
|
|||
}
|
||||
Label {
|
||||
id: cellText
|
||||
text: dragArea.modent ? dragArea.modent['author'] : '???'
|
||||
enabled: dragArea.modent['installed']
|
||||
text: modelData ? modelData['author'] : '???'
|
||||
enabled: modelData && modelData['installed']
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 5
|
||||
height: parent.height
|
||||
|
|
@ -217,8 +225,8 @@ Item {
|
|||
maximumLineCount: 1
|
||||
}
|
||||
Label {
|
||||
text: dragArea.modent ? dragArea.modent['version'] : '???'
|
||||
enabled: dragArea.modent['installed']
|
||||
text: modelData ? modelData['version'] : '???'
|
||||
enabled: modelData && modelData['installed']
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 5
|
||||
width: header.widths ? header.widths[4] + 4 : 100
|
||||
|
|
@ -227,8 +235,8 @@ Item {
|
|||
maximumLineCount: 1
|
||||
}
|
||||
Label {
|
||||
text: dragArea.modent ? dragArea.modent['description'] : '???'
|
||||
enabled: dragArea.modent['installed']
|
||||
text: modelData ? modelData['description'] : '???'
|
||||
enabled: modelData && modelData['installed']
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 5
|
||||
height: parent.height
|
||||
|
|
@ -237,8 +245,8 @@ Item {
|
|||
maximumLineCount: 1
|
||||
}
|
||||
Label {
|
||||
text: dragArea.modent ? dragArea.modent['website'] : '???'
|
||||
enabled: dragArea.modent['installed']
|
||||
text: modelData ? modelData['website'] : '???'
|
||||
enabled: modelData && modelData['installed']
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 5
|
||||
width: header.widths ? header.widths[6] + 4 : 100
|
||||
|
|
@ -250,33 +258,45 @@ Item {
|
|||
|
||||
Menu {
|
||||
id: cellMenu
|
||||
MenuItem {
|
||||
text: dragArea.modent && dragArea.modent['installed'] ? qsTr('Uninstall') : qsTr('Install')
|
||||
onTriggered: {
|
||||
if( dragArea.modent && dragArea.modent['installed'] )
|
||||
uninstallMod(dragArea.modent);
|
||||
else
|
||||
installMod(dragArea.modent);
|
||||
|
||||
Menu {
|
||||
enabled: !actionUninstall.enabled
|
||||
title: qsTr('Install')
|
||||
|
||||
Action {
|
||||
text: qsTr('Install Compressed')
|
||||
onTriggered: installMod(modsModel.get(dragArea.rowIndex));
|
||||
}
|
||||
Action {
|
||||
text: qsTr('Install Uncompressed')
|
||||
onTriggered: installMod(modsModel.get(dragArea.rowIndex));
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
text: dragArea.modent && dragArea.modent['enabled'] ? qsTr('Disable') : qsTr('Enable')
|
||||
enabled: dragArea.modent && dragArea.modent['installed'] ? true:false
|
||||
id: actionUninstall
|
||||
text: qsTr('Uninstall')
|
||||
enabled: modelData && modelData['installed']
|
||||
onTriggered: uninstallMod(modsModel.get(dragArea.rowIndex));
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
text: modelData && modelData['enabled'] ? qsTr('Disable') : qsTr('Enable')
|
||||
enabled: modelData && modelData['installed'] ? true:false
|
||||
onTriggered: {
|
||||
if( !dragArea.modent['enabled'] )
|
||||
enableMod(dragArea.modent);
|
||||
if( !modelData['enabled'] )
|
||||
enableMod(modsModel.get(dragArea.rowIndex));
|
||||
else
|
||||
disableMod(dragArea.modent);
|
||||
disableMod(modsModel.get(dragArea.rowIndex));
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr('Delete')
|
||||
onTriggered: deleteMod(dragArea.modent);
|
||||
onTriggered: deleteMod(modsModel.get(dragArea.rowIndex));
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr('Re-configure')
|
||||
enabled: dragArea.modent && dragArea.modent['installed'] ? true:false
|
||||
onTriggered: reinstallMod(dragArea.modent);
|
||||
enabled: modelData && modelData['installed'] ? true:false
|
||||
onTriggered: reinstallMod(modsModel.get(dragArea.rowIndex));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -296,7 +316,7 @@ Item {
|
|||
//enabled: !dragArea.held
|
||||
|
||||
onEntered: function(drag) {
|
||||
console.log(`Drag: ${drag.source.DelegateModel.itemsIndex} -> ${dragArea.rowIndex}`);
|
||||
console.log(`Drag: ${drag.source.DelegateModel.itemsIndex} -> ${dragArea.index}`);
|
||||
dragArea.targeted = true;
|
||||
dmodel.items.move(drag.source.DelegateModel.itemsIndex, dragArea.DelegateModel.itemsIndex);
|
||||
}
|
||||
|
|
@ -313,7 +333,27 @@ Item {
|
|||
|
||||
ListModel {
|
||||
id: modsModel
|
||||
|
||||
//dynamicRoles: true
|
||||
ListElement {
|
||||
modId: 0
|
||||
nexusId: 0
|
||||
nexusFileId: 0
|
||||
//groups: []
|
||||
groups: ""
|
||||
enabled: 0
|
||||
installed: 0
|
||||
|
||||
name: ""
|
||||
nexusGameCode: ""
|
||||
author: ""
|
||||
version: ""
|
||||
website: ""
|
||||
description: ""
|
||||
filename: ""
|
||||
moddir: ""
|
||||
}
|
||||
|
||||
function some( scb )
|
||||
{
|
||||
for( let a=0; a < count; a++ )
|
||||
|
|
@ -322,7 +362,7 @@ Item {
|
|||
if( scb(ment) )
|
||||
return a;
|
||||
}
|
||||
return false;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -335,6 +375,10 @@ Item {
|
|||
bottom: parent.bottom
|
||||
}
|
||||
|
||||
contentWidth: header.width
|
||||
ScrollBar.vertical.policy: ScrollBar.AlwaysOn
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOn
|
||||
|
||||
ListView {
|
||||
id: modsList
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,18 @@ Item {
|
|||
|
||||
ListModel {
|
||||
id: pluginsModel
|
||||
dynamicRoles: true
|
||||
|
||||
//dynamicRoles: true
|
||||
ListElement {
|
||||
modId: 0
|
||||
name: ""
|
||||
description: ""
|
||||
enabled: false
|
||||
filename: ""
|
||||
missing: false
|
||||
notfound: false
|
||||
}
|
||||
|
||||
function some( scb )
|
||||
{
|
||||
for( let a=0; a < count; a++ )
|
||||
|
|
@ -23,10 +34,39 @@ Item {
|
|||
if( scb(ment) )
|
||||
return a;
|
||||
}
|
||||
return false;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
function setModel(newModel) {
|
||||
console.log("Mod master listed updated, rebuilding list!");
|
||||
const model = pluginsModel;
|
||||
const newLen = newModel.length;
|
||||
const oldLen = model.count;
|
||||
|
||||
for( let a=0; a < newLen; a++ )
|
||||
{
|
||||
let nent = newModel[a];
|
||||
if( a >= oldLen ) {
|
||||
model.append(nent);
|
||||
continue;
|
||||
}
|
||||
|
||||
const mtents = model.get(a);
|
||||
for( const k in nent ) {
|
||||
if( nent[k] === mtents[k] )
|
||||
continue;
|
||||
|
||||
model.setProperty(a, k, nent[k]);
|
||||
}
|
||||
|
||||
//model.set(a, nent);
|
||||
}
|
||||
|
||||
if( newLen < oldLen )
|
||||
model.remove(newLen, oldLen-newLen);
|
||||
}
|
||||
/*
|
||||
function setModel(newModel)
|
||||
{
|
||||
// Remove old:
|
||||
|
|
@ -75,7 +115,7 @@ Item {
|
|||
//console.log(` +++ ${JSON.stringify(ne)}`);
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
SplitView {
|
||||
id: header
|
||||
x: 0-pluginsList.contentX
|
||||
|
|
@ -131,6 +171,10 @@ Item {
|
|||
bottom: parent.bottom
|
||||
}
|
||||
|
||||
contentWidth: header.width
|
||||
ScrollBar.vertical.policy: ScrollBar.AlwaysOn
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOn
|
||||
|
||||
ListView {
|
||||
id: pluginsList
|
||||
spacing: 0
|
||||
|
|
@ -154,9 +198,11 @@ Item {
|
|||
height: implicitHeight
|
||||
width: implicitWidth
|
||||
|
||||
readonly property int rowIndex: index
|
||||
readonly property int modelIndex: dmodel.items.get(rowIndex).model.index
|
||||
property variant modent: pluginsModel.get(modelIndex)
|
||||
required property int index
|
||||
required property var modelData
|
||||
|
||||
readonly property int rowIndex: dmodel.modelIndex(index).row
|
||||
|
||||
property bool held: false
|
||||
property bool targeted: false
|
||||
property int previousIndex: -1
|
||||
|
|
@ -247,13 +293,13 @@ Item {
|
|||
Drag.hotSpot.y: height / 2
|
||||
|
||||
property color rowColour: (dragArea.rowIndex % 2) === 0 ? Material.background : Qt.darker(Material.background, 1.20)
|
||||
property color validColour: dragArea.modent && dragArea.modent['notfound'] ? '#888800' : rowColour
|
||||
property color validColour: modelData['notfound'] ? '#888800' : rowColour
|
||||
|
||||
color: dragArea.modent && dragArea.modent['missing'] ? '#880000' : validColour
|
||||
color: modelData['missing'] ? '#880000' : validColour
|
||||
|
||||
ToolTip.visible: dragArea.containsMouse && dragArea.modent && (dragArea.modent['missing'] || dragArea.modent['notfound']) ? true : false
|
||||
ToolTip.text: dragArea.modent && dragArea.modent['missing'] && dragArea.modent['missing'].length ? qsTr('Missing (or loaded out of order) masters will prevent this plugin from loading:\n\n%1').arg(dragArea.modent['missing'].join('\n'))
|
||||
: dragArea.modent && dragArea.modent['notfound'] ? qsTr("The file for this entry can't be found, so it won't be loaded.") : ''
|
||||
ToolTip.visible: dragArea.containsMouse && (modelData['missing'] || modelData['notfound']) ? true : false
|
||||
ToolTip.text: modelData['missing'] && modelData['missing'].length ? qsTr('Missing (or loaded out of order) masters will prevent this plugin from loading:\n\n%1').arg(modelData['missing'].join('\n'))
|
||||
: modelData && modelData['notfound'] ? qsTr("The file for this entry can't be found, so it won't be loaded.") : ''
|
||||
|
||||
Row {
|
||||
id: realRow
|
||||
|
|
@ -264,13 +310,13 @@ Item {
|
|||
Switch {
|
||||
id: cellEnabled
|
||||
|
||||
checked: dragArea.modent && dragArea.modent['enabled'] ? true : false
|
||||
checked: modelData['enabled'] ? true : false
|
||||
anchors.centerIn: parent
|
||||
onClicked: {
|
||||
if( !dragArea.modent['enabled'] )
|
||||
enableMod(dragArea.modent);
|
||||
if( !modelData['enabled'] )
|
||||
enableMod(pluginsModel.get(dragArea.rowIndex));
|
||||
else
|
||||
disableMod(dragArea.modent);
|
||||
disableMod(pluginsModel.get(dragArea.rowIndex));
|
||||
}
|
||||
|
||||
indicator: Rectangle {
|
||||
|
|
@ -304,21 +350,21 @@ Item {
|
|||
|
||||
Loader {
|
||||
sourceComponent: textCell
|
||||
property string modelText: dragArea.modent ? dragArea.modent['filename'] : '???'
|
||||
property string modelText: modelData['filename'] || '???'
|
||||
width: header.widths ? header.widths[ 1 ] + 5 : 24
|
||||
height: 32
|
||||
}
|
||||
|
||||
Loader {
|
||||
sourceComponent: textCell
|
||||
property string modelText: dragArea.modent ? dragArea.modent['name'] : '???'
|
||||
property string modelText: modelData['name'] || '???'
|
||||
width: header.widths ? header.widths[ 2 ] + 5 : 24
|
||||
height: 32
|
||||
}
|
||||
|
||||
Loader {
|
||||
sourceComponent: textCell
|
||||
property string modelText: dragArea.modent ? dragArea.modent['description'] : '???'
|
||||
property string modelText: modelData['description'] || '???'
|
||||
width: header.widths ? header.widths[ 3 ] + 5 : 24
|
||||
height: 32
|
||||
}
|
||||
|
|
@ -327,12 +373,12 @@ Item {
|
|||
Menu {
|
||||
id: cellMenu
|
||||
MenuItem {
|
||||
text: dragArea.modent && dragArea.modent['enabled'] ? qsTr('Disable') : qsTr('Enable')
|
||||
text: modelData['enabled'] ? qsTr('Disable') : qsTr('Enable')
|
||||
onTriggered: {
|
||||
if( !dragArea.modent['enabled'] )
|
||||
enableMod(dragArea.modent);
|
||||
if( !modelData['enabled'] )
|
||||
enableMod(pluginsModel.get(dragArea.rowIndex));
|
||||
else
|
||||
disableMod(dragArea.modent);
|
||||
disableMod(pluginsModel.get(dragArea.rowIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,68 +1,80 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls.Material
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Controls.Material 2.15
|
||||
|
||||
ScrollView {
|
||||
id: root
|
||||
clip: true
|
||||
import Qt.labs.qmlmodels 1.0
|
||||
|
||||
// these are bound by the parent page
|
||||
property variant model
|
||||
property variant proxy
|
||||
Item {
|
||||
id: vfsTable
|
||||
|
||||
property int sortColumn: -1
|
||||
property bool sortAscending: true
|
||||
required property variant model
|
||||
required property variant proxy
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
property int sortColumn: 0
|
||||
property bool sortAscending: false
|
||||
|
||||
// ---------- HEADER ----------
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 28
|
||||
color: Qt.lighter(Material.background, 1.05)
|
||||
border.color: "#ccc"
|
||||
readonly property var originName: [ 'Undefined', 'Unknown', 'Archive', 'Proxy', 'Sandbox' ]
|
||||
readonly property var rowColours: [ '#808080','#ff0000',"#6ec8fa","#f59b14","#358611" ]
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 1
|
||||
SplitView {
|
||||
id: header
|
||||
x: 0-logEntryList.contentX
|
||||
height: 32
|
||||
width: implicitWidth > parent.width ? implicitWidth * 2 : parent.width * 2
|
||||
|
||||
Repeater {
|
||||
model: root.model ? root.model.columns : []
|
||||
readonly property variant preferredWidths: [ 65, header.width*0.33, 64, header.width*0.50, 32, header.width*0.40, 32, 16 ]
|
||||
property variant widths
|
||||
|
||||
delegate: Rectangle {
|
||||
color: "transparent"
|
||||
border.color: "#ccc"
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: fontMetrics.boundingRect(modelData.title).width + 60
|
||||
Repeater {
|
||||
id: headerRepeater
|
||||
model: [ qsTr('Open'), qsTr('Name'), qsTr('Origin'), qsTr('Facility'), qsTr('TotalOpen'), qsTr('Last Access'), qsTr('Open'), qsTr('') ]
|
||||
readonly property variant sortRoleId: [ 3, 0, 4, 1, 2, 5, 3, 3 ]
|
||||
property int sortColumn: 0
|
||||
Label {
|
||||
id: thisLabel
|
||||
required property int index
|
||||
required property var modelData
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
Label { text: modelData.title; font.bold: true }
|
||||
Label {
|
||||
text: (root.sortColumn === index)
|
||||
? (root.sortAscending ? "▲" : "▼")
|
||||
: ""
|
||||
font.pixelSize: 10
|
||||
}
|
||||
}
|
||||
SplitView.minimumWidth: 48
|
||||
text: modelData
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
onWidthChanged: header.updateSizes();
|
||||
font.pointSize: 10
|
||||
font.bold: true
|
||||
leftPadding: 5
|
||||
rightPadding: 48
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (root.sortColumn === index)
|
||||
root.sortAscending = !root.sortAscending
|
||||
else
|
||||
root.sortAscending = true
|
||||
root.sortColumn = index
|
||||
if (root.proxy)
|
||||
root.proxy.sort(index, root.sortAscending
|
||||
? Qt.AscendingOrder
|
||||
: Qt.DescendingOrder)
|
||||
Rectangle {
|
||||
visible: thisLabel.index < headerRepeater.model.length-1
|
||||
enabled: visible
|
||||
color: 'transparent'
|
||||
border.width: 1
|
||||
border.color: thisLabel.index === sortColumn ? Material.accent : Material.frameColor
|
||||
radius: 180
|
||||
width: parent.height * 0.5
|
||||
height: width
|
||||
anchors {
|
||||
rightMargin: 5
|
||||
right: parent.right
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: parent.height * 0.5
|
||||
text: vfsTable.sortAscending ? '↑' : '↓'
|
||||
visible: thisLabel.index === headerRepeater.sortColumn
|
||||
color: Material.foreground
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
if( headerRepeater.sortColumn === thisLabel.index )
|
||||
vfsTable.sortAscending = !vfsTable.sortAscending;
|
||||
else {
|
||||
headerRepeater.sortColumn = thisLabel.index;
|
||||
vfsTable.sortColumn = headerRepeater.sortRoleId[ thisLabel.index ];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -70,59 +82,155 @@ ScrollView {
|
|||
}
|
||||
}
|
||||
|
||||
// ---------- TABLE ----------
|
||||
TableView {
|
||||
id: table
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
columnSpacing: 1
|
||||
rowSpacing: 1
|
||||
reuseItems: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
model: root.proxy
|
||||
function updateSizes() {
|
||||
var newWidths = [];
|
||||
for( let sidx=0; sidx < headerRepeater.count; sidx++ )
|
||||
{
|
||||
const hri = headerRepeater.itemAt(sidx);
|
||||
if( !hri )
|
||||
return;
|
||||
newWidths.push( hri.width );
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
implicitHeight: 26
|
||||
color: styleData.selected
|
||||
? Material.color(Material.Blue, Material.Shade100)
|
||||
: "transparent"
|
||||
header.widths = newWidths;
|
||||
logEntryList.forceLayout();
|
||||
}
|
||||
|
||||
Label {
|
||||
text: modelData
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 8
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
width: parent.width - 8
|
||||
Component.onCompleted: {
|
||||
try {
|
||||
if( settings.logEntryListColumnSizes )
|
||||
{
|
||||
header.restoreState( settings.logEntryListColumnSizes );
|
||||
logEntryList.forceLayout();
|
||||
return;
|
||||
}
|
||||
} catch(err) {
|
||||
console.log("Couldn't restore column widths. Oh well.");
|
||||
}
|
||||
|
||||
columnWidthProvider: function(col) {
|
||||
const columns = root.model ? root.model.columns : []
|
||||
if (!columns.length) return 100
|
||||
const headerText = columns[col].title
|
||||
const headerWidth = fontMetrics.boundingRect(headerText).width + 20
|
||||
let maxCellWidth = headerWidth
|
||||
const rowsToCheck = Math.min(100, table.rows)
|
||||
for (let i = 0; i < rowsToCheck; ++i) {
|
||||
const value = table.proxy.data(table.model.index(i, col))
|
||||
if (value)
|
||||
maxCellWidth = Math.max(
|
||||
maxCellWidth,
|
||||
fontMetrics.boundingRect(value.toString()).width + 20
|
||||
)
|
||||
}
|
||||
return maxCellWidth
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
const cols = root.model ? root.model.columns : []
|
||||
for (let i = 0; i < cols.length; ++i)
|
||||
table.addColumn({ role: "display" }) // DisplayRole matches modelData
|
||||
}
|
||||
|
||||
FontMetrics { id: fontMetrics }
|
||||
for( let sidx=0; sidx < headerRepeater.count; sidx++ )
|
||||
headerRepeater.itemAt(sidx).width = preferredWidths[sidx] || 32;
|
||||
logEntryList.forceLayout();
|
||||
}
|
||||
Component.onDestruction: {
|
||||
settings.logEntryListColumnSizes = header.saveState();
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: rowDelegate
|
||||
|
||||
Rectangle {
|
||||
id: row
|
||||
|
||||
required property int index
|
||||
required property var modelData
|
||||
//readonly property int rowIndex: vfsTable.proxy.modelIndex(index).row
|
||||
|
||||
width: labelRow.implicitWidth
|
||||
height: labelRow.height
|
||||
|
||||
clip: true
|
||||
color: (index % 2) === 0 ? Material.background : Qt.darker(Material.background, 1.20)
|
||||
|
||||
Row {
|
||||
id: labelRow
|
||||
height: childrenRect.height
|
||||
spacing: 10
|
||||
|
||||
Item {
|
||||
width: header.widths ? header.widths[0] : 50
|
||||
height: parent.height
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
radius: 180
|
||||
color: row.modelData['refcount'] > 0 ? '#358611' : '#808080'
|
||||
width: 16
|
||||
height: 16
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: row.modelData['path']
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 5
|
||||
width: header.widths ? header.widths[1] : 100
|
||||
//height: parent.height
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
Label {
|
||||
text: vfsTable.originName[ row.modelData['type'] ]
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 5
|
||||
width: header.widths ? header.widths[2] : 100
|
||||
//height: parent.height
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
color: vfsTable.rowColours[ row.modelData["type"] ]
|
||||
}
|
||||
Label {
|
||||
text: row.modelData['facility']
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 5
|
||||
width: header.widths ? header.widths[3] : 100
|
||||
//height: parent.height
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
color: vfsTable.rowColours[ row.modelData["type"] ]
|
||||
}
|
||||
Label {
|
||||
text: row.modelData['count']
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 5
|
||||
//height: parent.height
|
||||
width: header.widths ? header.widths[4] : 100
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
Label {
|
||||
text: row.modelData['lastAccess']
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 5
|
||||
//height: parent.height
|
||||
width: header.widths ? header.widths[5] : 100
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
Label {
|
||||
text: row.modelData['refcount']
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 5
|
||||
width: header.widths ? header.widths[6] : 100
|
||||
//height: parent.height
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
} // Rectangle
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
id: scrollView
|
||||
anchors {
|
||||
top: header.bottom
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
}
|
||||
|
||||
contentWidth: header.width
|
||||
ScrollBar.vertical.policy: ScrollBar.AlwaysOn
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOn
|
||||
|
||||
ListView {
|
||||
id: logEntryList
|
||||
|
||||
clip: true
|
||||
delegate: rowDelegate
|
||||
model: vfsTable.proxy
|
||||
//model: vfsTable.model
|
||||
move: Transition { SmoothedAnimation {} }
|
||||
} // TableView
|
||||
} // ScrollView
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import QtQuick.Controls.Material
|
|||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
//anchors.fill: parent
|
||||
spacing: 8
|
||||
|
||||
// --- Filter bar ---
|
||||
|
|
@ -41,11 +41,7 @@ ColumnLayout {
|
|||
model: VFSModel
|
||||
proxy: VFSProxy
|
||||
|
||||
onSortColumnChanged: resort();
|
||||
onSortAscendingChanged: resort();
|
||||
|
||||
function resort() {
|
||||
VFSProxy.sort(sortColumn, sortAscending ? Qt.AscendingOrder : Qt.DescendingOrder);
|
||||
}
|
||||
onSortColumnChanged: VFSProxy.setSort(sortColumn);
|
||||
onSortAscendingChanged: VFSProxy.setSortDir(sortAscending ? Qt.AscendingOrder : Qt.DescendingOrder);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ function loadForGame()
|
|||
const dbpath = sobj.dbPath;
|
||||
db.open(dbpath);
|
||||
|
||||
//modMasterList = db.getMods();
|
||||
Mods.rereadMods();
|
||||
|
||||
statusBar.text = qsTr('Now managing "%1"').arg(currentGame);
|
||||
|
|
|
|||
45
qml/main.qml
45
qml/main.qml
|
|
@ -184,6 +184,9 @@ ApplicationWindow {
|
|||
TabButton {
|
||||
text: qsTr('VFS')
|
||||
}
|
||||
TabButton {
|
||||
text: qsTr('Launch')
|
||||
}
|
||||
}
|
||||
ComboBox {
|
||||
id: profilesMenu
|
||||
|
|
@ -278,11 +281,9 @@ ApplicationWindow {
|
|||
id: vfsStats
|
||||
}
|
||||
|
||||
/*
|
||||
LaunchPage {
|
||||
id: launchPage
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
Settings {
|
||||
|
|
@ -293,6 +294,7 @@ ApplicationWindow {
|
|||
property var modListColumnSizes
|
||||
property var pluginsListColumnSizes
|
||||
property var loadorderListColumnSizes
|
||||
property var logEntryListColumnSizes
|
||||
|
||||
property string nexusApiKey
|
||||
property string nexusUuid
|
||||
|
|
@ -453,31 +455,34 @@ ApplicationWindow {
|
|||
|
||||
property var modMasterList: []
|
||||
onModMasterListChanged: {
|
||||
/*
|
||||
modTable.model.clear();
|
||||
for( let ent of modMasterList ) {
|
||||
modTable.model.append(ent);
|
||||
}
|
||||
*/
|
||||
Qt.callLater( function() { updateMasterList(); } );
|
||||
}
|
||||
function updateMasterList() {
|
||||
console.log("Mod master listed updated, rebuilding list!");
|
||||
const newLen = modMasterList.length;
|
||||
const oldLen = modTable.model.count;
|
||||
|
||||
modTable.model.clear();
|
||||
for( let a=0; a < modMasterList.length; a++ )
|
||||
for( let a=0; a < newLen; a++ )
|
||||
{
|
||||
let nent = modMasterList[a];
|
||||
modTable.model.set(a, nent);
|
||||
/*
|
||||
let oent = modTable.model.get(a);
|
||||
for( k of Object.keys(nent) )
|
||||
{
|
||||
if( oent[k] != nent[k] )
|
||||
modTable.model.set(a, nent);
|
||||
if( a >= oldLen ) {
|
||||
modTable.model.append(nent);
|
||||
continue;
|
||||
}
|
||||
*/
|
||||
|
||||
const mtents = modTable.model.get(a);
|
||||
for( const k in nent ) {
|
||||
if( nent[k] === mtents[k] )
|
||||
continue;
|
||||
|
||||
modTable.model.setProperty(a, k, nent[k]);
|
||||
}
|
||||
|
||||
//modTable.model.set(a, nent);
|
||||
}
|
||||
|
||||
if( modMasterList.length < modTable.model.count )
|
||||
modTable.model.remove(modMasterList.length, modTable.model.count-modMasterList.length);
|
||||
if( newLen < oldLen )
|
||||
modTable.model.remove(newLen, oldLen-newLen);
|
||||
}
|
||||
|
||||
readonly property variant gameDefinitions: [
|
||||
|
|
|
|||
|
|
@ -65,11 +65,13 @@ function manifestFromFomod(filepath, cb)
|
|||
console.log("FomodDir: "+fomodDir);
|
||||
|
||||
let fomodParts = fomodDir.split(/\//g);
|
||||
/*
|
||||
if( fomodParts.length > 1 )
|
||||
{
|
||||
fomodParts.pop();
|
||||
rel = fomodParts.join('/');
|
||||
}
|
||||
*/
|
||||
|
||||
pathInfo = filelist.find( e => e['pathname'].toLowerCase() === fomodLow + '/info.xml' );
|
||||
pathConfig = filelist.find( e => e['pathname'].toLowerCase() === fomodLow + '/moduleconfig.xml' );
|
||||
|
|
@ -179,7 +181,7 @@ function installFromFilesystem(filepath, cb, gamecode, nexusModId, nexusFileId)
|
|||
});
|
||||
} catch(err) { console.log("ERROR: "+err); }
|
||||
}
|
||||
|
||||
/*
|
||||
function checkOverwrites(fileMap)
|
||||
{
|
||||
let overwrites = [];
|
||||
|
|
@ -198,7 +200,7 @@ function checkOverwrites(fileMap)
|
|||
} );
|
||||
return overwrites;
|
||||
}
|
||||
|
||||
*/
|
||||
function enableMod(mod)
|
||||
{
|
||||
let sobj = gameSettings.objFor(currentGame);
|
||||
|
|
@ -679,11 +681,12 @@ function installFancyMod(mod, files, folders, modinfo, flags)
|
|||
|
||||
// HACK: Don't -actually- extract anything:
|
||||
return finished();
|
||||
|
||||
/*
|
||||
if( Object.keys(fileMap).length > 0 )
|
||||
extractFileMap(mod, fileMap, finished);
|
||||
else
|
||||
finished();
|
||||
*/
|
||||
} );
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -301,13 +301,6 @@ function enableMod(mod)
|
|||
|
||||
function updatePluginsTable(plugins)
|
||||
{
|
||||
// Let's get hairy...
|
||||
let mods = db.getMods();
|
||||
let files = {};
|
||||
mods.forEach( function(mod) {
|
||||
files[mod['modId']] = db.getFiles(mod['modId']);
|
||||
} );
|
||||
|
||||
let model = [];
|
||||
plugins.forEach( function(ent) {
|
||||
let nent = { 'enabled':ent['enabled'], 'filename':ent['filename'], 'name':'???', 'description':'???' };
|
||||
|
|
@ -315,35 +308,19 @@ function updatePluginsTable(plugins)
|
|||
{
|
||||
nent['name'] = currentGameEntry['name'];
|
||||
nent['description'] = qsTr('Built-In');
|
||||
}
|
||||
} else {
|
||||
if( ent['description'] )
|
||||
nent['description'] = ent['description'];
|
||||
|
||||
if( ent['description'] )
|
||||
nent['description'] = ent['description'];
|
||||
if( ent['missing'] )
|
||||
nent['missing'] = ent['missing'];
|
||||
|
||||
if( ent['missing'] )
|
||||
nent['missing'] = ent['missing'];
|
||||
|
||||
/**
|
||||
* HACK We don't care, we're using a VFS now:
|
||||
**
|
||||
if( !ent['plugin'] )
|
||||
nent['notfound'] = true;
|
||||
*/
|
||||
|
||||
let done = false;
|
||||
for( let a=0; a < mods.length && !done; a++ )
|
||||
{
|
||||
const mod = mods[a];
|
||||
const fent = files[mod['modId']];
|
||||
for( let b=0; b < fent.length && !done; b++ )
|
||||
const results = db.getFilesEndingWith([ent['filename']]);
|
||||
if( results.length > 0 )
|
||||
{
|
||||
const f = fent[b];
|
||||
if( f['dest'].endsWith(ent['filename']) )
|
||||
{
|
||||
nent['name'] = mod['name'];
|
||||
nent['description'] = mod['description'];
|
||||
done = true;
|
||||
}
|
||||
const fent = results[0];
|
||||
nent['name'] = fent['name'];
|
||||
nent['description'] = fent['description'];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ QMAKE_LDFLAGS += -fPIE -g
|
|||
|
||||
LIBS += -larchive
|
||||
|
||||
#CONFIG+=sanitizer sanitize_address sanitize_undefined
|
||||
|
||||
debug {
|
||||
#CONFIG+=sanitizer sanitize_address sanitize_undefined
|
||||
#CONFIG+=sanitize_thread sanitize_memory
|
||||
|
|
@ -54,6 +56,8 @@ CONFIG += c++17 cmdline link_pkgconfig
|
|||
PKGCONFIG += fuse3 libunarr
|
||||
#QMAKE_CXXFLAGS += -D_FILE_OFFSET_BITS=64 -O3
|
||||
QMAKE_CXXFLAGS += -D_FILE_OFFSET_BITS=64 -g -O0
|
||||
QMAKE_CXXFLAGS_RELEASE -= -O2
|
||||
QMAKE_CXXFLAGS_DEBUG -= -O2
|
||||
|
||||
# You can make your code fail to compile if it uses deprecated APIs.
|
||||
# In order to do so, uncomment the following line.
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ public:
|
|||
Q_INVOKABLE FuseInterface *create(const QVariant &dbhandle, const QString &gameName, int profileId);
|
||||
|
||||
signals:
|
||||
void entryUpdated(const QString &path, const QString &facility, quint32 count, quint8 refcount);
|
||||
void entryUpdated(const QString &path, qint8 type, const QString &facility, quint32 count, quint8 refcount);
|
||||
};
|
||||
|
||||
#endif // FUSEMANAGER_H
|
||||
|
|
|
|||
15
src/main.cpp
15
src/main.cpp
|
|
@ -108,29 +108,18 @@ int main(int argc, char *argv[])
|
|||
engine.rootContext()->setContextProperty("FUSEManager", fuse);
|
||||
|
||||
VFSTableModel *vfsModel = new VFSTableModel();
|
||||
QSortFilterProxyModel *vfsProxy = new QSortFilterProxyModel();
|
||||
VFSSortProxy *vfsProxy = new VFSSortProxy();
|
||||
vfsProxy->setSourceModel(vfsModel);
|
||||
vfsProxy->setDynamicSortFilter(true);
|
||||
vfsProxy->setSortCaseSensitivity(Qt::CaseInsensitive);
|
||||
vfsProxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
|
||||
vfsProxy->setFilterKeyColumn(-1);
|
||||
vfsProxy->setFilterKeyColumn(0);
|
||||
//vfsProxy->setFilterRole(Qt::DisplayRole);
|
||||
|
||||
QObject::connect( fuse, &FUSEManager::entryUpdated, vfsModel, &VFSTableModel::upsertEntry );
|
||||
engine.rootContext()->setContextProperty("VFSModel", vfsModel);
|
||||
engine.rootContext()->setContextProperty("VFSProxy", vfsProxy);
|
||||
|
||||
QObject::connect(fuse, &FUSEManager::entryUpdated,
|
||||
[vfsModel, vfsProxy](const QString &path, const QString &facility, int count, int refcount)
|
||||
{
|
||||
qDebug().noquote() << "entryUpdated:" << path
|
||||
<< facility << count << refcount
|
||||
<< "rows(model)=" << vfsModel->rowCount()
|
||||
<< "rows(proxy)=" << vfsProxy->rowCount();
|
||||
qDebug() << "vfsModel:" << vfsModel->roleNames();
|
||||
qDebug() << "vfsProxy:" << vfsProxy->roleNames();
|
||||
});
|
||||
|
||||
QObject::connect(&engine, &QQmlEngine::destroyed, vfsModel, &QObject::deleteLater);
|
||||
QObject::connect(&engine, &QQmlEngine::destroyed, vfsProxy, &QObject::deleteLater);
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -2,103 +2,76 @@
|
|||
|
||||
VFSTableModel::VFSTableModel(QObject *parent)
|
||||
: QAbstractTableModel(parent)
|
||||
{
|
||||
// column definitions (DisplayRole = header, UserRole = data role)
|
||||
m_columns = {
|
||||
QMap<int, QVariant>{ {Qt::DisplayRole, "Path"}, {Qt::UserRole, "path"} },
|
||||
QMap<int, QVariant>{ {Qt::DisplayRole, "Facility"}, {Qt::UserRole, "facility"} },
|
||||
QMap<int, QVariant>{ {Qt::DisplayRole, "Count"}, {Qt::UserRole, "count"} },
|
||||
QMap<int, QVariant>{ {Qt::DisplayRole, "RefCount"}, {Qt::UserRole, "refcount"} }
|
||||
};
|
||||
{}
|
||||
|
||||
// build roleNames map
|
||||
int base = Qt::UserRole;
|
||||
for (int i = 0; i < m_columns.size(); ++i) {
|
||||
const QByteArray role = m_columns[i].value(Qt::UserRole).toByteArray();
|
||||
m_roleNames[base + i] = role;
|
||||
}
|
||||
|
||||
emit columnsChanged();
|
||||
}
|
||||
|
||||
int VFSTableModel::rowCount(const QModelIndex &) const
|
||||
{
|
||||
return m_rows.size();
|
||||
}
|
||||
|
||||
int VFSTableModel::columnCount(const QModelIndex &) const
|
||||
{
|
||||
return m_columns.size();
|
||||
}
|
||||
int VFSTableModel::rowCount(const QModelIndex &) const { return m_rows.size(); }
|
||||
int VFSTableModel::columnCount(const QModelIndex &) const { return 1; }
|
||||
|
||||
QVariant VFSTableModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() ||
|
||||
index.row() >= m_rows.size() ||
|
||||
index.column() >= m_columns.size())
|
||||
if (!index.isValid() || role < Path || role >= ColumnCount )
|
||||
return {};
|
||||
|
||||
const VFSEntry &e = m_rows[index.row()];
|
||||
if( Qt::DisplayRole != role )
|
||||
return {};
|
||||
|
||||
// Allow both DisplayRole and specific UserRole access
|
||||
switch( index.column() )
|
||||
{
|
||||
case 0:
|
||||
return e.path;
|
||||
case 1:
|
||||
return e.facility;
|
||||
case 2:
|
||||
return e.count;
|
||||
case 3:
|
||||
return e.refcount;
|
||||
const auto &row = m_rows[index.row()];
|
||||
switch (role) {
|
||||
case Path: return row.path;
|
||||
case Facility: return row.facility;
|
||||
case Count: return row.count;
|
||||
case RefCount: return row.refcount;
|
||||
case Type: return row.type;
|
||||
case LastAccess:return row.lastAccess;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
QVariant VFSTableModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||
QVariant VFSTableModel::headerData(int section, Qt::Orientation o, int role) const
|
||||
{
|
||||
if (orientation == Qt::Horizontal &&
|
||||
section >= 0 && section < m_columns.size())
|
||||
return m_columns[section].value(role);
|
||||
if (o == Qt::Horizontal && role == Qt::DisplayRole) {
|
||||
switch (section) {
|
||||
case Path: return "Path";
|
||||
case Facility: return "Facility";
|
||||
case Count: return "Count";
|
||||
case RefCount: return "RefCount";
|
||||
case Type: return "Type";
|
||||
case LastAccess:return "LastAccess";
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> VFSTableModel::roleNames() const
|
||||
{
|
||||
return m_roleNames;
|
||||
return {
|
||||
{ Path, "path" },
|
||||
{ Facility, "facility" },
|
||||
{ Count, "count" },
|
||||
{ RefCount, "refcount" },
|
||||
{ Type, "type" },
|
||||
{ LastAccess,"lastAccess" }
|
||||
};
|
||||
}
|
||||
|
||||
QVariantList VFSTableModel::columns()
|
||||
{
|
||||
QVariantList out;
|
||||
for (const auto &col : m_columns) {
|
||||
QVariantMap map;
|
||||
map.insert("title", col.value(Qt::DisplayRole).toString());
|
||||
map.insert("role", col.value(Qt::UserRole).toString());
|
||||
out.append(map);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void VFSTableModel::upsertEntry(const QString &path,
|
||||
const QString &facility,
|
||||
int count,
|
||||
int refcount)
|
||||
void VFSTableModel::upsertEntry(const QString &path, qint8 type, const QString &facility, int count, int refcount)
|
||||
{
|
||||
auto it = m_index.find(path);
|
||||
if (it == m_index.end()) {
|
||||
const int row = m_rows.size();
|
||||
beginInsertRows({}, row, row);
|
||||
m_rows.append({path, facility, count, refcount});
|
||||
m_index[path] = row;
|
||||
// new row
|
||||
int newRow = m_rows.size();
|
||||
beginInsertRows({}, newRow, newRow);
|
||||
m_rows.append({ path, facility, count, refcount, type, QDateTime::currentDateTime() });
|
||||
m_index.insert(path, newRow);
|
||||
endInsertRows();
|
||||
emit dataChanged(index(newRow, 0), index(newRow, 0));
|
||||
} else {
|
||||
const int row = it.value();
|
||||
m_rows[row] = {path, facility, count, refcount};
|
||||
emit dataChanged(index(row, 0), index(row, columnCount() - 1));
|
||||
// update existing
|
||||
int row = it.value();
|
||||
auto &entry = m_rows[row];
|
||||
entry.facility = facility;
|
||||
entry.count = count;
|
||||
entry.refcount = refcount;
|
||||
entry.type = type;
|
||||
entry.lastAccess = QDateTime::currentDateTime();
|
||||
emit dataChanged(index(row, 0), index(row, 0));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,55 +2,54 @@
|
|||
#define VFSTABLEMODEL_H
|
||||
|
||||
#include <QAbstractTableModel>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QDateTime>
|
||||
#include <QHash>
|
||||
#include <QMap>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QString>
|
||||
#include <QVariant>
|
||||
#include <QVector>
|
||||
#include <QString>
|
||||
|
||||
struct VFSEntry {
|
||||
QString path;
|
||||
QString facility;
|
||||
int count = 0;
|
||||
int refcount = 0;
|
||||
qint8 type = 0;
|
||||
QDateTime lastAccess;
|
||||
};
|
||||
|
||||
class VFSTableModel : public QAbstractTableModel
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QVariantList columns READ columns NOTIFY columnsChanged)
|
||||
|
||||
public:
|
||||
enum Columns { Path = Qt::UserRole, Facility, Count, RefCount, Type, LastAccess, ColumnCount };
|
||||
Q_ENUM(Columns)
|
||||
|
||||
explicit VFSTableModel(QObject *parent = nullptr);
|
||||
|
||||
// core model interface
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
|
||||
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
// QML integration
|
||||
QVariantList columns();
|
||||
|
||||
public slots:
|
||||
void upsertEntry(const QString &path, const QString &facility, int count, int refcount);
|
||||
void upsertEntry(const QString &path, qint8 type, const QString &facility, int count, int refcount);
|
||||
void clear();
|
||||
|
||||
signals:
|
||||
void columnsChanged();
|
||||
|
||||
private:
|
||||
QVector<VFSEntry> m_rows;
|
||||
QHash<QString, int> m_index; // path → row index
|
||||
QVector<QMap<int, QVariant>> m_columns;
|
||||
QHash<int, QByteArray> m_roleNames;
|
||||
QList<VFSEntry> m_rows;
|
||||
QHash<QString, int> m_index;
|
||||
};
|
||||
|
||||
class VFSSortProxy : public QSortFilterProxyModel {
|
||||
Q_OBJECT
|
||||
|
||||
Q_PROPERTY(bool onlyActive READ onlyActive WRITE setOnlyActive NOTIFY onlyActiveChanged)
|
||||
Q_PROPERTY(Qt::SortOrder sortDir READ sortDir WRITE setSortDir NOTIFY sortDirChanged)
|
||||
|
||||
public:
|
||||
using QSortFilterProxyModel::QSortFilterProxyModel;
|
||||
|
||||
|
|
@ -62,22 +61,41 @@ public:
|
|||
invalidateFilter();
|
||||
emit onlyActiveChanged();
|
||||
}
|
||||
Q_INVOKABLE void setSort(int roleId) {
|
||||
QSortFilterProxyModel::setDynamicSortFilter(false);
|
||||
QSortFilterProxyModel::setSortRole(Qt::UserRole + roleId);
|
||||
QSortFilterProxyModel::sort(0, m_sortDir);
|
||||
QSortFilterProxyModel::setDynamicSortFilter(true);
|
||||
}
|
||||
Q_INVOKABLE Qt::SortOrder sortDir() { return m_sortDir; }
|
||||
Q_INVOKABLE void setSortDir(Qt::SortOrder dir) {
|
||||
if( m_sortDir == dir )
|
||||
return;
|
||||
|
||||
m_sortDir = dir;
|
||||
QSortFilterProxyModel::setDynamicSortFilter(false);
|
||||
QSortFilterProxyModel::sort(0, m_sortDir);
|
||||
QSortFilterProxyModel::setDynamicSortFilter(true);
|
||||
emit sortDirChanged();
|
||||
}
|
||||
|
||||
protected:
|
||||
bool filterAcceptsRow(int sourceRow, const QModelIndex &srcParent) const override {
|
||||
if (!m_onlyActive)
|
||||
return true;
|
||||
|
||||
const QModelIndex idx = sourceModel()->index(sourceRow, 3, srcParent); // refcount column
|
||||
int refcount = sourceModel()->data(idx, Qt::DisplayRole).toInt();
|
||||
const QModelIndex idx = sourceModel()->index(sourceRow, 0, srcParent);
|
||||
int refcount = sourceModel()->data(idx, VFSTableModel::RefCount).toInt();
|
||||
return refcount > 0;
|
||||
}
|
||||
|
||||
signals:
|
||||
void onlyActiveChanged();
|
||||
void sortDirChanged();
|
||||
|
||||
private:
|
||||
bool m_onlyActive = false;
|
||||
Qt::SortOrder m_sortDir = Qt::DescendingOrder;
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue