[fs/core] initial external content without NAND install
This commit is contained in:
@@ -759,6 +759,7 @@ struct Values {
|
||||
|
||||
// Add-Ons
|
||||
std::map<u64, std::vector<std::string>> disabled_addons;
|
||||
std::vector<std::string> external_dirs;
|
||||
};
|
||||
|
||||
extern Values values;
|
||||
|
||||
@@ -54,6 +54,8 @@ add_library(core STATIC
|
||||
file_sys/control_metadata.cpp
|
||||
file_sys/control_metadata.h
|
||||
file_sys/errors.h
|
||||
file_sys/external_content_index.cpp
|
||||
file_sys/external_content_index.h
|
||||
file_sys/fs_directory.h
|
||||
file_sys/fs_file.h
|
||||
file_sys/fs_filesystem.h
|
||||
|
||||
271
src/core/file_sys/external_content_index.cpp
Normal file
271
src/core/file_sys/external_content_index.cpp
Normal file
@@ -0,0 +1,271 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "core/file_sys/external_content_index.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <cctype>
|
||||
#include "common/hex_util.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/fs/fs_util.h"
|
||||
#include "core/file_sys/nca_metadata.h"
|
||||
#include "core/file_sys/romfs.h"
|
||||
#include "core/file_sys/registered_cache.h"
|
||||
#include "core/file_sys/vfs/vfs.h"
|
||||
#include "core/file_sys/content_archive.h"
|
||||
#include "core/file_sys/submission_package.h"
|
||||
#include "core/loader/loader.h"
|
||||
#include "core/file_sys/common_funcs.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
ExternalContentIndexer::ExternalContentIndexer(VirtualFilesystem vfs,
|
||||
ManualContentProvider& provider,
|
||||
ExternalContentPaths paths)
|
||||
: m_vfs(std::move(vfs)), m_provider(provider), m_paths(std::move(paths)) {}
|
||||
|
||||
void ExternalContentIndexer::Rebuild() {
|
||||
m_provider.ClearAllEntries();
|
||||
m_best_update_by_title.clear();
|
||||
m_all_dlc.clear();
|
||||
|
||||
for (const auto& dir : m_paths.update_dirs) {
|
||||
IndexUpdatesDir(dir);
|
||||
}
|
||||
for (const auto& dir : m_paths.dlc_dirs) {
|
||||
IndexDlcDir(dir);
|
||||
}
|
||||
|
||||
Commit();
|
||||
}
|
||||
|
||||
static std::string ToLowerCopy(const std::string& s) {
|
||||
std::string out;
|
||||
out.resize(s.size());
|
||||
std::transform(s.begin(), s.end(), out.begin(), [](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
|
||||
return out;
|
||||
}
|
||||
|
||||
void ExternalContentIndexer::IndexUpdatesDir(const std::string& dir) {
|
||||
try {
|
||||
const fs::path p = Common::FS::ToU8String(dir);
|
||||
std::error_code ec;
|
||||
if (!fs::exists(p, ec) || ec)
|
||||
return;
|
||||
|
||||
if (fs::is_directory(p, ec) && !ec) {
|
||||
for (const auto& entry : fs::recursive_directory_iterator(p, fs::directory_options::skip_permission_denied, ec)) {
|
||||
if (entry.is_directory(ec)) continue;
|
||||
TryIndexFileAsContainer(Common::FS::ToUTF8String(entry.path().u8string()), true);
|
||||
}
|
||||
TryIndexLooseDir(Common::FS::ToUTF8String(p.u8string()), true);
|
||||
} else {
|
||||
TryIndexFileAsContainer(Common::FS::ToUTF8String(p.u8string()), true);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(Loader, "Error accessing update directory '{}': {}", dir, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void ExternalContentIndexer::IndexDlcDir(const std::string& dir) {
|
||||
try {
|
||||
const fs::path p = Common::FS::ToU8String(dir);
|
||||
std::error_code ec;
|
||||
if (!fs::exists(p, ec) || ec)
|
||||
return;
|
||||
|
||||
if (fs::is_directory(p, ec) && !ec) {
|
||||
for (const auto& entry : fs::recursive_directory_iterator(p, fs::directory_options::skip_permission_denied, ec)) {
|
||||
if (entry.is_directory(ec)) continue;
|
||||
TryIndexFileAsContainer(Common::FS::ToUTF8String(entry.path().u8string()), false);
|
||||
}
|
||||
TryIndexLooseDir(Common::FS::ToUTF8String(p.u8string()), false);
|
||||
} else {
|
||||
TryIndexFileAsContainer(Common::FS::ToUTF8String(p.u8string()), false);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(Loader, "Error accessing DLC directory '{}': {}", dir, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void ExternalContentIndexer::TryIndexFileAsContainer(const std::string& path, bool is_update) {
|
||||
const auto lower = ToLowerCopy(path);
|
||||
if (lower.size() >=4 && lower.rfind(".nsp") == lower.size() -4) {
|
||||
if (auto vf = m_vfs->OpenFile(path, OpenMode::Read)) {
|
||||
ParseContainerNSP(vf, is_update);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ExternalContentIndexer::TryIndexLooseDir(const std::string& dir, bool is_update) {
|
||||
fs::path p = Common::FS::ToU8String(dir);
|
||||
std::error_code ec;
|
||||
if (!fs::is_directory(p, ec) || ec) return;
|
||||
|
||||
for (const auto& entry : fs::recursive_directory_iterator(p, fs::directory_options::skip_permission_denied, ec)) {
|
||||
if (ec) break;
|
||||
if (!entry.is_regular_file(ec)) continue;
|
||||
const auto path = Common::FS::ToUTF8String(entry.path().u8string());
|
||||
const auto lower = ToLowerCopy(path);
|
||||
if (lower.size() >= 9 && lower.rfind(".cnmt.nca") == lower.size() - 9) {
|
||||
if (auto vf = m_vfs->OpenFile(path, OpenMode::Read)) {
|
||||
ParseLooseCnmtNca(vf, Common::FS::ToUTF8String(entry.path().parent_path().u8string()), is_update);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ExternalContentIndexer::ParseContainerNSP(VirtualFile file, bool is_update) {
|
||||
if (file == nullptr) return;
|
||||
NSP nsp(file);
|
||||
if (nsp.GetStatus() != Loader::ResultStatus::Success) {
|
||||
LOG_WARNING(Loader, "ExternalContent: NSP parse failed");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto title_map = nsp.GetNCAs();
|
||||
if (title_map.empty()) return;
|
||||
|
||||
for (const auto& [title_id, nca_map] : title_map) {
|
||||
std::shared_ptr<NCA> meta_nca;
|
||||
for (const auto& [key, nca_ptr] : nca_map) {
|
||||
if (nca_ptr && nca_ptr->GetType() == NCAContentType::Meta) {
|
||||
meta_nca = nca_ptr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!meta_nca)
|
||||
continue;
|
||||
|
||||
auto cnmt_opt = ExtractCnmtFromMetaNca(*meta_nca);
|
||||
if (!cnmt_opt)
|
||||
continue;
|
||||
const auto& cnmt = *cnmt_opt;
|
||||
|
||||
const auto base_id = BaseTitleId(title_id);
|
||||
|
||||
if (is_update && cnmt.GetType() == TitleType::Update) {
|
||||
ParsedUpdate candidate{};
|
||||
// Register updates under their Update TID so PatchManager can find/apply them
|
||||
candidate.title_id = FileSys::GetUpdateTitleID(base_id);
|
||||
candidate.version = cnmt.GetTitleVersion();
|
||||
for (const auto& rec : cnmt.GetContentRecords()) {
|
||||
const auto it = nca_map.find({cnmt.GetType(), rec.type});
|
||||
if (it != nca_map.end() && it->second) {
|
||||
candidate.ncas[rec.type] = it->second->GetBaseFile();
|
||||
}
|
||||
}
|
||||
|
||||
auto& best = m_best_update_by_title[base_id];
|
||||
if (best.title_id ==0 || candidate.version > best.version) {
|
||||
best = std::move(candidate);
|
||||
}
|
||||
} else if (cnmt.GetType() == TitleType::AOC) {
|
||||
const auto dlc_title_id = cnmt.GetTitleID();
|
||||
for (const auto& rec : cnmt.GetContentRecords()) {
|
||||
const auto it = nca_map.find({cnmt.GetType(), rec.type});
|
||||
if (it != nca_map.end() && it->second) {
|
||||
m_all_dlc.push_back(ParsedDlcRecord{dlc_title_id, {}, it->second->GetBaseFile()});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ExternalContentIndexer::ParseLooseCnmtNca(VirtualFile meta_nca_file, const std::string& folder, bool is_update) {
|
||||
if (meta_nca_file == nullptr)
|
||||
return;
|
||||
|
||||
NCA meta(meta_nca_file);
|
||||
|
||||
if (!IsMeta(meta))
|
||||
return;
|
||||
|
||||
auto cnmt_opt = ExtractCnmtFromMetaNca(meta);
|
||||
if (!cnmt_opt) return;
|
||||
const auto& cnmt = *cnmt_opt;
|
||||
|
||||
const auto base_id = BaseTitleId(cnmt.GetTitleID());
|
||||
|
||||
if (is_update && cnmt.GetType() == TitleType::Update) {
|
||||
ParsedUpdate candidate{};
|
||||
// Register updates under their Update TID so PatchManager can find/apply them
|
||||
candidate.title_id = FileSys::GetUpdateTitleID(base_id);
|
||||
candidate.version = cnmt.GetTitleVersion();
|
||||
|
||||
for (const auto& rec : cnmt.GetContentRecords()) {
|
||||
const auto file_name = Common::HexToString(rec.nca_id) + ".nca";
|
||||
const auto full = Common::FS::ToUTF8String((fs::path(Common::FS::ToU8String(folder)) / fs::path(file_name)).u8string());
|
||||
if (auto vf = m_vfs->OpenFile(full, OpenMode::Read)) {
|
||||
candidate.ncas[rec.type] = vf;
|
||||
}
|
||||
}
|
||||
|
||||
auto& best = m_best_update_by_title[base_id];
|
||||
if (best.title_id ==0 || candidate.version > best.version) {
|
||||
best = std::move(candidate);
|
||||
}
|
||||
} else if (cnmt.GetType() == TitleType::AOC) {
|
||||
const auto dlc_title_id = cnmt.GetTitleID();
|
||||
for (const auto& rec : cnmt.GetContentRecords()) {
|
||||
const auto file_name = Common::HexToString(rec.nca_id) + ".nca";
|
||||
const auto full = Common::FS::ToUTF8String((fs::path(Common::FS::ToU8String(folder)) / fs::path(file_name)).u8string());
|
||||
if (auto vf = m_vfs->OpenFile(full, OpenMode::Read)) {
|
||||
ParsedDlcRecord dl{dlc_title_id, {}, vf};
|
||||
m_all_dlc.push_back(std::move(dl));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<CNMT> ExternalContentIndexer::ExtractCnmtFromMetaNca(const NCA& meta_nca) {
|
||||
if (meta_nca.GetStatus() != Loader::ResultStatus::Success)
|
||||
return std::nullopt;
|
||||
|
||||
const auto subs = meta_nca.GetSubdirectories();
|
||||
if (subs.empty() || !subs[0])
|
||||
return std::nullopt;
|
||||
|
||||
const auto files = subs[0]->GetFiles();
|
||||
if (files.empty() || !files[0])
|
||||
return std::nullopt;
|
||||
|
||||
CNMT cnmt(files[0]);
|
||||
return cnmt;
|
||||
}
|
||||
|
||||
ExternalContentIndexer::TitleID ExternalContentIndexer::BaseTitleId(TitleID id) {
|
||||
return FileSys::GetBaseTitleID(id);
|
||||
}
|
||||
|
||||
bool ExternalContentIndexer::IsMeta(const NCA& nca) {
|
||||
return nca.GetType() == NCAContentType::Meta;
|
||||
}
|
||||
|
||||
void ExternalContentIndexer::Commit() {
|
||||
// Updates: for now just register the highest version per DLC
|
||||
for (auto& [base_title, upd] : m_best_update_by_title) {
|
||||
for (const auto& kv : upd.ncas) {
|
||||
const auto rec_type = kv.first;
|
||||
const auto& file = kv.second;
|
||||
if (!file) continue;
|
||||
// Use the Update TitleID for provider registration so core queries by update_tid succeed
|
||||
const auto update_tid = FileSys::GetUpdateTitleID(base_title);
|
||||
m_provider.AddEntry(TitleType::Update, rec_type, update_tid, file);
|
||||
}
|
||||
}
|
||||
// DLC: additiv
|
||||
for (const auto& dlc : m_all_dlc) {
|
||||
if (!dlc.file) continue;
|
||||
m_provider.AddEntry(TitleType::AOC, ContentRecordType::Data, dlc.title_id, dlc.file);
|
||||
}
|
||||
|
||||
LOG_INFO(Loader, "ExternalContent: registered {} titles with updates, {} DLC records",
|
||||
m_best_update_by_title.size(), m_all_dlc.size());
|
||||
}
|
||||
|
||||
} // namespace FileSys} // namespace FileSys
|
||||
74
src/core/file_sys/external_content_index.h
Normal file
74
src/core/file_sys/external_content_index.h
Normal file
@@ -0,0 +1,74 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include "common/common_types.h"
|
||||
#include "core/file_sys/registered_cache.h"
|
||||
#include "core/file_sys/vfs/vfs.h"
|
||||
#include "core/file_sys/nca_metadata.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
class ManualContentProvider;
|
||||
class NCA;
|
||||
|
||||
struct ExternalContentPaths {
|
||||
std::vector<std::string> update_dirs;
|
||||
std::vector<std::string> dlc_dirs;
|
||||
};
|
||||
|
||||
class ExternalContentIndexer {
|
||||
public:
|
||||
ExternalContentIndexer(VirtualFilesystem vfs,
|
||||
ManualContentProvider& provider,
|
||||
ExternalContentPaths paths);
|
||||
|
||||
void Rebuild();
|
||||
|
||||
private:
|
||||
using TitleID = u64;
|
||||
|
||||
struct ParsedUpdate {
|
||||
TitleID title_id{};
|
||||
u32 version{};
|
||||
std::unordered_map<ContentRecordType, VirtualFile> ncas;
|
||||
};
|
||||
|
||||
struct ParsedDlcRecord {
|
||||
TitleID title_id{};
|
||||
NcaID nca_id{};
|
||||
VirtualFile file{};
|
||||
};
|
||||
|
||||
void IndexUpdatesDir(const std::string& dir);
|
||||
void IndexDlcDir(const std::string& dir);
|
||||
|
||||
void TryIndexFileAsContainer(const std::string& path, bool is_update);
|
||||
void TryIndexLooseDir(const std::string& dir, bool is_update);
|
||||
|
||||
void ParseContainerNSP(VirtualFile file, bool is_update);
|
||||
void ParseLooseCnmtNca(VirtualFile meta_nca_file, const std::string& folder, bool is_update);
|
||||
|
||||
static std::optional<CNMT> ExtractCnmtFromMetaNca(const NCA& meta_nca);
|
||||
static TitleID BaseTitleId(TitleID id);
|
||||
static bool IsMeta(const NCA& nca);
|
||||
|
||||
void Commit();
|
||||
|
||||
private:
|
||||
VirtualFilesystem m_vfs;
|
||||
ManualContentProvider& m_provider;
|
||||
ExternalContentPaths m_paths;
|
||||
|
||||
std::unordered_map<TitleID, ParsedUpdate> m_best_update_by_title;
|
||||
|
||||
std::vector<ParsedDlcRecord> m_all_dlc;
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
||||
@@ -208,6 +208,7 @@ enum class ContentProviderUnionSlot {
|
||||
UserNAND, ///< User NAND
|
||||
SDMC, ///< SD Card
|
||||
FrontendManual, ///< Frontend-defined game list or similar
|
||||
External ///< External Updates/DLCs (not installed to NAND)
|
||||
};
|
||||
|
||||
// Combines multiple ContentProvider(s) (i.e. SysNAND, UserNAND, SDMC) into one interface.
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <utility>
|
||||
#include <filesystem>
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "common/fs/fs.h"
|
||||
@@ -12,6 +16,7 @@
|
||||
#include "core/file_sys/card_image.h"
|
||||
#include "core/file_sys/control_metadata.h"
|
||||
#include "core/file_sys/errors.h"
|
||||
#include "core/file_sys/external_content_index.h"
|
||||
#include "core/file_sys/patch_manager.h"
|
||||
#include "core/file_sys/registered_cache.h"
|
||||
#include "core/file_sys/romfs_factory.h"
|
||||
@@ -713,6 +718,36 @@ void FileSystemController::CreateFactories(FileSys::VfsFilesystem& vfs, bool ove
|
||||
system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::SDMC,
|
||||
sdmc_factory->GetSDMCContents());
|
||||
}
|
||||
|
||||
if (external_provider == nullptr) {
|
||||
external_provider = std::make_unique<FileSys::ManualContentProvider>();
|
||||
system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::External,
|
||||
external_provider.get());
|
||||
}
|
||||
|
||||
RebuildExternalContentIndex();
|
||||
}
|
||||
|
||||
void FileSystemController::RebuildExternalContentIndex() {
|
||||
if (external_provider == nullptr) {
|
||||
LOG_WARNING(Service_FS, "External provider not initialized, skipping re-index.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Settings::values.external_dirs.empty()) {
|
||||
FileSys::ExternalContentPaths paths{};
|
||||
for (const auto& dir : Settings::values.external_dirs) {
|
||||
if (dir.empty())
|
||||
continue;
|
||||
paths.update_dirs.push_back(dir);
|
||||
paths.dlc_dirs.push_back(dir);
|
||||
}
|
||||
FileSys::ExternalContentIndexer indexer{system.GetFilesystem(), *this->external_provider,
|
||||
std::move(paths)};
|
||||
indexer.Rebuild();
|
||||
} else {
|
||||
external_provider->ClearAllEntries();
|
||||
}
|
||||
}
|
||||
|
||||
void FileSystemController::Reset() {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
@@ -5,6 +8,7 @@
|
||||
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <core/file_sys/registered_cache.h>
|
||||
#include "common/common_types.h"
|
||||
#include "core/file_sys/fs_directory.h"
|
||||
#include "core/file_sys/fs_filesystem.h"
|
||||
@@ -121,6 +125,8 @@ public:
|
||||
// above is called.
|
||||
void CreateFactories(FileSys::VfsFilesystem& vfs, bool overwrite = true);
|
||||
|
||||
void RebuildExternalContentIndex();
|
||||
|
||||
void Reset();
|
||||
|
||||
private:
|
||||
@@ -141,6 +147,7 @@ private:
|
||||
std::unique_ptr<FileSys::XCI> gamecard;
|
||||
std::unique_ptr<FileSys::RegisteredCache> gamecard_registered;
|
||||
std::unique_ptr<FileSys::PlaceholderCache> gamecard_placeholder;
|
||||
std::unique_ptr<FileSys::ManualContentProvider> external_provider;
|
||||
|
||||
Core::System& system;
|
||||
};
|
||||
|
||||
@@ -283,6 +283,19 @@ void Config::ReadDataStorageValues() {
|
||||
|
||||
ReadCategory(Settings::Category::DataStorage);
|
||||
|
||||
Settings::values.external_dirs.clear();
|
||||
const int num_dirs = BeginArray(std::string("external_dirs"));
|
||||
Settings::values.external_dirs.reserve(num_dirs);
|
||||
for (int i = 0; i < num_dirs; ++i) {
|
||||
SetArrayIndex(i);
|
||||
std::string dir = ReadStringSetting(std::string("path"), std::string(""));
|
||||
if (!dir.empty()) {
|
||||
Settings::values.external_dirs.emplace_back(std::move(dir));
|
||||
}
|
||||
}
|
||||
|
||||
EndArray();
|
||||
|
||||
EndGroup();
|
||||
}
|
||||
|
||||
@@ -591,6 +604,14 @@ void Config::SaveDataStorageValues() {
|
||||
|
||||
WriteCategory(Settings::Category::DataStorage);
|
||||
|
||||
BeginArray(std::string("external_dirs"));
|
||||
for (std::size_t i = 0; i < Settings::values.external_dirs.size(); ++i) {
|
||||
SetArrayIndex(static_cast<int>(i));
|
||||
WriteStringSetting(std::string("path"), Settings::values.external_dirs[i],
|
||||
std::make_optional(std::string("")));
|
||||
}
|
||||
EndArray();
|
||||
|
||||
EndGroup();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,14 +7,18 @@
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <QFileDialog>
|
||||
#include <QListWidgetItem>
|
||||
#include <QMessageBox>
|
||||
#include "common/settings.h"
|
||||
#include "core/core.h"
|
||||
#include "core/hle/service/filesystem/filesystem.h"
|
||||
#include "ui_configure_general.h"
|
||||
#include "yuzu/configuration/configuration_shared.h"
|
||||
#include "yuzu/configuration/configure_general.h"
|
||||
#include "yuzu/configuration/shared_widget.h"
|
||||
#include "qt_common/config/uisettings.h"
|
||||
#include "qt_common/util/game.h"
|
||||
|
||||
ConfigureGeneral::ConfigureGeneral(const Core::System& system_,
|
||||
std::shared_ptr<std::vector<ConfigurationShared::Tab*>> group_,
|
||||
@@ -22,21 +26,45 @@ ConfigureGeneral::ConfigureGeneral(const Core::System& system_,
|
||||
: Tab(group_, parent), ui{std::make_unique<Ui::ConfigureGeneral>()}, system{system_} {
|
||||
ui->setupUi(this);
|
||||
|
||||
apply_funcs.push_back([this](bool) {
|
||||
Settings::values.external_dirs.clear();
|
||||
for (int i = 0; i < ui->external_dirs_list->count(); ++i) {
|
||||
QListWidgetItem* item = ui->external_dirs_list->item(i);
|
||||
if (item) {
|
||||
Settings::values.external_dirs.push_back(item->text().toStdString());
|
||||
}
|
||||
}
|
||||
auto& fs_controller = const_cast<Core::System&>(system).GetFileSystemController();
|
||||
fs_controller.RebuildExternalContentIndex();
|
||||
QtCommon::Game::ResetMetadata(false);
|
||||
UISettings::values.is_game_list_reload_pending.exchange(true);
|
||||
});
|
||||
|
||||
Setup(builder);
|
||||
|
||||
SetConfiguration();
|
||||
|
||||
connect(ui->button_reset_defaults, &QPushButton::clicked, this,
|
||||
&ConfigureGeneral::ResetDefaults);
|
||||
connect(ui->add_dir_button, &QPushButton::clicked, this, &ConfigureGeneral::OnAddDirClicked);
|
||||
connect(ui->remove_dir_button, &QPushButton::clicked, this,
|
||||
&ConfigureGeneral::OnRemoveDirClicked);
|
||||
connect(ui->external_dirs_list, &QListWidget::itemSelectionChanged, this,
|
||||
&ConfigureGeneral::OnDirSelectionChanged);
|
||||
|
||||
ui->remove_dir_button->setEnabled(false);
|
||||
|
||||
if (!Settings::IsConfiguringGlobal()) {
|
||||
ui->button_reset_defaults->setVisible(false);
|
||||
ui->DataDirsGroupBox->setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
ConfigureGeneral::~ConfigureGeneral() = default;
|
||||
|
||||
void ConfigureGeneral::SetConfiguration() {}
|
||||
void ConfigureGeneral::SetConfiguration() {
|
||||
LoadExternalDirs();
|
||||
}
|
||||
|
||||
void ConfigureGeneral::Setup(const ConfigurationShared::Builder& builder) {
|
||||
QLayout& general_layout = *ui->general_widget->layout();
|
||||
@@ -109,6 +137,7 @@ void ConfigureGeneral::ResetDefaults() {
|
||||
UISettings::values.reset_to_defaults = true;
|
||||
UISettings::values.is_game_list_reload_pending.exchange(true);
|
||||
reset_callback();
|
||||
SetConfiguration();
|
||||
}
|
||||
|
||||
void ConfigureGeneral::ApplyConfiguration() {
|
||||
@@ -129,3 +158,37 @@ void ConfigureGeneral::changeEvent(QEvent* event) {
|
||||
void ConfigureGeneral::RetranslateUI() {
|
||||
ui->retranslateUi(this);
|
||||
}
|
||||
|
||||
void ConfigureGeneral::LoadExternalDirs() {
|
||||
ui->external_dirs_list->clear();
|
||||
for (const auto& dir : Settings::values.external_dirs) {
|
||||
ui->external_dirs_list->addItem(QString::fromStdString(dir));
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigureGeneral::OnAddDirClicked() {
|
||||
QString default_path = QDir::homePath();
|
||||
if (ui->external_dirs_list->count() > 0) {
|
||||
default_path = ui->external_dirs_list->item(ui->external_dirs_list->count() - 1)->text();
|
||||
}
|
||||
|
||||
QString dir = QFileDialog::getExistingDirectory(this, tr("Select Directory"), default_path);
|
||||
if (!dir.isEmpty()) {
|
||||
if (ui->external_dirs_list->findItems(dir, Qt::MatchExactly).isEmpty()) {
|
||||
ui->external_dirs_list->addItem(dir);
|
||||
} else {
|
||||
QMessageBox::warning(this, tr("Directory already added"),
|
||||
tr("The directory \"%1\" is already in the list.").arg(dir));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigureGeneral::OnRemoveDirClicked() {
|
||||
for (auto* item : ui->external_dirs_list->selectedItems()) {
|
||||
delete ui->external_dirs_list->takeItem(ui->external_dirs_list->row(item));
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigureGeneral::OnDirSelectionChanged() {
|
||||
ui->remove_dir_button->setEnabled(!ui->external_dirs_list->selectedItems().isEmpty());
|
||||
}
|
||||
|
||||
@@ -45,6 +45,11 @@ private:
|
||||
void changeEvent(QEvent* event) override;
|
||||
void RetranslateUI();
|
||||
|
||||
void LoadExternalDirs();
|
||||
void OnAddDirClicked();
|
||||
void OnRemoveDirClicked();
|
||||
void OnDirSelectionChanged();
|
||||
|
||||
std::function<void()> reset_callback;
|
||||
|
||||
std::unique_ptr<Ui::ConfigureGeneral> ui;
|
||||
|
||||
@@ -73,6 +73,59 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="DataDirsGroupBox">
|
||||
<property name="title">
|
||||
<string>Additional Directories (Updates, DLC, etc.)</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="DataDirsVerticalLayout">
|
||||
<item>
|
||||
<widget class="QListWidget" name="external_dirs_list">
|
||||
<property name="toolTip">
|
||||
<string>This list contains directories that will be searched for game updates and DLC.</string>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="DataDirsHorizontalLayout">
|
||||
<item>
|
||||
<spacer name="spacer_dirs_left">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="add_dir_button">
|
||||
<property name="text">
|
||||
<string>Add</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="remove_dir_button">
|
||||
<property name="text">
|
||||
<string>Remove</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
|
||||
Reference in New Issue
Block a user