mirror of
https://github.com/ModOrganizer2/modorganizer.git
synced 2026-07-27 13:58:24 -07:00
Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08e0d85d85 | ||
|
|
2094d7d15f | ||
|
|
25a69bccd3 | ||
|
|
a39a80ec4c | ||
|
|
42d432e5aa | ||
|
|
acb55d431c | ||
|
|
95a2cc93b9 | ||
|
|
35a0474ac1 | ||
|
|
bd93864ed2 | ||
|
|
406d6f28c0 | ||
|
|
ee4c364ccc | ||
|
|
607fafa364 | ||
|
|
7dc28b10d9 | ||
|
|
9f0c8c31a6 | ||
|
|
2fe627c261 | ||
|
|
879c37984e | ||
|
|
1cf8a2bfee | ||
|
|
7531a860cf | ||
|
|
31c8415e88 | ||
|
|
48274f3357 | ||
|
|
f486e6fa4e | ||
|
|
9e5a64b545 | ||
|
|
3dd05ff869 | ||
|
|
eba98e8469 | ||
|
|
73418ec616 | ||
|
|
7283e6c3ba | ||
|
|
d179ad01c8 | ||
|
|
04d80bb567 | ||
|
|
595f6eccf6 | ||
|
|
f058125e66 | ||
|
|
7d36bc20d0 |
+3
-3
@@ -52,10 +52,10 @@ build_script:
|
||||
if($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode ) }
|
||||
test: off
|
||||
artifacts:
|
||||
- path: '\modorganizer-build\install\bin'
|
||||
- path: '\modorganizer-build\install\bin\ModOrganizer.exe'
|
||||
name: Mod.Organizer-$(APPVEYOR_BUILD_VERSION)
|
||||
- path: '\modorganizer-build\install\pdb'
|
||||
name: PDBs-Mod.Organizer-$(APPVEYOR_BUILD_VERSION)
|
||||
- path: '\modorganizer-build\install\pdb\ModOrganizer.pdb'
|
||||
name: PDB-Mod.Organizer-$(APPVEYOR_BUILD_VERSION)
|
||||
deploy: off
|
||||
on_success:
|
||||
- ps: >-
|
||||
|
||||
@@ -18,6 +18,7 @@ add_filter(NAME src/application GROUPS
|
||||
multiprocess
|
||||
sanitychecks
|
||||
selfupdater
|
||||
updatedialog
|
||||
)
|
||||
|
||||
add_filter(NAME src/browser GROUPS
|
||||
@@ -64,6 +65,7 @@ add_filter(NAME src/downloads GROUPS
|
||||
downloadlist
|
||||
downloadlistview
|
||||
downloadmanager
|
||||
downloadmanager2
|
||||
)
|
||||
|
||||
add_filter(NAME src/env GROUPS
|
||||
|
||||
+5
-6
@@ -251,7 +251,7 @@ std::optional<int> CommandLine::runPostApplication(MOApplication& a)
|
||||
env::Console c;
|
||||
|
||||
if (auto i=InstanceManager::singleton().currentInstance()) {
|
||||
std::cout << i->name().toStdString() << "\n";
|
||||
std::cout << i->displayName().toStdString() << "\n";
|
||||
} else {
|
||||
std::cout << "no instance configured\n";
|
||||
}
|
||||
@@ -289,9 +289,8 @@ std::optional<int> CommandLine::runPostOrganizer(OrganizerCore& core)
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (const std::exception &e) {
|
||||
reportError(
|
||||
QObject::tr("failed to start shortcut: %1").arg(e.what()));
|
||||
catch (std::exception&) {
|
||||
// user was already warned
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -451,7 +450,7 @@ std::optional<QString> CommandLine::instance() const
|
||||
// note that moshortcut:// overrides -i
|
||||
|
||||
if (m_shortcut.isValid() && m_shortcut.hasInstance()) {
|
||||
return m_shortcut.instance();
|
||||
return m_shortcut.instanceName();
|
||||
} else if (m_vm.count("instance")) {
|
||||
return QString::fromStdString(m_vm["instance"].as<std::string>());
|
||||
}
|
||||
@@ -817,7 +816,7 @@ std::optional<int> RunCommand::runPostOrganizer(OrganizerCore& core)
|
||||
reportError(
|
||||
QObject::tr("Executable '%1' not found in instance '%2'.")
|
||||
.arg(program)
|
||||
.arg(InstanceManager::singleton().currentInstance()->name()));
|
||||
.arg(InstanceManager::singleton().currentInstance()->displayName()));
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,212 @@
|
||||
#ifndef MODORGANIZER_CURLDOWNLOADER_INCLUDED
|
||||
#define MODORGANIZER_CURLDOWNLOADER_INCLUDED
|
||||
|
||||
#include <ipluginrepository.h>
|
||||
|
||||
namespace dm::curl
|
||||
{
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
using hr_clock = std::chrono::high_resolution_clock;
|
||||
|
||||
struct defer_t {};
|
||||
extern const defer_t defer;
|
||||
|
||||
class GlobalHandle
|
||||
{
|
||||
public:
|
||||
GlobalHandle();
|
||||
~GlobalHandle();
|
||||
|
||||
GlobalHandle(const GlobalHandle&) = delete;
|
||||
GlobalHandle& operator=(const GlobalHandle) = delete;
|
||||
};
|
||||
|
||||
|
||||
class EasyHandle
|
||||
{
|
||||
public:
|
||||
EasyHandle();
|
||||
EasyHandle(defer_t);
|
||||
~EasyHandle();
|
||||
|
||||
EasyHandle(const EasyHandle&) = delete;
|
||||
EasyHandle& operator=(const EasyHandle&) = delete;
|
||||
|
||||
bool create();
|
||||
CURL* get() const;
|
||||
|
||||
private:
|
||||
CURL* m_handle;
|
||||
};
|
||||
|
||||
|
||||
class MultiHandle
|
||||
{
|
||||
public:
|
||||
MultiHandle();
|
||||
MultiHandle(defer_t);
|
||||
~MultiHandle();
|
||||
|
||||
MultiHandle(const MultiHandle&) = delete;
|
||||
MultiHandle& operator=(const MultiHandle&) = delete;
|
||||
|
||||
bool create();
|
||||
CURLM* get() const;
|
||||
|
||||
private:
|
||||
CURLM* m_handle;
|
||||
};
|
||||
|
||||
|
||||
class FileHandle
|
||||
{
|
||||
public:
|
||||
FileHandle();
|
||||
~FileHandle();
|
||||
|
||||
FileHandle(const FileHandle&) = delete;
|
||||
FileHandle& operator=(const FileHandle&) = delete;
|
||||
|
||||
bool opened() const;
|
||||
std::size_t open(fs::path p, bool append);
|
||||
void close();
|
||||
|
||||
bool write(std::string_view sv);
|
||||
|
||||
private:
|
||||
HANDLE m_handle;
|
||||
fs::path m_path;
|
||||
|
||||
bool doOpen(bool append);
|
||||
};
|
||||
|
||||
|
||||
struct SListDeleter
|
||||
{
|
||||
void operator()(curl_slist* p)
|
||||
{
|
||||
if (p) {
|
||||
curl_slist_free_all(p);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
using SList = std::unique_ptr<curl_slist, SListDeleter>;
|
||||
|
||||
|
||||
class Download : public MOBase::IDownload
|
||||
{
|
||||
public:
|
||||
Download(std::string url, Info info);
|
||||
|
||||
CURL* setup(curl_off_t maxSpeed);
|
||||
|
||||
CURL* handle() const;
|
||||
States state() const override;
|
||||
Stats stats() const override;
|
||||
std::string stealBuffer();
|
||||
QByteArray buffer() const override;
|
||||
int httpCode() const override;
|
||||
std::string error() const;
|
||||
std::string debugName() const;
|
||||
|
||||
void start();
|
||||
void stop() override;
|
||||
bool finish(CURLcode code);
|
||||
|
||||
bool xfer(
|
||||
curl_off_t dltotal, curl_off_t dlnow,
|
||||
curl_off_t ultotal, curl_off_t ulnow);
|
||||
|
||||
bool header(std::string_view sv);
|
||||
bool write(std::string_view data);
|
||||
void debug(curl_infotype t, std::string_view data);
|
||||
|
||||
private:
|
||||
std::string m_url;
|
||||
Info m_info;
|
||||
EasyHandle m_handle;
|
||||
FileHandle m_out;
|
||||
SList m_headers;
|
||||
std::string m_buffer;
|
||||
States m_state;
|
||||
std::string m_error;
|
||||
|
||||
hr_clock::time_point m_lastCheck;
|
||||
std::size_t m_bytes;
|
||||
std::atomic<double> m_bytesPerSecond;
|
||||
std::atomic<double> m_progress;
|
||||
|
||||
std::size_t resumeFrom();
|
||||
bool rename();
|
||||
fs::path outputFile() const;
|
||||
|
||||
static int s_xfer(
|
||||
void* p,
|
||||
curl_off_t dltotal, curl_off_t dlnow,
|
||||
curl_off_t ultotal, curl_off_t ulnow);
|
||||
|
||||
static size_t s_header(char* data, size_t size, size_t n, void* p);
|
||||
static size_t s_write(char* data, size_t size, size_t n, void* p);
|
||||
static int s_debug(CURL* h, curl_infotype t, char* data, size_t n, void *p);
|
||||
};
|
||||
|
||||
|
||||
class Downloader : public MOBase::IDownloader
|
||||
{
|
||||
public:
|
||||
static const std::size_t NoLimit =
|
||||
std::numeric_limits<std::size_t>::max();
|
||||
|
||||
Downloader();
|
||||
~Downloader();
|
||||
|
||||
void cancel();
|
||||
void stop();
|
||||
void join();
|
||||
|
||||
void maxSpeed(std::size_t bytesPerSecond);
|
||||
|
||||
bool finished() const;
|
||||
|
||||
std::shared_ptr<MOBase::IDownload> add(
|
||||
const QUrl& url, const MOBase::IDownload::Info& info={}) override;
|
||||
|
||||
private:
|
||||
using DownloadList = std::list<std::shared_ptr<Download>>;
|
||||
|
||||
std::shared_ptr<GlobalHandle> m_global;
|
||||
MultiHandle m_handle;
|
||||
|
||||
std::vector<std::shared_ptr<Download>> m_temp;
|
||||
std::mutex m_tempMutex;
|
||||
|
||||
std::thread m_thread;
|
||||
std::atomic<bool> m_cancel, m_stop, m_finished;
|
||||
std::condition_variable m_cv;
|
||||
DownloadList m_list;
|
||||
std::map<CURL*, DownloadList::iterator> m_map;
|
||||
|
||||
std::atomic<std::size_t> m_maxSpeed;
|
||||
|
||||
|
||||
void run();
|
||||
void checkTemp();
|
||||
void perform();
|
||||
void poll();
|
||||
bool start(std::shared_ptr<Download> d);
|
||||
void setLimits();
|
||||
void checkCancel();
|
||||
|
||||
void checkQueue();
|
||||
bool cleanupActive();
|
||||
DownloadList::iterator removeFromActive(DownloadList::iterator itor);
|
||||
void stopOverMax(std::size_t max);
|
||||
bool addFromQueue(std::size_t max);
|
||||
curl_off_t maxSpeedPer() const;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
#endif // MODORGANIZER_CURLDOWNLOADER_INCLUDED
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,114 @@
|
||||
#ifndef MODORGANIZER_DOWNLOADEDMANAGER2_INCLUDED
|
||||
#define MODORGANIZER_DOWNLOADEDMANAGER2_INCLUDED
|
||||
|
||||
#include <ipluginrepository.h>
|
||||
|
||||
class PluginContainer;
|
||||
namespace dm::curl { class Downloader; class Download; }
|
||||
|
||||
|
||||
namespace dm
|
||||
{
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
class DownloadManager2;
|
||||
|
||||
class Download
|
||||
{
|
||||
public:
|
||||
enum States
|
||||
{
|
||||
None = 0,
|
||||
|
||||
Queueing,
|
||||
Queued,
|
||||
|
||||
Finished,
|
||||
Errored,
|
||||
|
||||
Running,
|
||||
|
||||
Pausing,
|
||||
Paused,
|
||||
|
||||
Cancelling,
|
||||
Cancelled
|
||||
};
|
||||
|
||||
Download(DownloadManager2& dm, MOBase::IPluginRepository& repo, QString what);
|
||||
|
||||
const QString& what() const;
|
||||
const QString& error() const;
|
||||
|
||||
States state() const;
|
||||
|
||||
bool start();
|
||||
void cancel();
|
||||
void pause();
|
||||
void queue();
|
||||
void tick();
|
||||
|
||||
QString debugName() const;
|
||||
|
||||
private:
|
||||
DownloadManager2& m_dm;
|
||||
MOBase::IPluginRepository& m_repo;
|
||||
std::unique_ptr<MOBase::IRepositoryDownload> m_download;
|
||||
QString m_what;
|
||||
States m_state;
|
||||
QString m_error;
|
||||
|
||||
void setState(States s);
|
||||
void next();
|
||||
};
|
||||
|
||||
|
||||
class DownloadManager2
|
||||
{
|
||||
public:
|
||||
static const std::size_t NoLimit =
|
||||
std::numeric_limits<std::size_t>::max();
|
||||
|
||||
DownloadManager2(PluginContainer& pc);
|
||||
~DownloadManager2();
|
||||
|
||||
void add(QString what);
|
||||
|
||||
void maxActive(std::size_t n);
|
||||
void maxSpeed(std::size_t bytesPerSecond);
|
||||
|
||||
bool hasActive() const;
|
||||
|
||||
curl::Downloader& downloader();
|
||||
|
||||
private:
|
||||
using DownloadList = std::list<std::shared_ptr<Download>>;
|
||||
|
||||
PluginContainer& m_pc;
|
||||
|
||||
std::thread m_thread;
|
||||
std::atomic<bool> m_stop;
|
||||
|
||||
std::unique_ptr<curl::Downloader> m_downloader;
|
||||
|
||||
std::vector<std::shared_ptr<Download>> m_temp;
|
||||
std::mutex m_tempMutex;
|
||||
std::condition_variable m_cv;
|
||||
|
||||
DownloadList m_queued, m_active, m_inactive;
|
||||
std::atomic<std::size_t> m_maxActive, m_maxSpeed;
|
||||
std::atomic<bool> m_hasActive;
|
||||
|
||||
|
||||
void run();
|
||||
void checkTemp();
|
||||
void checkQueue();
|
||||
void cleanupActive();
|
||||
void stopOverMax(std::size_t max);
|
||||
void addFromQueue(std::size_t max);
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
#endif // MODORGANIZER_DOWNLOADEDMANAGER2_INCLUDED
|
||||
+3
-5
@@ -418,9 +418,9 @@ QString prependToPath(const QString& s)
|
||||
return old;
|
||||
}
|
||||
|
||||
QString setPath(const QString& s)
|
||||
void setPath(const QString& s)
|
||||
{
|
||||
return set("PATH", s);
|
||||
set("PATH", s);
|
||||
}
|
||||
|
||||
QString get(const QString& name)
|
||||
@@ -457,11 +457,9 @@ QString get(const QString& name)
|
||||
return QString::fromWCharArray(buffer.get(), realSize);
|
||||
}
|
||||
|
||||
QString set(const QString& n, const QString& v)
|
||||
void set(const QString& n, const QString& v)
|
||||
{
|
||||
auto old = get(n);
|
||||
::SetEnvironmentVariableW(n.toStdWString().c_str(), v.toStdWString().c_str());
|
||||
return old;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -233,12 +233,12 @@ private:
|
||||
// environment variables
|
||||
//
|
||||
QString get(const QString& name);
|
||||
QString set(const QString& name, const QString& value);
|
||||
void set(const QString& name, const QString& value);
|
||||
|
||||
QString path();
|
||||
QString appendToPath(const QString& s);
|
||||
QString prependToPath(const QString& s);
|
||||
QString setPath(const QString& s);
|
||||
void setPath(const QString& s);
|
||||
|
||||
|
||||
class Service
|
||||
|
||||
+21
-5
@@ -603,9 +603,16 @@ Process getProcessTreeFromProcess(HANDLE h)
|
||||
|
||||
std::vector<DWORD> processesInJob(HANDLE h)
|
||||
{
|
||||
for (int tries=0; tries<5; ++tries) {
|
||||
DWORD maxIds = 100;
|
||||
const int MaxTries = 5;
|
||||
|
||||
// doubled MaxTries times on failure
|
||||
DWORD maxIds = 100;
|
||||
|
||||
// for logging
|
||||
DWORD lastCount=0, lastAssigned=0;
|
||||
|
||||
|
||||
for (int tries=0; tries<MaxTries; ++tries) {
|
||||
const DWORD idsSize = sizeof(ULONG_PTR) * maxIds;
|
||||
const DWORD bufferSize = sizeof(JOBOBJECT_BASIC_PROCESS_ID_LIST) + idsSize;
|
||||
|
||||
@@ -617,8 +624,10 @@ std::vector<DWORD> processesInJob(HANDLE h)
|
||||
|
||||
if (!r) {
|
||||
const auto e = GetLastError();
|
||||
log::error("failed to get process ids in job, {}", formatSystemMessage(e));
|
||||
return {};
|
||||
if (e != ERROR_MORE_DATA) {
|
||||
log::error("failed to get process ids in job, {}", formatSystemMessage(e));
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
if (ids->NumberOfProcessIdsInList >= ids->NumberOfAssignedProcesses) {
|
||||
@@ -632,9 +641,16 @@ std::vector<DWORD> processesInJob(HANDLE h)
|
||||
|
||||
// try again with a larger buffer
|
||||
maxIds *= 2;
|
||||
|
||||
// for logging
|
||||
lastCount = ids->NumberOfProcessIdsInList;
|
||||
lastAssigned = ids->NumberOfAssignedProcesses;
|
||||
}
|
||||
|
||||
log::error("failed to get processes in job, can't get a buffer large enough");
|
||||
log::error(
|
||||
"failed to get processes in job, can't get a buffer large enough, "
|
||||
"{}/{} ids", lastCount, lastAssigned);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -145,11 +145,13 @@ Shortcut::Shortcut()
|
||||
Shortcut::Shortcut(const Executable& exe)
|
||||
: Shortcut()
|
||||
{
|
||||
const auto i = *InstanceManager::singleton().currentInstance();
|
||||
|
||||
m_name = exe.title();
|
||||
m_target = QFileInfo(qApp->applicationFilePath()).absoluteFilePath();
|
||||
|
||||
m_arguments = QString("\"moshortcut://%1:%2\"")
|
||||
.arg(InstanceManager::singleton().currentInstance()->name())
|
||||
.arg(i.isPortable() ? "" : i.displayName())
|
||||
.arg(exe.title());
|
||||
|
||||
m_description = QString("Run %1 with ModOrganizer").arg(exe.title());
|
||||
|
||||
@@ -48,7 +48,7 @@ Instance::Instance(QString dir, bool portable, QString profileName) :
|
||||
{
|
||||
}
|
||||
|
||||
QString Instance::name() const
|
||||
QString Instance::displayName() const
|
||||
{
|
||||
if (isPortable())
|
||||
return QObject::tr("Portable");
|
||||
@@ -105,7 +105,7 @@ bool Instance::isActive() const
|
||||
if (m_portable) {
|
||||
return i->isPortable();
|
||||
} else {
|
||||
return (i->name() == name());
|
||||
return (i->displayName() == displayName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -822,7 +822,7 @@ SetupInstanceResults selectGame(Instance& instance, PluginContainer& pc)
|
||||
CreateInstanceDialog dlg(pc, nullptr);
|
||||
|
||||
// only show the game page
|
||||
dlg.setSinglePage<cid::GamePage>(instance.name());
|
||||
dlg.setSinglePage<cid::GamePage>(instance.displayName());
|
||||
|
||||
dlg.show();
|
||||
dlg.activateWindow();
|
||||
@@ -860,7 +860,7 @@ SetupInstanceResults selectVariant(Instance& instance, PluginContainer& pc)
|
||||
instance.gamePlugin(), instance.gameDirectory());
|
||||
|
||||
// only show the variant page
|
||||
dlg.setSinglePage<cid::VariantsPage>(instance.name());
|
||||
dlg.setSinglePage<cid::VariantsPage>(instance.displayName());
|
||||
|
||||
dlg.show();
|
||||
dlg.activateWindow();
|
||||
@@ -897,7 +897,7 @@ SetupInstanceResults setupInstance(Instance& instance, PluginContainer& pc)
|
||||
|
||||
reportError(
|
||||
QObject::tr("Cannot open instance '%1', failed to read INI file %2.")
|
||||
.arg(instance.name()).arg(instance.iniPath()));
|
||||
.arg(instance.displayName()).arg(instance.iniPath()));
|
||||
|
||||
return SetupInstanceResults::SelectAnother;
|
||||
}
|
||||
@@ -914,7 +914,7 @@ SetupInstanceResults setupInstance(Instance& instance, PluginContainer& pc)
|
||||
QObject::tr(
|
||||
"Cannot open instance '%1', the managed game was not found in the INI "
|
||||
"file %2. Select the game managed by this instance.")
|
||||
.arg(instance.name()).arg(instance.iniPath()));
|
||||
.arg(instance.displayName()).arg(instance.iniPath()));
|
||||
|
||||
return selectGame(instance, pc);
|
||||
}
|
||||
@@ -928,7 +928,7 @@ SetupInstanceResults setupInstance(Instance& instance, PluginContainer& pc)
|
||||
QObject::tr(
|
||||
"Cannot open instance '%1', the game plugin '%2' doesn't exist. It "
|
||||
"may have been deleted by an antivirus. Select another instance.")
|
||||
.arg(instance.name()).arg(instance.gameName()));
|
||||
.arg(instance.displayName()).arg(instance.gameName()));
|
||||
|
||||
return SetupInstanceResults::SelectAnother;
|
||||
}
|
||||
@@ -943,7 +943,7 @@ SetupInstanceResults setupInstance(Instance& instance, PluginContainer& pc)
|
||||
"Cannot open instance '%1', the game directory '%2' doesn't exist or "
|
||||
"the game plugin '%3' doesn't recognize it. Select the game managed "
|
||||
"by this instance.")
|
||||
.arg(instance.name())
|
||||
.arg(instance.displayName())
|
||||
.arg(instance.gameDirectory())
|
||||
.arg(instance.gameName()));
|
||||
|
||||
|
||||
@@ -132,9 +132,14 @@ public:
|
||||
// returns the instance name; this is the directory name or "Portable" for
|
||||
// portable instances
|
||||
//
|
||||
// be careful when using this function to check whether two instances are the
|
||||
// same, some parts of MO use an empty string to represent portable instances,
|
||||
// but this function will return "Portable" for them; it's safer to check
|
||||
// for isPortable() first
|
||||
//
|
||||
// can be called without setup()
|
||||
//
|
||||
QString name() const;
|
||||
QString displayName() const;
|
||||
|
||||
// returns either:
|
||||
// 1) the game name from the INI, if readFromIni() was called;
|
||||
|
||||
@@ -221,7 +221,7 @@ void InstanceManagerDialog::updateInstances()
|
||||
|
||||
// sort first, prepend portable after so it's always on top
|
||||
std::sort(m_instances.begin(), m_instances.end(), [](auto&& a, auto&& b) {
|
||||
return (MOBase::naturalCompare(a->name(), b->name()) < 0);
|
||||
return (MOBase::naturalCompare(a->displayName(), b->displayName()) < 0);
|
||||
});
|
||||
|
||||
if (m.portableInstanceExists()) {
|
||||
@@ -249,7 +249,7 @@ void InstanceManagerDialog::updateList()
|
||||
for (std::size_t i=0; i<m_instances.size(); ++i) {
|
||||
const auto& ii = *m_instances[i];
|
||||
|
||||
auto* item = new QStandardItem(ii.name());
|
||||
auto* item = new QStandardItem(ii.displayName());
|
||||
item->setIcon(instanceIcon(m_pc, ii));
|
||||
|
||||
m_model->appendRow(item);
|
||||
@@ -295,7 +295,7 @@ void InstanceManagerDialog::select(std::size_t i)
|
||||
void InstanceManagerDialog::select(const QString& name)
|
||||
{
|
||||
for (std::size_t i=0; i<m_instances.size(); ++i) {
|
||||
if (m_instances[i]->name() == name) {
|
||||
if (m_instances[i]->displayName() == name) {
|
||||
select(i);
|
||||
return;
|
||||
}
|
||||
@@ -310,7 +310,7 @@ void InstanceManagerDialog::selectActiveInstance()
|
||||
|
||||
if (active) {
|
||||
for (std::size_t i=0; i<m_instances.size(); ++i) {
|
||||
if (m_instances[i]->name() == active->name()) {
|
||||
if (m_instances[i]->displayName() == active->displayName()) {
|
||||
select(i);
|
||||
|
||||
ui->list->scrollTo(
|
||||
@@ -340,7 +340,7 @@ void InstanceManagerDialog::openSelectedInstance()
|
||||
if (to.isPortable()) {
|
||||
InstanceManager::singleton().setCurrentInstance("");
|
||||
} else {
|
||||
InstanceManager::singleton().setCurrentInstance(to.name());
|
||||
InstanceManager::singleton().setCurrentInstance(to.displayName());
|
||||
}
|
||||
|
||||
if (m_restartOnSelect) {
|
||||
@@ -372,7 +372,7 @@ bool InstanceManagerDialog::confirmSwitch(const Instance& to)
|
||||
const auto r = dlg
|
||||
.title(tr("Switching instances"))
|
||||
.main(tr("Mod Organizer must restart to manage the instance '%1'.")
|
||||
.arg(to.name()))
|
||||
.arg(to.displayName()))
|
||||
.content(tr("This confirmation can be disabled in the settings."))
|
||||
.icon(QMessageBox::Question)
|
||||
.button({tr("Restart Mod Organizer"), QMessageBox::Ok})
|
||||
@@ -401,7 +401,7 @@ void InstanceManagerDialog::rename()
|
||||
|
||||
// getting new name
|
||||
const auto newName = getInstanceName(
|
||||
this, tr("Rename instance"), "", tr("Instance name"), i->name());
|
||||
this, tr("Rename instance"), "", tr("Instance name"), i->displayName());
|
||||
|
||||
if (newName.isEmpty()) {
|
||||
return;
|
||||
@@ -661,7 +661,7 @@ const Instance* InstanceManagerDialog::singleSelection() const
|
||||
|
||||
void InstanceManagerDialog::fillData(const Instance& ii)
|
||||
{
|
||||
ui->name->setText(ii.name());
|
||||
ui->name->setText(ii.displayName());
|
||||
ui->location->setText(ii.directory());
|
||||
ui->baseDirectory->setText(ii.baseDirectory());
|
||||
ui->gameName->setText(ii.gameName());
|
||||
|
||||
+8
-2
@@ -7,6 +7,7 @@
|
||||
#include "instancemanager.h"
|
||||
#include "thread_utils.h"
|
||||
#include "shared/util.h"
|
||||
#include "downloadmanager2.h"
|
||||
#include <report.h>
|
||||
#include <log.h>
|
||||
|
||||
@@ -80,7 +81,8 @@ int run(int argc, char *argv[])
|
||||
// stuff that's done only once, even if MO restarts in the loop below
|
||||
app.firstTimeSetup(multiProcess);
|
||||
|
||||
// force the "Select instance" dialog on startup (only for first loop)
|
||||
// force the "Select instance" dialog on startup, only for first loop or when
|
||||
// the current instance cannot be used
|
||||
bool pick = cl.pick();
|
||||
|
||||
// MO runs in a loop because it can be restarted in several ways, such as
|
||||
@@ -105,13 +107,17 @@ int run(int argc, char *argv[])
|
||||
const auto r = app.setup(multiProcess, pick);
|
||||
pick = false;
|
||||
|
||||
if (r == RestartExitCode) {
|
||||
if (r == RestartExitCode || r == ReselectExitCode) {
|
||||
// resets things when MO is "restarted"
|
||||
app.resetForRestart();
|
||||
|
||||
// don't reprocess command line
|
||||
cl.clear();
|
||||
|
||||
if (r == ReselectExitCode) {
|
||||
pick = true;
|
||||
}
|
||||
|
||||
continue;
|
||||
} else if (r != 0) {
|
||||
// something failed, quit
|
||||
|
||||
+1
-5
@@ -2951,7 +2951,6 @@ void MainWindow::nxmModInfoAvailable(QString gameName, int modID, QVariant userD
|
||||
}
|
||||
std::vector<ModInfo::Ptr> modsList = ModInfo::getByModID(gameNameReal, modID);
|
||||
for (auto mod : modsList) {
|
||||
bool foundUpdate = false;
|
||||
QDateTime now = QDateTime::currentDateTimeUtc();
|
||||
QDateTime updateTarget = mod->getExpires();
|
||||
if (now >= updateTarget) {
|
||||
@@ -2959,7 +2958,6 @@ void MainWindow::nxmModInfoAvailable(QString gameName, int modID, QVariant userD
|
||||
// with an older version than the main mod version.
|
||||
if (mod->getNexusFileStatus() != 3 && mod->getNexusFileStatus() != 5) {
|
||||
mod->setNewestVersion(result["version"].toString());
|
||||
foundUpdate = true;
|
||||
}
|
||||
// update the LastNexusUpdate time in any case since we did perform the check.
|
||||
mod->setLastNexusUpdate(QDateTime::currentDateTimeUtc());
|
||||
@@ -2979,9 +2977,7 @@ void MainWindow::nxmModInfoAvailable(QString gameName, int modID, QVariant userD
|
||||
mod->setNexusLastModified(QDateTime::fromSecsSinceEpoch(result["updated_timestamp"].toInt(), Qt::UTC));
|
||||
mod->saveMeta();
|
||||
|
||||
if (foundUpdate) {
|
||||
m_OrganizerCore.modList()->notifyChange(ModInfo::getIndex(mod->name()));
|
||||
}
|
||||
m_OrganizerCore.modList()->notifyChange(ModInfo::getIndex(mod->name()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
-7
@@ -392,10 +392,14 @@ void MOApplication::externalMessage(const QString& message)
|
||||
|
||||
if (moshortcut.isValid()) {
|
||||
if(moshortcut.hasExecutable()) {
|
||||
m_core->processRunner()
|
||||
.setFromShortcut(moshortcut)
|
||||
.setWaitForCompletion(ProcessRunner::TriggerRefresh)
|
||||
.run();
|
||||
try {
|
||||
m_core->processRunner()
|
||||
.setFromShortcut(moshortcut)
|
||||
.setWaitForCompletion(ProcessRunner::TriggerRefresh)
|
||||
.run();
|
||||
} catch(std::exception&) {
|
||||
// user was already warned
|
||||
}
|
||||
}
|
||||
} else if (isNxmLink(message)) {
|
||||
MessageDialog::showMessage(tr("Download started"), qApp->activeWindow(), false);
|
||||
@@ -414,11 +418,11 @@ void MOApplication::externalMessage(const QString& message)
|
||||
if (auto i=cl.instance()) {
|
||||
const auto ci = InstanceManager::singleton().currentInstance();
|
||||
|
||||
if (*i != ci->name()) {
|
||||
if (*i != ci->displayName()) {
|
||||
reportError(tr(
|
||||
"This shortcut or command line is for instance '%1', but the current "
|
||||
"instance is '%2'.")
|
||||
.arg(*i).arg(ci->name()));
|
||||
.arg(*i).arg(ci->displayName()));
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -488,7 +492,7 @@ std::optional<int> MOApplication::setupInstanceLoop(
|
||||
continue;
|
||||
} else if (setupResult == SetupInstanceResults::SelectAnother) {
|
||||
InstanceManager::singleton().clearCurrentInstance();
|
||||
return RestartExitCode;
|
||||
return ReselectExitCode;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -897,11 +897,6 @@ public: // Conflicts
|
||||
//
|
||||
virtual const std::set<unsigned int>& getModArchiveLooseOverwritten() const { return s_EmptySet; }
|
||||
|
||||
/**
|
||||
* @brief Update conflict information.
|
||||
*/
|
||||
virtual void doConflictCheck() const {}
|
||||
|
||||
public slots:
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,11 +19,11 @@ ModInfoWithConflictInfo::ModInfoWithConflictInfo(OrganizerCore& core) :
|
||||
m_FileTree([this]() { return QDirFileTree::makeTree(absolutePath()); }),
|
||||
m_Valid([this]() { return doIsValid(); }),
|
||||
m_Contents([this]() { return doGetContents(); }),
|
||||
m_HasLooseOverwrite(false), m_HasHiddenFiles(false) {}
|
||||
m_Conflicts([this]() { return doConflictCheck(); }) { }
|
||||
|
||||
void ModInfoWithConflictInfo::clearCaches()
|
||||
{
|
||||
m_LastConflictCheck = QTime();
|
||||
m_Conflicts.invalidate();
|
||||
}
|
||||
|
||||
std::vector<ModInfo::EFlag> ModInfoWithConflictInfo::getFlags() const
|
||||
@@ -82,14 +82,9 @@ std::vector<ModInfo::EConflictFlag> ModInfoWithConflictInfo::getConflictFlags()
|
||||
}
|
||||
|
||||
|
||||
void ModInfoWithConflictInfo::doConflictCheck() const
|
||||
ModInfoWithConflictInfo::Conflicts ModInfoWithConflictInfo::doConflictCheck() const
|
||||
{
|
||||
m_OverwriteList.clear();
|
||||
m_OverwrittenList.clear();
|
||||
m_ArchiveOverwriteList.clear();
|
||||
m_ArchiveOverwrittenList.clear();
|
||||
m_ArchiveLooseOverwriteList.clear();
|
||||
m_ArchiveLooseOverwrittenList.clear();
|
||||
Conflicts conflicts;
|
||||
|
||||
bool providesAnything = false;
|
||||
bool hasHiddenFiles = false;
|
||||
@@ -102,10 +97,6 @@ void ModInfoWithConflictInfo::doConflictCheck() const
|
||||
std::wstring name = ToWString(this->name());
|
||||
const std::wstring hideExt = ToWString(ModInfo::s_HiddenExt);
|
||||
|
||||
m_CurrentConflictState = CONFLICT_NONE;
|
||||
m_ArchiveConflictState = CONFLICT_NONE;
|
||||
m_ArchiveConflictLooseState = CONFLICT_NONE;
|
||||
|
||||
if (m_Core.directoryStructure()->originExists(name)) {
|
||||
FilesOrigin &origin = m_Core.directoryStructure()->getOriginByName(name);
|
||||
std::vector<FileEntryPtr> files = origin.getFiles();
|
||||
@@ -168,12 +159,12 @@ void ModInfoWithConflictInfo::doConflictCheck() const
|
||||
unsigned int altIndex = ModInfo::getIndex(ToQString(altOrigin.getName()));
|
||||
if (!file->isFromArchive()) {
|
||||
if (!archiveData.isValid())
|
||||
m_OverwrittenList.insert(altIndex);
|
||||
conflicts.m_OverwrittenList.insert(altIndex);
|
||||
else
|
||||
m_ArchiveLooseOverwrittenList.insert(altIndex);
|
||||
conflicts.m_ArchiveLooseOverwrittenList.insert(altIndex);
|
||||
}
|
||||
else {
|
||||
m_ArchiveOverwrittenList.insert(altIndex);
|
||||
conflicts.m_ArchiveOverwrittenList.insert(altIndex);
|
||||
}
|
||||
} else {
|
||||
providesAnything = true;
|
||||
@@ -188,21 +179,21 @@ void ModInfoWithConflictInfo::doConflictCheck() const
|
||||
if (!altInfo.isFromArchive()) {
|
||||
if (!archiveData.isValid()) {
|
||||
if (origin.getPriority() > altOrigin.getPriority()) {
|
||||
m_OverwriteList.insert(altIndex);
|
||||
conflicts.m_OverwriteList.insert(altIndex);
|
||||
} else {
|
||||
m_OverwrittenList.insert(altIndex);
|
||||
conflicts.m_OverwrittenList.insert(altIndex);
|
||||
}
|
||||
} else {
|
||||
m_ArchiveLooseOverwrittenList.insert(altIndex);
|
||||
conflicts.m_ArchiveLooseOverwrittenList.insert(altIndex);
|
||||
}
|
||||
} else {
|
||||
if (!archiveData.isValid()) {
|
||||
m_ArchiveLooseOverwriteList.insert(altIndex);
|
||||
conflicts.m_ArchiveLooseOverwriteList.insert(altIndex);
|
||||
} else {
|
||||
if (archiveData.order() > altInfo.archive().order()) {
|
||||
m_ArchiveOverwriteList.insert(altIndex);
|
||||
conflicts.m_ArchiveOverwriteList.insert(altIndex);
|
||||
} else if (archiveData.order() < altInfo.archive().order()) {
|
||||
m_ArchiveOverwrittenList.insert(altIndex);
|
||||
conflicts.m_ArchiveOverwrittenList.insert(altIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -210,66 +201,51 @@ void ModInfoWithConflictInfo::doConflictCheck() const
|
||||
}
|
||||
}
|
||||
}
|
||||
m_LastConflictCheck = QTime::currentTime();
|
||||
|
||||
if (files.size() != 0) {
|
||||
if (!providesAnything)
|
||||
m_CurrentConflictState = CONFLICT_REDUNDANT;
|
||||
else if (!m_OverwriteList.empty() && !m_OverwrittenList.empty())
|
||||
m_CurrentConflictState = CONFLICT_MIXED;
|
||||
else if (!m_OverwriteList.empty())
|
||||
m_CurrentConflictState = CONFLICT_OVERWRITE;
|
||||
else if (!m_OverwrittenList.empty())
|
||||
m_CurrentConflictState = CONFLICT_OVERWRITTEN;
|
||||
conflicts.m_CurrentConflictState = CONFLICT_REDUNDANT;
|
||||
else if (!conflicts.m_OverwriteList.empty() && !conflicts.m_OverwrittenList.empty())
|
||||
conflicts.m_CurrentConflictState = CONFLICT_MIXED;
|
||||
else if (!conflicts.m_OverwriteList.empty())
|
||||
conflicts.m_CurrentConflictState = CONFLICT_OVERWRITE;
|
||||
else if (!conflicts.m_OverwrittenList.empty())
|
||||
conflicts.m_CurrentConflictState = CONFLICT_OVERWRITTEN;
|
||||
|
||||
if (!m_ArchiveOverwriteList.empty() && !m_ArchiveOverwrittenList.empty())
|
||||
m_ArchiveConflictState = CONFLICT_MIXED;
|
||||
else if (!m_ArchiveOverwriteList.empty())
|
||||
m_ArchiveConflictState = CONFLICT_OVERWRITE;
|
||||
else if (!m_ArchiveOverwrittenList.empty())
|
||||
m_ArchiveConflictState = CONFLICT_OVERWRITTEN;
|
||||
if (!conflicts.m_ArchiveOverwriteList.empty() && !conflicts.m_ArchiveOverwrittenList.empty())
|
||||
conflicts.m_ArchiveConflictState = CONFLICT_MIXED;
|
||||
else if (!conflicts.m_ArchiveOverwriteList.empty())
|
||||
conflicts.m_ArchiveConflictState = CONFLICT_OVERWRITE;
|
||||
else if (!conflicts.m_ArchiveOverwrittenList.empty())
|
||||
conflicts.m_ArchiveConflictState = CONFLICT_OVERWRITTEN;
|
||||
|
||||
if (!m_ArchiveLooseOverwrittenList.empty() && !m_ArchiveLooseOverwriteList.empty())
|
||||
m_ArchiveConflictLooseState = CONFLICT_MIXED;
|
||||
else if (!m_ArchiveLooseOverwrittenList.empty())
|
||||
m_ArchiveConflictLooseState = CONFLICT_OVERWRITTEN;
|
||||
else if (!m_ArchiveLooseOverwriteList.empty())
|
||||
m_ArchiveConflictLooseState = CONFLICT_OVERWRITE;
|
||||
if (!conflicts.m_ArchiveLooseOverwrittenList.empty() && !conflicts.m_ArchiveLooseOverwriteList.empty())
|
||||
conflicts.m_ArchiveConflictLooseState = CONFLICT_MIXED;
|
||||
else if (!conflicts.m_ArchiveLooseOverwrittenList.empty())
|
||||
conflicts.m_ArchiveConflictLooseState = CONFLICT_OVERWRITTEN;
|
||||
else if (!conflicts.m_ArchiveLooseOverwriteList.empty())
|
||||
conflicts.m_ArchiveConflictLooseState = CONFLICT_OVERWRITE;
|
||||
|
||||
m_HasHiddenFiles = hasHiddenFiles;
|
||||
conflicts.m_HasHiddenFiles = hasHiddenFiles;
|
||||
}
|
||||
}
|
||||
|
||||
return conflicts;
|
||||
}
|
||||
|
||||
ModInfoWithConflictInfo::EConflictType ModInfoWithConflictInfo::isConflicted() const
|
||||
{
|
||||
// this is costy so cache the result
|
||||
QTime now = QTime::currentTime();
|
||||
if (m_LastConflictCheck.isNull() || (m_LastConflictCheck.secsTo(now) > 10)) {
|
||||
doConflictCheck();
|
||||
}
|
||||
|
||||
return m_CurrentConflictState;
|
||||
return m_Conflicts.value().m_CurrentConflictState;
|
||||
}
|
||||
|
||||
ModInfoWithConflictInfo::EConflictType ModInfoWithConflictInfo::isArchiveConflicted() const
|
||||
{
|
||||
QTime now = QTime::currentTime();
|
||||
if (m_LastConflictCheck.isNull() || (m_LastConflictCheck.secsTo(now) > 10)) {
|
||||
doConflictCheck();
|
||||
}
|
||||
|
||||
return m_ArchiveConflictState;
|
||||
return m_Conflicts.value().m_ArchiveConflictState;
|
||||
}
|
||||
|
||||
ModInfoWithConflictInfo::EConflictType ModInfoWithConflictInfo::isLooseArchiveConflicted() const
|
||||
{
|
||||
QTime now = QTime::currentTime();
|
||||
if (m_LastConflictCheck.isNull() || (m_LastConflictCheck.secsTo(now) > 10)) {
|
||||
doConflictCheck();
|
||||
}
|
||||
|
||||
return m_ArchiveConflictLooseState;
|
||||
return m_Conflicts.value().m_ArchiveConflictLooseState;
|
||||
}
|
||||
|
||||
|
||||
@@ -294,12 +270,7 @@ bool ModInfoWithConflictInfo::isRedundant() const
|
||||
|
||||
bool ModInfoWithConflictInfo::hasHiddenFiles() const
|
||||
{
|
||||
QTime now = QTime::currentTime();
|
||||
if (m_LastConflictCheck.isNull() || (m_LastConflictCheck.secsTo(now) > 10)) {
|
||||
doConflictCheck();
|
||||
}
|
||||
|
||||
return m_HasHiddenFiles;
|
||||
return m_Conflicts.value().m_HasHiddenFiles;
|
||||
}
|
||||
|
||||
void ModInfoWithConflictInfo::diskContentModified() {
|
||||
|
||||
@@ -54,14 +54,12 @@ public:
|
||||
*/
|
||||
void clearCaches() override;
|
||||
|
||||
const std::set<unsigned int>& getModOverwrite() const override { return m_OverwriteList; }
|
||||
const std::set<unsigned int>& getModOverwritten() const override { return m_OverwrittenList; }
|
||||
const std::set<unsigned int>& getModArchiveOverwrite() const override { return m_ArchiveOverwriteList; }
|
||||
const std::set<unsigned int>& getModArchiveOverwritten() const override { return m_ArchiveOverwrittenList; }
|
||||
const std::set<unsigned int>& getModArchiveLooseOverwrite() const override { return m_ArchiveLooseOverwriteList; }
|
||||
const std::set<unsigned int>& getModArchiveLooseOverwritten() const override { return m_ArchiveLooseOverwrittenList; }
|
||||
|
||||
void doConflictCheck() const override;
|
||||
const std::set<unsigned int>& getModOverwrite() const override { return m_Conflicts.value().m_OverwriteList; }
|
||||
const std::set<unsigned int>& getModOverwritten() const override { return m_Conflicts.value().m_OverwrittenList; }
|
||||
const std::set<unsigned int>& getModArchiveOverwrite() const override { return m_Conflicts.value().m_ArchiveOverwriteList; }
|
||||
const std::set<unsigned int>& getModArchiveOverwritten() const override { return m_Conflicts.value().m_ArchiveOverwrittenList; }
|
||||
const std::set<unsigned int>& getModArchiveLooseOverwrite() const override { return m_Conflicts.value().m_ArchiveLooseOverwriteList; }
|
||||
const std::set<unsigned int>& getModArchiveLooseOverwritten() const override { return m_Conflicts.value().m_ArchiveLooseOverwrittenList; }
|
||||
|
||||
public slots:
|
||||
|
||||
@@ -72,11 +70,8 @@ public slots:
|
||||
|
||||
protected:
|
||||
|
||||
/**
|
||||
* @brief Check if the content of this mod is valid.
|
||||
*
|
||||
* @return true if the content is valid, false otherwise.
|
||||
**/
|
||||
// check if the content of this mod is valid
|
||||
//
|
||||
virtual bool doIsValid() const;
|
||||
|
||||
/**
|
||||
@@ -136,23 +131,27 @@ protected:
|
||||
|
||||
private:
|
||||
|
||||
struct Conflicts {
|
||||
EConflictType m_CurrentConflictState = CONFLICT_NONE;
|
||||
EConflictType m_ArchiveConflictState = CONFLICT_NONE;
|
||||
EConflictType m_ArchiveConflictLooseState = CONFLICT_NONE;
|
||||
bool m_HasLooseOverwrite = false;
|
||||
bool m_HasHiddenFiles = false;
|
||||
|
||||
std::set<unsigned int> m_OverwriteList; // indices of mods overritten by this mod
|
||||
std::set<unsigned int> m_OverwrittenList; // indices of mods overwriting this mod
|
||||
std::set<unsigned int> m_ArchiveOverwriteList; // indices of mods with archive files overritten by this mod
|
||||
std::set<unsigned int> m_ArchiveOverwrittenList; // indices of mods with archive files overwriting this mod
|
||||
std::set<unsigned int> m_ArchiveLooseOverwriteList; // indices of mods with archives being overwritten by this mod's loose files
|
||||
std::set<unsigned int> m_ArchiveLooseOverwrittenList; // indices of mods with loose files overwriting this mod's archive files
|
||||
};
|
||||
|
||||
Conflicts doConflictCheck() const;
|
||||
|
||||
MOBase::MemoizedLocked<std::shared_ptr<const MOBase::IFileTree>> m_FileTree;
|
||||
MOBase::MemoizedLocked<bool> m_Valid;
|
||||
MOBase::MemoizedLocked<std::set<int>> m_Contents;
|
||||
|
||||
mutable EConflictType m_CurrentConflictState;
|
||||
mutable EConflictType m_ArchiveConflictState;
|
||||
mutable EConflictType m_ArchiveConflictLooseState;
|
||||
mutable bool m_HasLooseOverwrite;
|
||||
mutable bool m_HasHiddenFiles;
|
||||
mutable QTime m_LastConflictCheck;
|
||||
|
||||
mutable std::set<unsigned int> m_OverwriteList; // indices of mods overritten by this mod
|
||||
mutable std::set<unsigned int> m_OverwrittenList; // indices of mods overwriting this mod
|
||||
mutable std::set<unsigned int> m_ArchiveOverwriteList; // indices of mods with archive files overritten by this mod
|
||||
mutable std::set<unsigned int> m_ArchiveOverwrittenList; // indices of mods with archive files overwriting this mod
|
||||
mutable std::set<unsigned int> m_ArchiveLooseOverwriteList; // indices of mods with archives being overwritten by this mod's loose files
|
||||
mutable std::set<unsigned int> m_ArchiveLooseOverwrittenList; // indices of mods with loose files overwriting this mod's archive files
|
||||
MOBase::MemoizedLocked<Conflicts> m_Conflicts;
|
||||
|
||||
};
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user