Changes to maintain and persist <folder/> information in a gamelist:

1. folder elements are loaded when present in a gamelist and if they match the filesystem representation
2. missing optional folder elements are rendered empty in theme (and not "unknown")
3. As before non-existing folder elements are generated by ES, with mandatory elements <path/> and <name/>, but with this PR, whenever metadata is edited for this folder object (FileData class) it is persisted
4. Editiing metadata on existing <folder/> gets persisted too
5. UI metadata edit: layout fix that renders entered metadata not centered when returning from editing
6. UI metadata edit: format tooltip for date format in release date field
This commit is contained in:
Gemba 2023-12-17 19:22:25 +01:00
parent 01de7618d0
commit a113b22ca7
10 changed files with 121 additions and 64 deletions

View file

@ -12,11 +12,12 @@
FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType type)
{
// first, verify that path is within the system's root folder
FileData* root = system->getRootFolder();
bool contains = false;
std::string relative = Utils::FileSystem::removeCommonPath(path, root->getPath(), contains, true);
const std::string systemPath = root->getPath();
// first, verify that path is within the system's root folder
std::string relative = Utils::FileSystem::removeCommonPath(path, systemPath, contains, true);
if(!contains)
{
LOG(LogError) << "File path \"" << path << "\" is outside system path \"" << system->getStartPath() << "\"";
@ -24,17 +25,21 @@ FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType
}
Utils::FileSystem::stringList pathList = Utils::FileSystem::getPathList(relative);
auto path_it = pathList.begin();
FileData* treeNode = root;
bool found = false;
// iterate over all subpaths below the provided path
while(path_it != pathList.end())
{
const std::unordered_map<std::string, FileData*>& children = treeNode->getChildrenByFilename();
std::string key = *path_it;
found = children.find(key) != children.cend();
std::string pathSegment = *path_it;
auto candidate = children.find(pathSegment);
found = candidate != children.cend();
if (found) {
treeNode = children.at(key);
treeNode = candidate->second;
}
// this is the end
@ -65,12 +70,16 @@ FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType
// if type is a folder it's gonna be empty, so don't bother
if(type == FOLDER)
{
LOG(LogWarning) << "gameList: folder doesn't already exist, won't create";
std::string absFolder = Utils::FileSystem::getAbsolutePath(pathSegment, systemPath);
LOG(LogWarning) << "gameList: folder " << absFolder << " absent on fs, no FileData object created. Do remove leftover in gamelist.xml to remediate this warning.";
return NULL;
}
// create missing folder
FileData* folder = new FileData(FOLDER, Utils::FileSystem::getStem(treeNode->getPath()) + "/" + *path_it, system->getSystemEnvData(), system);
// create folder filedata object
std::string absPath = Utils::FileSystem::resolveRelativePath(treeNode->getPath() + "/" + pathSegment, systemPath, false, true);
FileData* folder = new FileData(FOLDER, absPath, system->getSystemEnvData(), system);
LOG(LogDebug) << "folder not found as FileData, adding: " << folder->getPath();
treeNode->addChild(folder);
treeNode = folder;
}
@ -118,7 +127,8 @@ void parseGamelist(SystemData* system)
FileType type = typeList[i];
for(pugi::xml_node fileNode = root.child(tag); fileNode; fileNode = fileNode.next_sibling(tag))
{
const std::string path = Utils::FileSystem::resolveRelativePath(fileNode.child("path").text().get(), relativeTo, false, true);
std::string path = fileNode.child("path").text().get();
path = Utils::FileSystem::resolveRelativePath(path, relativeTo, false, true);
if(!trustGamelist && !Utils::FileSystem::exists(path))
{
@ -127,7 +137,7 @@ void parseGamelist(SystemData* system)
}
// Check whether the file's extension is allowed in the system
if (std::find(allowedExtensions.cbegin(), allowedExtensions.cend(), Utils::FileSystem::getExtension(path)) == allowedExtensions.cend())
if (i == 0 /*game*/ && std::find(allowedExtensions.cbegin(), allowedExtensions.cend(), Utils::FileSystem::getExtension(path)) == allowedExtensions.cend())
{
LOG(LogDebug) << "file " << path << " found in gamelist, but has unregistered extension";
continue;
@ -142,7 +152,7 @@ void parseGamelist(SystemData* system)
else if(!file->isArcadeAsset())
{
std::string defaultName = file->metadata.get("name");
file->metadata = MetaDataList::createFromXML(GAME_METADATA, fileNode, relativeTo);
file->metadata = MetaDataList::createFromXML(i == 0 ? GAME_METADATA : FOLDER_METADATA, fileNode, relativeTo);
//make sure name gets set if one didn't exist
if(file->metadata.get("name").empty())
@ -173,7 +183,8 @@ void addFileDataNode(pugi::xml_node& parent, const FileData* file, const char* t
//there's something useful in there so we'll keep the node, add the path
// try and make the path relative if we can so things still work if we change the rom folder location in the future
newNode.prepend_child("path").text().set(Utils::FileSystem::createRelativePath(file->getPath(), system->getStartPath(), false, true).c_str());
std::string relPath = Utils::FileSystem::createRelativePath(file->getPath(), system->getStartPath(), false, true);
newNode.prepend_child("path").text().set(relPath.c_str());
}
}
@ -224,9 +235,8 @@ void updateGamelist(SystemData* system)
{
int numUpdated = 0;
//get only files, no folders
std::vector<FileData*> files = rootFolder->getFilesRecursive(GAME | FOLDER);
// Stage 1: iterate through all files in memory, checking for changes
for(std::vector<FileData*>::const_iterator fit = files.cbegin(); fit != files.cend(); ++fit)
{
@ -234,13 +244,13 @@ void updateGamelist(SystemData* system)
// do not touch if it wasn't changed anyway
if (!(*fit)->metadata.wasChanged())
continue;
// adding item to changed list
if ((*fit)->getType() == GAME)
if ((*fit)->getType() == GAME)
{
changedGames.push_back((*fit));
changedGames.push_back((*fit));
}
else
else
{
changedFolders.push_back((*fit));
}
@ -251,13 +261,13 @@ void updateGamelist(SystemData* system)
const char* tagList[2] = { "game", "folder" };
FileType typeList[2] = { GAME, FOLDER };
std::vector<FileData*> changedList[2] = { changedGames, changedFolders };
for(int i = 0; i < 2; i++)
{
const char* tag = tagList[i];
std::vector<FileData*> changes = changedList[i];
// if changed items of this type
// check for changed items of this type
if (changes.size() > 0) {
// check if the item already exists in the XML
// if it does, remove all corresponding items before adding
@ -274,9 +284,10 @@ void updateGamelist(SystemData* system)
continue;
}
std::string xmlpath = pathNode.text().get();
// apply the same transformation as in Gamelist::parseGamelist
std::string xmlpath = Utils::FileSystem::resolveRelativePath(pathNode.text().get(), relativeTo, false, true);
xmlpath = Utils::FileSystem::resolveRelativePath(xmlpath, relativeTo, false, true);
for(std::vector<FileData*>::const_iterator cfit = changes.cbegin(); cfit != changes.cend(); ++cfit)
{
if(xmlpath == (*cfit)->getPath())

View file

@ -1,6 +1,7 @@
#include "MetaData.h"
#include "utils/FileSystemUtil.h"
#include "utils/TimeUtil.h"
#include "Log.h"
#include <pugixml.hpp>
@ -27,6 +28,12 @@ MetaDataDecl gameDecls[] = {
};
const std::vector<MetaDataDecl> gameMDD(gameDecls, gameDecls + sizeof(gameDecls) / sizeof(gameDecls[0]));
const inline std::string blankDate() {
// blank date (1970-01-02) is used to render "" (see DateTimeComponent.cpp) for
// folder metadata when no date is provided (=default case)
return Utils::Time::timeToString(Utils::Time::BLANK_DATE, "%Y%m%d");
}
MetaDataDecl folderDecls[] = {
{"name", MD_STRING, "", false, "name", "enter game name"},
{"sortname", MD_STRING, "", false, "sortname", "enter game sort name"},
@ -35,12 +42,12 @@ MetaDataDecl folderDecls[] = {
{"thumbnail", MD_PATH, "", false, "thumbnail", "enter path to thumbnail"},
{"video", MD_PATH, "", false, "video", "enter path to video"},
{"marquee", MD_PATH, "", false, "marquee", "enter path to marquee"},
{"rating", MD_RATING, "0.000000", false, "rating", "enter rating"},
{"releasedate", MD_DATE, "not-a-date-time", false, "release date", "enter release date"},
{"developer", MD_STRING, "unknown", false, "developer", "enter game developer"},
{"publisher", MD_STRING, "unknown", false, "publisher", "enter game publisher"},
{"genre", MD_STRING, "unknown", false, "genre", "enter game genre"},
{"players", MD_INT, "1", false, "players", "enter number of players"}
{"rating", MD_RATING, "", false, "rating", "enter rating"},
{"releasedate", MD_DATE, blankDate(), true, "release date", "enter release date"},
{"developer", MD_STRING, "", false, "developer", "enter game developer"},
{"publisher", MD_STRING, "", false, "publisher", "enter game publisher"},
{"genre", MD_STRING, "", false, "genre", "enter game genre"},
{"players", MD_INT, "", false, "players", "enter number of players"}
};
const std::vector<MetaDataDecl> folderMDD(folderDecls, folderDecls + sizeof(folderDecls) / sizeof(folderDecls[0]));

View file

@ -29,7 +29,7 @@ struct MetaDataDecl
std::string key;
MetaDataType type;
std::string defaultValue;
bool isStatistic; //if true, ignore scraper values for this metadata
bool isStatistic; // if true: ignore in scraping and hide in metadata edits for this key
std::string displayName; // displayed as this in editors
std::string displayPrompt; // phrase displayed in editors when prompted to enter value (currently only for strings)
};

View file

@ -121,7 +121,10 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui
if (UIModeController::getInstance()->isUIModeFull() && !mFromPlaceholder && !(mSystem->isCollection() && file->getType() == FOLDER))
{
row.elements.clear();
row.addElement(std::make_shared<TextComponent>(mWindow, "EDIT THIS GAME'S METADATA", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true);
std::string lblTxt = std::string("EDIT THIS ");
lblTxt += std::string((file->getType() == FOLDER ? "FOLDER" : "GAME"));
lblTxt += std::string("'S METADATA");
row.addElement(std::make_shared<TextComponent>(mWindow, lblTxt, Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true);
row.addElement(makeArrow(mWindow), false);
row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::openMetaDataEd, this));
mMenu.addRow(row);

View file

@ -2,6 +2,7 @@
#include "components/ButtonComponent.h"
#include "components/ComponentList.h"
#include "components/DateTimeComponent.h"
#include "components/DateTimeEditComponent.h"
#include "components/MenuComponent.h"
#include "components/RatingComponent.h"
@ -37,8 +38,9 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector
mHeaderGrid = std::make_shared<ComponentGrid>(mWindow, Vector2i(1, 5));
mTitle = std::make_shared<TextComponent>(mWindow, "EDIT METADATA", Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER);
mSubtitle = std::make_shared<TextComponent>(mWindow, Utils::String::toUpper(Utils::FileSystem::getFileName(scraperParams.game->getPath())),
Font::get(FONT_SIZE_SMALL), 0x777777FF, ALIGN_CENTER);
std::string tgt = md->getType() == GAME_METADATA ? "GAME" : "FOLDER";
std::string subt = tgt + ": " + Utils::String::toUpper(Utils::FileSystem::getFileName(scraperParams.game->getPath()));
mSubtitle = std::make_shared<TextComponent>(mWindow, subt, Font::get(FONT_SIZE_SMALL), 0x777777FF, ALIGN_CENTER);
mHeaderGrid->setEntry(mTitle, Vector2i(0, 1), false, true);
mHeaderGrid->setEntry(mSubtitle, Vector2i(0, 3), false, true);
@ -50,16 +52,20 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector
// populate list
for(auto iter = mdd.cbegin(); iter != mdd.cend(); iter++)
{
std::shared_ptr<GuiComponent> ed;
// don't add statistics
if(iter->isStatistic)
continue;
std::shared_ptr<GuiComponent> ed;
// create ed and add it (and any related components) to mMenu
// ed's value will be set below
ComponentListRow row;
auto lbl = std::make_shared<TextComponent>(mWindow, Utils::String::toUpper(iter->displayName), Font::get(FONT_SIZE_SMALL), 0x777777FF);
auto lblTxt = Utils::String::toUpper(iter->displayName);
if (iter->type == MD_DATE) {
lblTxt += " (" + DateTimeComponent::getDateformatTip() + ")";
}
auto lbl = std::make_shared<TextComponent>(mWindow, lblTxt, Font::get(FONT_SIZE_SMALL), 0x777777FF);
row.addElement(lbl, true); // label
switch(iter->type)
@ -111,6 +117,8 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector
{
// MD_STRING
ed = std::make_shared<TextComponent>(window, "", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT), 0x777777FF, ALIGN_RIGHT);
const float height = lbl->getSize().y() * 0.71f;
ed->setSize(0, height);
row.addElement(ed, true);
auto spacer = std::make_shared<GuiComponent>(mWindow);
@ -140,7 +148,7 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector
std::vector< std::shared_ptr<ButtonComponent> > buttons;
if(!scraperParams.system->hasPlatformId(PlatformIds::PLATFORM_IGNORE))
if(md->getType() == GAME_METADATA && !scraperParams.system->hasPlatformId(PlatformIds::PLATFORM_IGNORE))
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "SCRAPE", "scrape", std::bind(&GuiMetaDataEd::fetch, this)));
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "SAVE", "save", [&] { save(); delete this; }));
@ -185,11 +193,17 @@ void GuiMetaDataEd::save()
// remove game from index
mScraperParams.system->getIndex()->removeFromIndex(mScraperParams.game);
for(unsigned int i = 0; i < mEditors.size(); i++)
assert(mMetaDataDecl.size() >= mEditors.size());
// there may be less editfields than metadata entries as
// statistic md fields are not shown to the user.
// md statistic fields are not necessarily at the end of the md list
int edIdx = 0;
for(auto &mdd : mMetaDataDecl)
{
if(mMetaDataDecl.at(i).isStatistic)
continue;
mMetaData->set(mMetaDataDecl.at(i).key, mEditors.at(i)->getValue());
if(!mdd.isStatistic) {
mMetaData->set(mdd.key, mEditors.at(edIdx)->getValue());
edIdx++;
}
}
// enter game in index
@ -212,29 +226,21 @@ void GuiMetaDataEd::fetch()
void GuiMetaDataEd::fetchDone(const ScraperSearchResult& result)
{
for(unsigned int i = 0; i < mEditors.size(); i++)
assert(mMetaDataDecl.size() >= mEditors.size());
int edIdx = 0;
for(auto &mdd : mMetaDataDecl)
{
if(mMetaDataDecl.at(i).isStatistic)
if(mdd.isStatistic)
continue;
const std::string& key = mMetaDataDecl.at(i).key;
mEditors.at(i)->setValue(result.mdl.get(key));
mEditors.at(edIdx)->setValue(result.mdl.get(mdd.key));
edIdx++;
}
}
void GuiMetaDataEd::close(bool closeAllWindows)
{
// find out if the user made any changes
bool dirty = false;
for(unsigned int i = 0; i < mEditors.size(); i++)
{
const std::string& key = mMetaDataDecl.at(i).key;
if(mMetaData->get(key) != mEditors.at(i)->getValue())
{
dirty = true;
break;
}
}
bool dirty = hasChanges();
std::function<void()> closeFunc;
if(!closeAllWindows)
@ -248,7 +254,6 @@ void GuiMetaDataEd::close(bool closeAllWindows)
};
}
if(dirty)
{
// changes were made, ask if the user wants to save them
@ -262,6 +267,20 @@ void GuiMetaDataEd::close(bool closeAllWindows)
}
}
bool GuiMetaDataEd::hasChanges()
{
assert(mMetaDataDecl.size() >= mEditors.size());
// find out if the user made any changes
int edIdx = 0;
for(auto &mdd : mMetaDataDecl)
{
if(!mdd.isStatistic && mMetaData->get(mdd.key) != mEditors.at(edIdx++)->getValue())
return true;
}
return false;
}
bool GuiMetaDataEd::input(InputConfig* config, Input input)
{
if(GuiComponent::input(config, input))

View file

@ -26,6 +26,7 @@ private:
void fetch();
void fetchDone(const ScraperSearchResult& result);
void close(bool closeAllWindows);
bool hasChanges();
NinePatchComponent mBackground;
ComponentGrid mGrid;

View file

@ -4,15 +4,17 @@
#include "Log.h"
#include "Settings.h"
#include <ctime>
DateTimeComponent::DateTimeComponent(Window* window) : TextComponent(window), mDisplayRelative(false)
{
setFormat("%m/%d/%Y");
setFormat(getDateformat());
}
DateTimeComponent::DateTimeComponent(Window* window, const std::string& text, const std::shared_ptr<Font>& font, unsigned int color, Alignment align,
Vector3f pos, Vector2f size, unsigned int bgcolor) : TextComponent(window, text, font, color, align, pos, size, bgcolor), mDisplayRelative(false)
{
setFormat("%m/%d/%Y");
setFormat(getDateformat());
}
void DateTimeComponent::setValue(const std::string& val)
@ -47,9 +49,13 @@ void DateTimeComponent::onTextChanged()
std::string DateTimeComponent::getDisplayString() const
{
if(std::difftime(mTime.getTime(), Utils::Time::BLANK_DATE) == 0.0) {
return "";
}
if (mDisplayRelative) {
//relative time
if(mTime.getTime() == 0)
if(mTime.getTime() == Utils::Time::NOT_A_DATE_TIME)
return "never";
Utils::Time::DateTime now(Utils::Time::now());
@ -69,7 +75,7 @@ std::string DateTimeComponent::getDisplayString() const
return std::string(buf);
}
if(mTime.getTime() == 0)
if(mTime.getTime() == Utils::Time::NOT_A_DATE_TIME)
return "unknown";
return Utils::Time::timeToString(mTime.getTime(), mFormat);

View file

@ -25,6 +25,9 @@ public:
virtual void applyTheme(const std::shared_ptr<ThemeData>& theme, const std::string& view, const std::string& element, unsigned int properties) override;
static std::string getDateformat() { return "%m/%d/%Y"; }
static std::string getDateformatTip() { return "MM/DD/YYYY"; }
protected:
void onTextChanged() override;

View file

@ -1,5 +1,6 @@
#include "components/DateTimeEditComponent.h"
#include "DateTimeComponent.h"
#include "resources/Font.h"
#include "utils/StringUtil.h"
@ -196,17 +197,17 @@ std::string DateTimeEditComponent::getDisplayString(DisplayMode mode) const
switch(mode)
{
case DISP_DATE:
fmt = "%m/%d/%Y";
fmt = DateTimeComponent::getDateformat();
break;
case DISP_DATE_TIME:
if(mTime.getTime() == 0)
if(mTime.getTime() == Utils::Time::NOT_A_DATE_TIME)
return "unknown";
fmt = "%m/%d/%Y %H:%M:%S";
fmt = DateTimeComponent::getDateformat() + " %H:%M:%S";
break;
case DISP_RELATIVE_TO_NOW:
{
//relative time
if(mTime.getTime() == 0)
if(mTime.getTime() == Utils::Time::NOT_A_DATE_TIME)
return "never";
Utils::Time::DateTime now(Utils::Time::now());

View file

@ -9,7 +9,13 @@ namespace Utils
{
namespace Time
{
static inline time_t blankDate() {
// 1970-01-02
tm timeStruct = { 0, 0, 0, 2, 0, 70, 0, 0, -1 };
return mktime(&timeStruct);
}
static int NOT_A_DATE_TIME = 0;
static time_t BLANK_DATE = blankDate();
class DateTime
{