mirror of
https://github.com/RetroDECK/ES-DE.git
synced 2025-04-10 19:15:13 +00:00
Added an orphaned data cleanup utility
This commit is contained in:
parent
26f084c12a
commit
6e25eb2698
|
@ -38,6 +38,7 @@ set(ES_HEADERS
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMenu.h
|
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMenu.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMetaDataEd.h
|
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMetaDataEd.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiOfflineGenerator.h
|
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiOfflineGenerator.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiOrphanedDataCleanup.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperMenu.h
|
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperMenu.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperMulti.h
|
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperMulti.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperSearch.h
|
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperSearch.h
|
||||||
|
@ -89,6 +90,7 @@ set(ES_SOURCES
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMediaViewerOptions.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMediaViewerOptions.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMetaDataEd.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMetaDataEd.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiOfflineGenerator.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiOfflineGenerator.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiOrphanedDataCleanup.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperMenu.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperMenu.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperMulti.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperMulti.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperSearch.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperSearch.cpp
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
#include "guis/GuiDetectDevice.h"
|
#include "guis/GuiDetectDevice.h"
|
||||||
#include "guis/GuiMediaViewerOptions.h"
|
#include "guis/GuiMediaViewerOptions.h"
|
||||||
#include "guis/GuiMsgBox.h"
|
#include "guis/GuiMsgBox.h"
|
||||||
|
#include "guis/GuiOrphanedDataCleanup.h"
|
||||||
#include "guis/GuiScraperMenu.h"
|
#include "guis/GuiScraperMenu.h"
|
||||||
#include "guis/GuiScreensaverOptions.h"
|
#include "guis/GuiScreensaverOptions.h"
|
||||||
#include "guis/GuiTextEditKeyboardPopup.h"
|
#include "guis/GuiTextEditKeyboardPopup.h"
|
||||||
|
@ -1739,6 +1740,24 @@ void GuiMenu::openUtilities()
|
||||||
|
|
||||||
ComponentListRow row;
|
ComponentListRow row;
|
||||||
|
|
||||||
|
row.addElement(std::make_shared<TextComponent>("ORPHANED DATA CLEANUP (EXPERIMENTAL)",
|
||||||
|
Font::get(FONT_SIZE_MEDIUM), mMenuColorPrimary),
|
||||||
|
true);
|
||||||
|
row.addElement(mMenu.makeArrow(), false);
|
||||||
|
row.makeAcceptInputHandler(std::bind(
|
||||||
|
[this] { mWindow->pushGui(new GuiOrphanedDataCleanup([&]() { close(true); })); }));
|
||||||
|
s->addRow(row);
|
||||||
|
|
||||||
|
row.elements.clear();
|
||||||
|
row.addElement(std::make_shared<TextComponent>("RESCAN ROM DIRECTORY",
|
||||||
|
Font::get(FONT_SIZE_MEDIUM), mMenuColorPrimary),
|
||||||
|
true);
|
||||||
|
|
||||||
|
// This transparent dummy arrow is only here to enable the "select" help prompt.
|
||||||
|
auto dummyArrow = mMenu.makeArrow();
|
||||||
|
dummyArrow->setOpacity(0.0f);
|
||||||
|
row.addElement(dummyArrow, false);
|
||||||
|
|
||||||
row.makeAcceptInputHandler([s, window, this] {
|
row.makeAcceptInputHandler([s, window, this] {
|
||||||
window->pushGui(new GuiMsgBox(
|
window->pushGui(new GuiMsgBox(
|
||||||
this->getHelpStyle(),
|
this->getHelpStyle(),
|
||||||
|
@ -1751,47 +1770,17 @@ void GuiMenu::openUtilities()
|
||||||
CollectionSystemsManager::getInstance()->exitEditMode();
|
CollectionSystemsManager::getInstance()->exitEditMode();
|
||||||
window->stopInfoPopup();
|
window->stopInfoPopup();
|
||||||
GuiMenu::close(true);
|
GuiMenu::close(true);
|
||||||
// Write any gamelist.xml changes before proceeding with the reload.
|
// Write any gamelist.xml changes before proceeding with the rescan.
|
||||||
if (Settings::getInstance()->getString("SaveGamelistsMode") == "on exit") {
|
if (Settings::getInstance()->getString("SaveGamelistsMode") == "on exit") {
|
||||||
for (auto system : SystemData::sSystemVector)
|
for (auto system : SystemData::sSystemVector)
|
||||||
system->writeMetaData();
|
system->writeMetaData();
|
||||||
}
|
}
|
||||||
window->renderSplashScreen(Window::SplashScreenState::SCANNING, 0.0f);
|
ViewController::getInstance()->rescanROMDirectory();
|
||||||
ViewController::getInstance()->resetAll();
|
|
||||||
CollectionSystemsManager::getInstance()->deinit(false);
|
|
||||||
SystemData::loadConfig();
|
|
||||||
if (SystemData::sStartupExitSignal) {
|
|
||||||
SDL_Event quit;
|
|
||||||
quit.type = SDL_QUIT;
|
|
||||||
SDL_PushEvent(&quit);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (SystemData::sSystemVector.empty()) {
|
|
||||||
// It's possible that there are no longer any games.
|
|
||||||
window->invalidateCachedBackground();
|
|
||||||
ViewController::getInstance()->noGamesDialog();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
ViewController::getInstance()->preload();
|
|
||||||
if (SystemData::sStartupExitSignal) {
|
|
||||||
SDL_Event quit;
|
|
||||||
quit.type = SDL_QUIT;
|
|
||||||
SDL_PushEvent(&quit);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ViewController::getInstance()->goToStart(false);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"NO", nullptr));
|
"NO", nullptr));
|
||||||
});
|
});
|
||||||
auto rescanROMDirectory = std::make_shared<TextComponent>(
|
|
||||||
"RESCAN ROM DIRECTORY", Font::get(FONT_SIZE_MEDIUM), mMenuColorPrimary);
|
|
||||||
rescanROMDirectory->setSelectable(true);
|
|
||||||
row.addElement(rescanROMDirectory, true);
|
|
||||||
s->addRow(row);
|
s->addRow(row);
|
||||||
|
|
||||||
row.elements.clear();
|
|
||||||
|
|
||||||
s->setSize(mSize);
|
s->setSize(mSize);
|
||||||
mWindow->pushGui(s);
|
mWindow->pushGui(s);
|
||||||
}
|
}
|
||||||
|
|
525
es-app/src/guis/GuiOrphanedDataCleanup.cpp
Normal file
525
es-app/src/guis/GuiOrphanedDataCleanup.cpp
Normal file
|
@ -0,0 +1,525 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// EmulationStation Desktop Edition
|
||||||
|
// GuiOrphanedDataCleanup.cpp
|
||||||
|
//
|
||||||
|
// Removes orphaned game media, gamelist.xml entries and custom collections entries.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "guis/GuiOrphanedDataCleanup.h"
|
||||||
|
|
||||||
|
#include "CollectionSystemsManager.h"
|
||||||
|
#include "utils/FileSystemUtil.h"
|
||||||
|
#include "utils/PlatformUtil.h"
|
||||||
|
#include "views/ViewController.h"
|
||||||
|
|
||||||
|
#include <SDL2/SDL.h>
|
||||||
|
|
||||||
|
GuiOrphanedDataCleanup::GuiOrphanedDataCleanup(std::function<void()> reloadCallback)
|
||||||
|
: mRenderer {Renderer::getInstance()}
|
||||||
|
, mBackground {":/graphics/frame.svg"}
|
||||||
|
, mGrid {glm::ivec2 {4, 11}}
|
||||||
|
, mReloadCallback {reloadCallback}
|
||||||
|
, mCursorPos {0}
|
||||||
|
, mMediaDirectory {FileData::getMediaDirectory()}
|
||||||
|
, mMediaTypes {"3dboxes", "backcovers", "covers", "fanart",
|
||||||
|
"manuals", "marquees", "miximages", "physicalmedia",
|
||||||
|
"screenshots", "titlescreens", "videos"}
|
||||||
|
, mIsProcessing {false}
|
||||||
|
, mStopProcessing {false}
|
||||||
|
, mCompleted {false}
|
||||||
|
, mFailed {false}
|
||||||
|
, mNeedsReloading {false}
|
||||||
|
, mProcessedCount {0}
|
||||||
|
, mHasCustomCollections {false}
|
||||||
|
, mCleanupType {CleanupType::MEDIA}
|
||||||
|
{
|
||||||
|
addChild(&mBackground);
|
||||||
|
addChild(&mGrid);
|
||||||
|
|
||||||
|
mMediaDescription =
|
||||||
|
"THIS WILL REMOVE ALL MEDIA FILES WHERE NO MATCHING GAME FILES CAN BE FOUND. "
|
||||||
|
"THESE FILES WILL BE MOVED TO A CLEANUP FOLDER INSIDE YOUR GAME MEDIA "
|
||||||
|
"DIRECTORY. YOU CAN MANUALLY DELETE THIS FOLDER WHEN YOU ARE SURE IT'S NO "
|
||||||
|
"LONGER NEEDED.";
|
||||||
|
|
||||||
|
mGamelistDescription =
|
||||||
|
"THIS WILL REMOVE ALL ENTRIES FROM YOUR GAMELIST XML FILES WHERE NO MATCHING "
|
||||||
|
"GAME FILES CAN BE FOUND. BACKUPS OF THE ORIGINAL FILES WILL BE SAVED TO A CLEANUP FOLDER "
|
||||||
|
"INSIDE YOUR GAMELISTS DIRECTORY. YOU CAN MANUALLY DELETE THIS FOLDER WHEN YOU ARE SURE "
|
||||||
|
"IT'S NO LONGER NEEDED.";
|
||||||
|
|
||||||
|
mCollectionsDescription =
|
||||||
|
"THIS WILL REMOVE ALL ENTRIES FROM YOUR CUSTOM COLLECTIONS CONFIGURATION FILES WHERE NO "
|
||||||
|
"MATCHING GAME FILES CAN BE FOUND. BACKUPS OF THE ORIGINAL FILES WILL BE SAVED TO A "
|
||||||
|
"CLEANUP FOLDER INSIDE YOUR COLLECTIONS DIRECTORY. ONLY CURRENTLY ENABLED COLLECTIONS WILL "
|
||||||
|
"BE PROCESSED.";
|
||||||
|
|
||||||
|
// Stop any ongoing custom collections editing.
|
||||||
|
if (CollectionSystemsManager::getInstance()->isEditing())
|
||||||
|
CollectionSystemsManager::getInstance()->exitEditMode();
|
||||||
|
|
||||||
|
for (auto& collection : CollectionSystemsManager::getInstance()->getCustomCollectionSystems()) {
|
||||||
|
if (collection.second.isEnabled)
|
||||||
|
mHasCustomCollections = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up grid.
|
||||||
|
mTitle = std::make_shared<TextComponent>("ORPHANED DATA CLEANUP", Font::get(FONT_SIZE_LARGE),
|
||||||
|
mMenuColorTitle, ALIGN_CENTER);
|
||||||
|
mGrid.setEntry(mTitle, glm::ivec2 {0, 0}, false, true, glm::ivec2 {4, 1},
|
||||||
|
GridFlags::BORDER_NONE);
|
||||||
|
|
||||||
|
mStatus = std::make_shared<TextComponent>("NOT STARTED", Font::get(FONT_SIZE_MEDIUM),
|
||||||
|
mMenuColorPrimary, ALIGN_CENTER);
|
||||||
|
mGrid.setEntry(mStatus, glm::ivec2 {0, 1}, false, true, glm::ivec2 {4, 1},
|
||||||
|
GridFlags::BORDER_NONE);
|
||||||
|
|
||||||
|
// Spacer row with bottom border.
|
||||||
|
mGrid.setEntry(std::make_shared<GuiComponent>(), glm::ivec2 {0, 2}, false, false,
|
||||||
|
glm::ivec2 {4, 1}, GridFlags::BORDER_BOTTOM);
|
||||||
|
|
||||||
|
mDescriptionHeader = std::make_shared<TextComponent>("DESCRIPTION:", Font::get(FONT_SIZE_MINI),
|
||||||
|
mMenuColorPrimary, ALIGN_LEFT);
|
||||||
|
mGrid.setEntry(mDescriptionHeader, glm::ivec2 {1, 3}, false, true, glm::ivec2 {2, 1});
|
||||||
|
|
||||||
|
mDescription = std::make_shared<TextComponent>(mMediaDescription, Font::get(FONT_SIZE_MEDIUM),
|
||||||
|
mMenuColorPrimary, ALIGN_LEFT, ALIGN_TOP);
|
||||||
|
mGrid.setEntry(mDescription, glm::ivec2 {1, 4}, false, true, glm::ivec2 {2, 1});
|
||||||
|
|
||||||
|
mEntryCountHeader = std::make_shared<TextComponent>(
|
||||||
|
"TOTAL ENTRIES REMOVED:", Font::get(FONT_SIZE_SMALL), mMenuColorPrimary, ALIGN_LEFT);
|
||||||
|
mGrid.setEntry(mEntryCountHeader, glm::ivec2 {1, 6}, false, true, glm::ivec2 {1, 1});
|
||||||
|
|
||||||
|
mEntryCount = std::make_shared<TextComponent>("0", Font::get(FONT_SIZE_SMALL),
|
||||||
|
mMenuColorPrimary, ALIGN_LEFT);
|
||||||
|
mGrid.setEntry(mEntryCount, glm::ivec2 {2, 6}, false, true, glm::ivec2 {1, 1});
|
||||||
|
|
||||||
|
mSystemProcessingHeader = std::make_shared<TextComponent>(
|
||||||
|
"PROCESSING SYSTEM:", Font::get(FONT_SIZE_SMALL), mMenuColorPrimary, ALIGN_LEFT);
|
||||||
|
mGrid.setEntry(mSystemProcessingHeader, glm::ivec2 {1, 7}, false, true, glm::ivec2 {1, 1});
|
||||||
|
|
||||||
|
mSystemProcessing = std::make_shared<TextComponent>("", Font::get(FONT_SIZE_SMALL),
|
||||||
|
mMenuColorPrimary, ALIGN_LEFT);
|
||||||
|
mGrid.setEntry(mSystemProcessing, glm::ivec2 {2, 7}, false, true, glm::ivec2 {1, 1});
|
||||||
|
|
||||||
|
mErrorHeader = std::make_shared<TextComponent>(
|
||||||
|
"LAST ERROR MESSAGE:", Font::get(FONT_SIZE_SMALL), mMenuColorPrimary, ALIGN_LEFT);
|
||||||
|
mGrid.setEntry(mErrorHeader, glm::ivec2 {1, 8}, false, true, glm::ivec2 {1, 1});
|
||||||
|
|
||||||
|
mError =
|
||||||
|
std::make_shared<TextComponent>("", Font::get(FONT_SIZE_SMALL), mMenuColorRed, ALIGN_LEFT);
|
||||||
|
mGrid.setEntry(mError, glm::ivec2 {2, 8}, false, true, glm::ivec2 {1, 1});
|
||||||
|
|
||||||
|
// Spacer row.
|
||||||
|
mGrid.setEntry(std::make_shared<GuiComponent>(), glm::ivec2 {1, 9}, false, false,
|
||||||
|
glm::ivec2 {2, 1});
|
||||||
|
|
||||||
|
// Buttons.
|
||||||
|
std::vector<std::shared_ptr<ButtonComponent>> buttons;
|
||||||
|
|
||||||
|
mButton1 = std::make_shared<ButtonComponent>("MEDIA", "start processing", [this]() {
|
||||||
|
if (mIsProcessing)
|
||||||
|
return;
|
||||||
|
if (mThread) {
|
||||||
|
mThread->join();
|
||||||
|
mThread.reset();
|
||||||
|
}
|
||||||
|
mProcessedCount = 0;
|
||||||
|
mCurrentSystem = "";
|
||||||
|
mCleanupType = CleanupType::MEDIA;
|
||||||
|
mIsProcessing = true;
|
||||||
|
mCompleted = false;
|
||||||
|
mFailed = false;
|
||||||
|
mStopProcessing = false;
|
||||||
|
mError->setValue("");
|
||||||
|
mEntryCount->setValue("0");
|
||||||
|
mStatus->setValue("RUNNING MEDIA CLEANUP");
|
||||||
|
mButton4->setText("STOP", "stop processing");
|
||||||
|
mThread = std::make_unique<std::thread>(&GuiOrphanedDataCleanup::cleanupMediaFiles, this);
|
||||||
|
});
|
||||||
|
|
||||||
|
buttons.push_back(mButton1);
|
||||||
|
|
||||||
|
mButton2 = std::make_shared<ButtonComponent>("GAMELISTS", "start processing", [this]() {
|
||||||
|
if (mIsProcessing)
|
||||||
|
return;
|
||||||
|
if (mThread) {
|
||||||
|
mThread->join();
|
||||||
|
mThread.reset();
|
||||||
|
}
|
||||||
|
mProcessedCount = 0;
|
||||||
|
mCurrentSystem = "";
|
||||||
|
mCleanupType = CleanupType::GAMELISTS;
|
||||||
|
mIsProcessing = true;
|
||||||
|
mCompleted = false;
|
||||||
|
mFailed = false;
|
||||||
|
mStopProcessing = false;
|
||||||
|
mError->setValue("");
|
||||||
|
mEntryCount->setValue("0");
|
||||||
|
mStatus->setValue("RUNNING GAMELISTS CLEANUP");
|
||||||
|
mButton4->setText("STOP", "stop processing");
|
||||||
|
// Write any gamelist.xml changes before proceeding with the cleanup.
|
||||||
|
if (Settings::getInstance()->getString("SaveGamelistsMode") == "on exit") {
|
||||||
|
for (auto system : SystemData::sSystemVector)
|
||||||
|
system->writeMetaData();
|
||||||
|
}
|
||||||
|
mThread = std::make_unique<std::thread>(&GuiOrphanedDataCleanup::cleanupGamelists, this);
|
||||||
|
});
|
||||||
|
buttons.push_back(mButton2);
|
||||||
|
|
||||||
|
mButton3 = std::make_shared<ButtonComponent>("COLLECTIONS", "start processing", [this]() {
|
||||||
|
if (mIsProcessing)
|
||||||
|
return;
|
||||||
|
if (!mHasCustomCollections) {
|
||||||
|
mError->setValue("THERE ARE NO ENABLED CUSTOM COLLECTIONS");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mThread) {
|
||||||
|
mThread->join();
|
||||||
|
mThread.reset();
|
||||||
|
}
|
||||||
|
mProcessedCount = 0;
|
||||||
|
mCurrentSystem = "";
|
||||||
|
mCleanupType = CleanupType::COLLECTIONS;
|
||||||
|
mIsProcessing = true;
|
||||||
|
mCompleted = false;
|
||||||
|
mFailed = false;
|
||||||
|
mStopProcessing = false;
|
||||||
|
mError->setValue("");
|
||||||
|
mEntryCount->setValue("0");
|
||||||
|
mStatus->setValue("RUNNING COLLECTIONS CLEANUP");
|
||||||
|
mButton4->setText("STOP", "stop processing");
|
||||||
|
mThread = std::make_unique<std::thread>(&GuiOrphanedDataCleanup::cleanupCollections, this);
|
||||||
|
});
|
||||||
|
buttons.push_back(mButton3);
|
||||||
|
|
||||||
|
mButton4 = std::make_shared<ButtonComponent>("CLOSE", "close", [this]() {
|
||||||
|
if (mIsProcessing) {
|
||||||
|
mStopProcessing = true;
|
||||||
|
if (mThread) {
|
||||||
|
mThread->join();
|
||||||
|
mThread.reset();
|
||||||
|
}
|
||||||
|
mButton4->setText("CLOSE", "close");
|
||||||
|
}
|
||||||
|
else if (mNeedsReloading) {
|
||||||
|
ViewController::getInstance()->rescanROMDirectory();
|
||||||
|
mReloadCallback();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
delete this;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
buttons.push_back(mButton4);
|
||||||
|
|
||||||
|
mButtons = MenuComponent::makeButtonGrid(buttons);
|
||||||
|
mGrid.setEntry(mButtons, glm::ivec2 {0, 10}, true, false, glm::ivec2 {4, 1},
|
||||||
|
GridFlags::BORDER_TOP);
|
||||||
|
|
||||||
|
// Limit the width of the GUI on ultrawide monitors. The 1.778 aspect ratio value is
|
||||||
|
// the 16:9 reference.
|
||||||
|
const float aspectValue {1.778f / Renderer::getScreenAspectRatio()};
|
||||||
|
const float width {glm::clamp(0.80f * aspectValue, 0.45f,
|
||||||
|
(mRenderer->getIsVerticalOrientation() ? 0.95f : 0.80f)) *
|
||||||
|
mRenderer->getScreenWidth()};
|
||||||
|
setSize(width,
|
||||||
|
mTitle->getSize().y +
|
||||||
|
(FONT_SIZE_MEDIUM * 1.5f * (mRenderer->getIsVerticalOrientation() ? 9.7f : 8.7f)) +
|
||||||
|
mButtons->getSize().y);
|
||||||
|
|
||||||
|
setPosition((mRenderer->getScreenWidth() - mSize.x) / 2.0f,
|
||||||
|
(mRenderer->getScreenHeight() - mSize.y) / 2.0f);
|
||||||
|
|
||||||
|
setPosition((mRenderer->getScreenWidth() - mSize.x) / 2.0f,
|
||||||
|
std::round(mRenderer->getScreenHeight() * 0.13f));
|
||||||
|
|
||||||
|
mBusyAnim.setSize(mSize);
|
||||||
|
mBusyAnim.setText("PROCESSING");
|
||||||
|
mBusyAnim.onSizeChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
GuiOrphanedDataCleanup::~GuiOrphanedDataCleanup()
|
||||||
|
{
|
||||||
|
mStopProcessing = true;
|
||||||
|
|
||||||
|
if (mThread)
|
||||||
|
mThread->join();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GuiOrphanedDataCleanup::cleanupMediaFiles()
|
||||||
|
{
|
||||||
|
LOG(LogInfo) << "GuiOrphanedDataCleanup: Starting cleanup of game media";
|
||||||
|
|
||||||
|
const std::time_t currentTime {
|
||||||
|
std::chrono::system_clock::to_time_t(std::chrono::system_clock::now())};
|
||||||
|
|
||||||
|
for (auto system : SystemData::sSystemVector) {
|
||||||
|
if (mStopProcessing)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (system->isCollection())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock {mMutex};
|
||||||
|
mCurrentSystem = system->getName() + " (" + system->getFullName() + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> systemFilesRelative;
|
||||||
|
for (auto& systemFile : system->getRootFolder()->getFilesRecursive(GAME | FOLDER)) {
|
||||||
|
std::string fileEntry {systemFile->getPath()};
|
||||||
|
// Check that game entries are not directories as this may be the case when using the
|
||||||
|
// directories interpreted as files functionality.
|
||||||
|
if (systemFile->getType() == GAME && !Utils::FileSystem::isDirectory(fileEntry)) {
|
||||||
|
// If the file has an extension, then remove it.
|
||||||
|
const size_t separatorPos {fileEntry.find_last_of('/')};
|
||||||
|
if (fileEntry.substr(separatorPos).find_last_of('.') != std::string::npos)
|
||||||
|
fileEntry = fileEntry.substr(0, fileEntry.find_last_of('.'));
|
||||||
|
}
|
||||||
|
systemFilesRelative.emplace_back(
|
||||||
|
fileEntry.substr(system->getSystemEnvData()->mStartPath.length() + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> cleanupFiles;
|
||||||
|
const std::string systemMediaDir {mMediaDirectory + system->getName()};
|
||||||
|
for (auto& mediaType : mMediaTypes) {
|
||||||
|
const std::string mediaTypeDir {systemMediaDir + "/" + mediaType};
|
||||||
|
const Utils::FileSystem::StringList& dirContent {
|
||||||
|
Utils::FileSystem::getDirContent(mediaTypeDir, true)};
|
||||||
|
for (auto& mediaFile : dirContent) {
|
||||||
|
std::string relativePath {mediaFile.substr(mediaTypeDir.length() + 1)};
|
||||||
|
relativePath = relativePath.substr(0, relativePath.find_last_of('.'));
|
||||||
|
if (std::find(systemFilesRelative.cbegin(), systemFilesRelative.cend(),
|
||||||
|
relativePath) == systemFilesRelative.end()) {
|
||||||
|
cleanupFiles.emplace_back(mediaFile);
|
||||||
|
LOG(LogInfo) << "Found orphaned media file \"" << mediaFile << "\"";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mStopProcessing)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (cleanupFiles.size() > 0) {
|
||||||
|
std::string dateString(20, '\0');
|
||||||
|
std::strftime(&dateString[0], 20, "%Y-%m-%d_%H%M%S", localtime(¤tTime));
|
||||||
|
dateString.erase(dateString.find('\0'));
|
||||||
|
const std::string targetDirectory {mMediaDirectory + "CLEANUP/" + dateString + "/"};
|
||||||
|
|
||||||
|
LOG(LogInfo) << "Moving orphaned files to \"" << targetDirectory << "\"";
|
||||||
|
|
||||||
|
for (auto& file : cleanupFiles) {
|
||||||
|
const std::string fileDirectory {
|
||||||
|
targetDirectory +
|
||||||
|
Utils::FileSystem::getParent(file.substr(mMediaDirectory.length()))};
|
||||||
|
const std::string fileName {Utils::FileSystem::getFileName(file)};
|
||||||
|
if (!Utils::FileSystem::isDirectory(fileDirectory) &&
|
||||||
|
!Utils::FileSystem::createDirectory(fileDirectory)) {
|
||||||
|
LOG(LogError) << "Couldn't create target directory \"" << fileDirectory << "\"";
|
||||||
|
mErrorMessage = "COULDN'T CREATE TARGET DIRECTORY, PERMISSION PROBLEMS?";
|
||||||
|
mFailed = true;
|
||||||
|
mIsProcessing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Utils::FileSystem::renameFile(file, fileDirectory + "/" + fileName, false)) {
|
||||||
|
LOG(LogError) << "Couldn't move file \"" << file << "\"";
|
||||||
|
mErrorMessage = "COULDN'T MOVE MEDIA FILE, PERMISSION PROBLEMS?";
|
||||||
|
mFailed = true;
|
||||||
|
mIsProcessing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
++mProcessedCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Delay(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
mIsProcessing = false;
|
||||||
|
mCompleted = true;
|
||||||
|
LOG(LogInfo) << "GuiOrphanedDataCleanup: Completed cleanup of game media, removed "
|
||||||
|
<< mProcessedCount << " file" << (mProcessedCount == 1 ? "" : "s");
|
||||||
|
}
|
||||||
|
|
||||||
|
void GuiOrphanedDataCleanup::cleanupGamelists()
|
||||||
|
{
|
||||||
|
LOG(LogInfo) << "GuiOrphanedDataCleanup: Starting cleanup of gamelist.xml files";
|
||||||
|
|
||||||
|
for (auto system : SystemData::sSystemVector) {
|
||||||
|
if (mStopProcessing)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (system->isCollection())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock {mMutex};
|
||||||
|
mCurrentSystem = system->getName() + " (" + system->getFullName() + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mStopProcessing)
|
||||||
|
break;
|
||||||
|
|
||||||
|
mNeedsReloading = true;
|
||||||
|
|
||||||
|
SDL_Delay(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
mIsProcessing = false;
|
||||||
|
mCompleted = true;
|
||||||
|
LOG(LogInfo) << "GuiOrphanedDataCleanup: Completed cleanup of gamelist.xml files, removed "
|
||||||
|
<< mProcessedCount << (mProcessedCount == 1 ? " entry" : " entries");
|
||||||
|
}
|
||||||
|
|
||||||
|
void GuiOrphanedDataCleanup::cleanupCollections()
|
||||||
|
{
|
||||||
|
LOG(LogInfo)
|
||||||
|
<< "GuiOrphanedDataCleanup: Starting cleanup of custom collections configuration files";
|
||||||
|
|
||||||
|
for (auto& collection : CollectionSystemsManager::getInstance()->getCustomCollectionSystems()) {
|
||||||
|
if (mStopProcessing)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (!collection.second.isEnabled)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock {mMutex};
|
||||||
|
mCurrentSystem = collection.second.system->getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mStopProcessing)
|
||||||
|
break;
|
||||||
|
|
||||||
|
mNeedsReloading = true;
|
||||||
|
|
||||||
|
SDL_Delay(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
mIsProcessing = false;
|
||||||
|
mCompleted = true;
|
||||||
|
LOG(LogInfo) << "GuiOrphanedDataCleanup: Completed cleanup of custom collections configuration "
|
||||||
|
"files, removed "
|
||||||
|
<< mProcessedCount << (mProcessedCount == 1 ? " entry" : " entries");
|
||||||
|
}
|
||||||
|
|
||||||
|
void GuiOrphanedDataCleanup::update(int deltaTime)
|
||||||
|
{
|
||||||
|
if (mIsProcessing) {
|
||||||
|
mBusyAnim.update(deltaTime);
|
||||||
|
if (mEntryCount->getValue() != std::to_string(mProcessedCount))
|
||||||
|
mEntryCount->setValue(std::to_string(mProcessedCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mCompleted) {
|
||||||
|
std::string message {mStopProcessing ? "ABORTED" : "COMPLETED"};
|
||||||
|
if (mCleanupType == CleanupType::MEDIA)
|
||||||
|
message.append(" MEDIA ");
|
||||||
|
else if (mCleanupType == CleanupType::GAMELISTS)
|
||||||
|
message.append(" GAMELISTS ");
|
||||||
|
else
|
||||||
|
message.append(" COLLECTIONS ");
|
||||||
|
message.append("CLEANUP");
|
||||||
|
mStatus->setValue(message);
|
||||||
|
mSystemProcessing->setValue("");
|
||||||
|
mButton4->setText("CLOSE", "close");
|
||||||
|
mCompleted = false;
|
||||||
|
}
|
||||||
|
else if (mFailed) {
|
||||||
|
std::string message;
|
||||||
|
if (mCleanupType == CleanupType::MEDIA)
|
||||||
|
message.append("MEDIA CLEANUP FAILED");
|
||||||
|
else if (mCleanupType == CleanupType::GAMELISTS)
|
||||||
|
message.append("GAMELISTS CLEANUP FAILED");
|
||||||
|
else
|
||||||
|
message.append("COLLECTIONS CLEANUP FAILED");
|
||||||
|
mStatus->setValue(message);
|
||||||
|
mError->setValue(mErrorMessage);
|
||||||
|
mFailed = false;
|
||||||
|
}
|
||||||
|
else if (mIsProcessing) {
|
||||||
|
std::unique_lock<std::mutex> lock {mMutex};
|
||||||
|
if (mSystemProcessing->getValue() != mCurrentSystem)
|
||||||
|
mSystemProcessing->setValue(mCurrentSystem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GuiOrphanedDataCleanup::render(const glm::mat4& parentTrans)
|
||||||
|
{
|
||||||
|
glm::mat4 trans {parentTrans * getTransform()};
|
||||||
|
renderChildren(trans);
|
||||||
|
|
||||||
|
if (mIsProcessing)
|
||||||
|
mBusyAnim.render(trans);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GuiOrphanedDataCleanup::onSizeChanged()
|
||||||
|
{
|
||||||
|
const float screenSize {mRenderer->getIsVerticalOrientation() ? mRenderer->getScreenWidth() :
|
||||||
|
mRenderer->getScreenHeight()};
|
||||||
|
mGrid.setRowHeightPerc(0, (mTitle->getFont()->getLetterHeight() + screenSize * 0.2f) / mSize.y /
|
||||||
|
2.0f);
|
||||||
|
mGrid.setRowHeightPerc(1, (mStatus->getFont()->getLetterHeight() + 2.0f) / mSize.y, false);
|
||||||
|
mGrid.setRowHeightPerc(2, (mStatus->getFont()->getLetterHeight() * 0.5f) / mSize.y, false);
|
||||||
|
mGrid.setRowHeightPerc(
|
||||||
|
3, (mDescriptionHeader->getFont()->getLetterHeight() + screenSize * 0.2f) / mSize.y / 4.0f);
|
||||||
|
mGrid.setRowHeightPerc(4, (mDescription->getFont()->getLetterHeight() * 8.5f) / mSize.y);
|
||||||
|
mGrid.setRowHeightPerc(5, (mStatus->getFont()->getLetterHeight() * 0.3f) / mSize.y);
|
||||||
|
mGrid.setRowHeightPerc(
|
||||||
|
6, (mEntryCountHeader->getFont()->getLetterHeight() + screenSize * 0.2f) / mSize.y / 4.0f);
|
||||||
|
mGrid.setRowHeightPerc(
|
||||||
|
7, (mSystemProcessingHeader->getFont()->getLetterHeight() + screenSize * 0.2f) / mSize.y /
|
||||||
|
4.0f);
|
||||||
|
mGrid.setRowHeightPerc(8, (mErrorHeader->getFont()->getLetterHeight() + screenSize * 0.2f) /
|
||||||
|
mSize.y / 4.0f);
|
||||||
|
mGrid.setRowHeightPerc(10, mButtons->getSize().y / mSize.y);
|
||||||
|
|
||||||
|
mGrid.setColWidthPerc(0, 0.01f);
|
||||||
|
mGrid.setColWidthPerc(1, 0.25f);
|
||||||
|
mGrid.setColWidthPerc(3, 0.01f);
|
||||||
|
|
||||||
|
mGrid.setSize(mSize);
|
||||||
|
mBackground.fitTo(mSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GuiOrphanedDataCleanup::input(InputConfig* config, Input input)
|
||||||
|
{
|
||||||
|
if (input.value &&
|
||||||
|
(config->isMappedLike("left", input) || config->isMappedLike("right", input))) {
|
||||||
|
const int prevCursorPos {mCursorPos};
|
||||||
|
if (config->isMappedLike("left", input)) {
|
||||||
|
if (mCursorPos > 0)
|
||||||
|
--mCursorPos;
|
||||||
|
}
|
||||||
|
else if (config->isMappedLike("right", input)) {
|
||||||
|
if (mCursorPos < 3)
|
||||||
|
++mCursorPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mCursorPos != prevCursorPos) {
|
||||||
|
if (mCursorPos == 0) {
|
||||||
|
mDescription->setValue(mMediaDescription);
|
||||||
|
}
|
||||||
|
else if (mCursorPos == 1) {
|
||||||
|
mDescription->setValue(mGamelistDescription);
|
||||||
|
}
|
||||||
|
else if (mCursorPos == 2) {
|
||||||
|
mDescription->setValue(mCollectionsDescription);
|
||||||
|
}
|
||||||
|
else if (mCursorPos == 3) {
|
||||||
|
mDescription->setValue(
|
||||||
|
mNeedsReloading ? "THE APPLICATION WILL RELOAD WHEN CLOSING THIS UTILITY." :
|
||||||
|
"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return GuiComponent::input(config, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<HelpPrompt> GuiOrphanedDataCleanup::getHelpPrompts()
|
||||||
|
{
|
||||||
|
std::vector<HelpPrompt> prompts {mGrid.getHelpPrompts()};
|
||||||
|
return prompts;
|
||||||
|
}
|
94
es-app/src/guis/GuiOrphanedDataCleanup.h
Normal file
94
es-app/src/guis/GuiOrphanedDataCleanup.h
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// EmulationStation Desktop Edition
|
||||||
|
// GuiOrphanedDataCleanup.h
|
||||||
|
//
|
||||||
|
// Removes orphaned game media, gamelist.xml entries and custom collections entries.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef ES_APP_GUIS_GUI_ORPHANED_DATA_CLEANUP_H
|
||||||
|
#define ES_APP_GUIS_GUI_ORPHANED_DATA_CLEANUP_H
|
||||||
|
|
||||||
|
#include "GuiComponent.h"
|
||||||
|
#include "components/BusyComponent.h"
|
||||||
|
#include "guis/GuiSettings.h"
|
||||||
|
#include "views/ViewController.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <mutex>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
class GuiOrphanedDataCleanup : public GuiComponent
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GuiOrphanedDataCleanup(std::function<void()> reloadCallback);
|
||||||
|
~GuiOrphanedDataCleanup();
|
||||||
|
|
||||||
|
void cleanupMediaFiles();
|
||||||
|
void cleanupGamelists();
|
||||||
|
void cleanupCollections();
|
||||||
|
|
||||||
|
void update(int deltaTime) override;
|
||||||
|
void render(const glm::mat4& parentTrans) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void onSizeChanged() override;
|
||||||
|
bool input(InputConfig* config, Input input) override;
|
||||||
|
|
||||||
|
std::vector<HelpPrompt> getHelpPrompts() override;
|
||||||
|
HelpStyle getHelpStyle() override { return ViewController::getInstance()->getViewHelpStyle(); }
|
||||||
|
|
||||||
|
Renderer* mRenderer;
|
||||||
|
NinePatchComponent mBackground;
|
||||||
|
ComponentGrid mGrid;
|
||||||
|
BusyComponent mBusyAnim;
|
||||||
|
std::function<void()> mReloadCallback;
|
||||||
|
|
||||||
|
std::shared_ptr<ComponentGrid> mButtons;
|
||||||
|
std::shared_ptr<ButtonComponent> mButton1;
|
||||||
|
std::shared_ptr<ButtonComponent> mButton2;
|
||||||
|
std::shared_ptr<ButtonComponent> mButton3;
|
||||||
|
std::shared_ptr<ButtonComponent> mButton4;
|
||||||
|
|
||||||
|
std::shared_ptr<TextComponent> mTitle;
|
||||||
|
std::shared_ptr<TextComponent> mStatus;
|
||||||
|
std::shared_ptr<TextComponent> mDescriptionHeader;
|
||||||
|
std::shared_ptr<TextComponent> mDescription;
|
||||||
|
std::shared_ptr<TextComponent> mSystemProcessingHeader;
|
||||||
|
std::shared_ptr<TextComponent> mEntryCountHeader;
|
||||||
|
std::shared_ptr<TextComponent> mSystemProcessing;
|
||||||
|
std::shared_ptr<TextComponent> mEntryCount;
|
||||||
|
std::shared_ptr<TextComponent> mErrorHeader;
|
||||||
|
std::shared_ptr<TextComponent> mError;
|
||||||
|
|
||||||
|
std::unique_ptr<std::thread> mThread;
|
||||||
|
std::mutex mMutex;
|
||||||
|
int mCursorPos;
|
||||||
|
|
||||||
|
std::string mMediaDescription;
|
||||||
|
std::string mGamelistDescription;
|
||||||
|
std::string mCollectionsDescription;
|
||||||
|
std::string mCurrentSystem;
|
||||||
|
std::string mErrorMessage;
|
||||||
|
|
||||||
|
std::string mMediaDirectory;
|
||||||
|
std::vector<std::string> mMediaTypes;
|
||||||
|
|
||||||
|
std::atomic<bool> mIsProcessing;
|
||||||
|
std::atomic<bool> mStopProcessing;
|
||||||
|
std::atomic<bool> mCompleted;
|
||||||
|
std::atomic<bool> mFailed;
|
||||||
|
std::atomic<bool> mNeedsReloading;
|
||||||
|
std::atomic<int> mProcessedCount;
|
||||||
|
bool mHasCustomCollections;
|
||||||
|
|
||||||
|
enum class CleanupType {
|
||||||
|
MEDIA,
|
||||||
|
GAMELISTS,
|
||||||
|
COLLECTIONS
|
||||||
|
};
|
||||||
|
|
||||||
|
CleanupType mCleanupType;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // ES_APP_GUIS_GUI_ORPHANED_DATA_CLEANUP_H
|
|
@ -1408,13 +1408,40 @@ void ViewController::reloadAll()
|
||||||
updateHelpPrompts();
|
updateHelpPrompts();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ViewController::resetAll()
|
void ViewController::rescanROMDirectory()
|
||||||
{
|
{
|
||||||
mGamelistViews.clear();
|
mGamelistViews.clear();
|
||||||
mSystemListView.reset();
|
mSystemListView.reset();
|
||||||
mCurrentView.reset();
|
mCurrentView.reset();
|
||||||
mPreviousView.reset();
|
mPreviousView.reset();
|
||||||
mSkipView.reset();
|
mSkipView.reset();
|
||||||
|
|
||||||
|
mWindow->renderSplashScreen(Window::SplashScreenState::SCANNING, 0.0f);
|
||||||
|
CollectionSystemsManager::getInstance()->deinit(false);
|
||||||
|
SystemData::loadConfig();
|
||||||
|
|
||||||
|
if (SystemData::sStartupExitSignal) {
|
||||||
|
SDL_Event quit;
|
||||||
|
quit.type = SDL_QUIT;
|
||||||
|
SDL_PushEvent(&quit);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SystemData::sSystemVector.empty()) {
|
||||||
|
// It's possible that there are no longer any games.
|
||||||
|
mWindow->invalidateCachedBackground();
|
||||||
|
noGamesDialog();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
preload();
|
||||||
|
if (SystemData::sStartupExitSignal) {
|
||||||
|
SDL_Event quit;
|
||||||
|
quit.type = SDL_QUIT;
|
||||||
|
SDL_PushEvent(&quit);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
goToStart(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<HelpPrompt> ViewController::getHelpPrompts()
|
std::vector<HelpPrompt> ViewController::getHelpPrompts()
|
||||||
|
|
|
@ -54,8 +54,8 @@ public:
|
||||||
// Used when the "ThemeSet" setting changes.
|
// Used when the "ThemeSet" setting changes.
|
||||||
void reloadAll();
|
void reloadAll();
|
||||||
|
|
||||||
// Reset all views, which is needed when rescanning the ROM directory.
|
// Rescan the ROM directory for any changes to games and systems.
|
||||||
void resetAll();
|
void rescanROMDirectory();
|
||||||
|
|
||||||
// Navigation.
|
// Navigation.
|
||||||
void goToNextGamelist();
|
void goToNextGamelist();
|
||||||
|
|
Loading…
Reference in a new issue