From ad436512580ab56a623834e223eef11cf3084bb3 Mon Sep 17 00:00:00 2001 From: TSOLfan Date: Tue, 17 Mar 2026 13:31:14 -0400 Subject: [PATCH 1/2] feat: Collection Display Customizations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Includes the following new settings under Appearance: - Show Collections Promotion If all systems are hidden, the files in the Collection folder are force promoted to the main screen. This setting controls whether or not that will happen if you want to keep them behind a Collections menu item. - Sort Collections Entries This setting will force entries in a Collection file to be displayed in the order they are in the file. A use case would be to list games in a specific order, such as order of release for games in a series. - Use Collections Nested Map Collections only support a single map.txt file in the root of the Collections folder. This will check for a map.txt file in a Collections subfolder and use it for entries in that subfolder. If one doesn’t exist, it will fall back and look for one in the Collections folder root. A use case would be adding platform tags to games in a Franchise specific collection folder, and have the custom names appear only there and not in a separate collection the same game may appear in such as another collection of just games for a single platform. - Show Quickswitcher UI Games Icon If all systems are hidden for a “Collections Only” setup, the Games icon will still display in the Menu alongside the Collections icon. This allows you to control whether or not you want it to still display. --- workspace/all/common/config.c | 92 +++++++++++++++++++++++++++++ workspace/all/common/config.h | 20 +++++++ workspace/all/nextui/nextui.c | 30 ++++++++-- workspace/all/settings/settings.cpp | 16 +++++ 4 files changed, 152 insertions(+), 6 deletions(-) diff --git a/workspace/all/common/config.c b/workspace/all/common/config.c index f47c4e32d..9f34d8b00 100644 --- a/workspace/all/common/config.c +++ b/workspace/all/common/config.c @@ -50,10 +50,14 @@ void CFG_defaults(NextUISettings *cfg) .showRecents = CFG_DEFAULT_SHOWRECENTS, .showTools = CFG_DEFAULT_SHOWTOOLS, .showCollections = CFG_DEFAULT_SHOWCOLLECTIONS, + .showCollectionsPromotion = CFG_DEFAULT_SHOWCOLLECTIONSPROMOTION, + .sortCollectionsEntries = CFG_DEFAULT_SORTCOLLECTIONSENTRIES, + .useCollectionsNestedMap = CFG_DEFAULT_USECOLLECTIONSNESTEDMAP, .showGameArt = CFG_DEFAULT_SHOWGAMEART, .gameSwitcherScaling = CFG_DEFAULT_GAMESWITCHERSCALING, .defaultView = CFG_DEFAULT_VIEW, .showQuickSwitcherUi = CFG_DEFAULT_SHOWQUICKWITCHERUI, + .showQuickSwitcherUiGames = CFG_DEFAULT_SHOWQUICKWITCHERUIGAMES, .muteLeds = CFG_DEFAULT_MUTELEDS, @@ -205,6 +209,21 @@ void CFG_init(FontLoad_callback_t cb, ColorSet_callback_t ccb) CFG_setShowCollections((bool)temp_value); continue; } + if (sscanf(line, "collectionspromotion=%i", &temp_value) == 1) + { + CFG_setShowCollectionsPromotion((bool)temp_value); + continue; + } + if (sscanf(line, "collectionsentriessort=%i", &temp_value) == 1) + { + CFG_setSortCollectionsEntries((bool)temp_value); + continue; + } + if (sscanf(line, "usecollectionsnestedmap=%i", &temp_value) == 1) + { + CFG_setUseCollectionsNestedMap((bool)temp_value); + continue; + } if (sscanf(line, "gameart=%i", &temp_value) == 1) { CFG_setShowGameArt((bool)temp_value); @@ -285,6 +304,11 @@ void CFG_init(FontLoad_callback_t cb, ColorSet_callback_t ccb) CFG_setShowQuickswitcherUI(temp_value); continue; } + if (sscanf(line, "quickSwitcherUiGames=%i", &temp_value) == 1) + { + CFG_setShowQuickswitcherUIGames(temp_value); + continue; + } if (sscanf(line, "wifiDiagnostics=%i", &temp_value) == 1) { CFG_setWifiDiagnostics(temp_value); @@ -638,6 +662,39 @@ void CFG_setShowCollections(bool show) CFG_sync(); } +bool CFG_getShowCollectionsPromotion(void) +{ + return settings.showCollectionsPromotion; +} + +void CFG_setShowCollectionsPromotion(bool show) +{ + settings.showCollectionsPromotion = show; + CFG_sync(); +} + +bool CFG_getSortCollectionsEntries(void) +{ + return settings.sortCollectionsEntries; +} + +void CFG_setSortCollectionsEntries(bool show) +{ + settings.sortCollectionsEntries = show; + CFG_sync(); +} + +bool CFG_getUseCollectionsNestedMap(void) +{ + return settings.useCollectionsNestedMap; +} + +void CFG_setUseCollectionsNestedMap(bool show) +{ + settings.useCollectionsNestedMap = show; + CFG_sync(); +} + bool CFG_getShowGameArt(void) { return settings.showGameArt; @@ -770,6 +827,17 @@ void CFG_setShowQuickswitcherUI(bool on) CFG_sync(); } +bool CFG_getShowQuickswitcherUIGames(void) +{ + return settings.showQuickSwitcherUiGames; +} + +void CFG_setShowQuickswitcherUIGames(bool on) +{ + settings.showQuickSwitcherUiGames = on; + CFG_sync(); +} + bool CFG_getWifiDiagnostics(void) { return settings.wifiDiagnostics; @@ -1085,6 +1153,18 @@ void CFG_get(const char *key, char *value) else if (strcmp(key, "collections") == 0) { sprintf(value, "%i", CFG_getShowCollections()); + } + else if (strcmp(key, "collectionspromotion") == 0) + { + sprintf(value, "%i", CFG_getShowCollectionsPromotion()); + } + else if (strcmp(key, "collectionsentriessort") == 0) + { + sprintf(value, "%i", CFG_getSortCollectionsEntries()); + } + else if (strcmp(key, "usecollectionsnestedmap") == 0) + { + sprintf(value, "%i", CFG_getUseCollectionsNestedMap()); } else if (strcmp(key, "gameart") == 0) { @@ -1146,6 +1226,10 @@ void CFG_get(const char *key, char *value) { sprintf(value, "%i", (int)(CFG_getShowQuickswitcherUI())); } + else if (strcmp(key, "quickSwitcherUiGames") == 0) + { + sprintf(value, "%i", (int)(CFG_getShowQuickswitcherUIGames())); + } else if (strcmp(key, "wifiDiagnostics") == 0) { sprintf(value, "%i", (int)(CFG_getWifiDiagnostics())); @@ -1221,6 +1305,9 @@ void CFG_sync(void) fprintf(file, "recents=%i\n", settings.showRecents); fprintf(file, "tools=%i\n", settings.showTools); fprintf(file, "collections=%i\n", settings.showCollections); + fprintf(file, "collectionspromotion=%i\n", settings.showCollectionsPromotion); + fprintf(file, "collectionsentriessort=%i\n", settings.sortCollectionsEntries); + fprintf(file, "usecollectionsnestedmap=%i\n", settings.useCollectionsNestedMap); fprintf(file, "gameart=%i\n", settings.showGameArt); fprintf(file, "showfoldernamesatroot=%i\n", settings.showFolderNamesAtRoot); fprintf(file, "screentimeout=%i\n", settings.screenTimeoutSecs); @@ -1237,6 +1324,7 @@ void CFG_sync(void) fprintf(file, "wifi=%i\n", settings.wifi); fprintf(file, "defaultView=%i\n", settings.defaultView); fprintf(file, "quickSwitcherUi=%i\n", settings.showQuickSwitcherUi); + fprintf(file, "quickSwitcherUiGames=%i\n", settings.showQuickSwitcherUiGames); fprintf(file, "wifiDiagnostics=%i\n", settings.wifiDiagnostics); fprintf(file, "bluetooth=%i\n", settings.bluetooth); fprintf(file, "btDiagnostics=%i\n", settings.bluetoothDiagnostics); @@ -1282,6 +1370,9 @@ void CFG_print(void) printf("\t\"recents\": %i,\n", settings.showRecents); printf("\t\"tools\": %i,\n", settings.showTools); printf("\t\"collections\": %i,\n", settings.showCollections); + printf("\t\"collectionspromotion\": %i,\n", settings.showCollectionsPromotion); + printf("\t\"collectionsentriessort\": %i,\n", settings.sortCollectionsEntries); + printf("\t\"usecollectionsnestedmap\": %i,\n", settings.useCollectionsNestedMap); printf("\t\"gameart\": %i,\n", settings.showGameArt); printf("\t\"showfoldernamesatroot\": %i,\n", settings.showFolderNamesAtRoot); printf("\t\"screentimeout\": %i,\n", settings.screenTimeoutSecs); @@ -1298,6 +1389,7 @@ void CFG_print(void) printf("\t\"wifi\": %i,\n", settings.wifi); printf("\t\"defaultView\": %i,\n", settings.defaultView); printf("\t\"quickSwitcherUi\": %i,\n", settings.showQuickSwitcherUi); + printf("\t\"quickSwitcherUiGames\": %i,\n", settings.showQuickSwitcherUiGames); printf("\t\"wifiDiagnostics\": %i,\n", settings.wifiDiagnostics); printf("\t\"bluetooth\": %i,\n", settings.bluetooth); printf("\t\"btDiagnostics\": %i,\n", settings.bluetoothDiagnostics); diff --git a/workspace/all/common/config.h b/workspace/all/common/config.h index 56c74ffaf..7ad1874e2 100644 --- a/workspace/all/common/config.h +++ b/workspace/all/common/config.h @@ -101,10 +101,14 @@ typedef struct bool showRecents; bool showTools; bool showCollections; + bool showCollectionsPromotion; + bool sortCollectionsEntries; + bool useCollectionsNestedMap; bool showGameArt; bool showFolderNamesAtRoot; bool romsUseFolderBackground; bool showQuickSwitcherUi; + bool showQuickSwitcherUiGames; int defaultView; // Mute switch @@ -169,6 +173,9 @@ typedef struct #define CFG_DEFAULT_SHOWMENUTRANSITIONS true #define CFG_DEFAULT_SHOWRECENTS true #define CFG_DEFAULT_SHOWCOLLECTIONS true +#define CFG_DEFAULT_SHOWCOLLECTIONSPROMOTION true +#define CFG_DEFAULT_SORTCOLLECTIONSENTRIES true +#define CFG_DEFAULT_USECOLLECTIONSNESTEDMAP false #define CFG_DEFAULT_SHOWGAMEART true #define CFG_DEFAULT_SHOWFOLDERNAMESATROOT true #define CFG_DEFAULT_GAMESWITCHERSCALING GFX_SCALE_FULLSCREEN @@ -185,6 +192,7 @@ typedef struct #define CFG_DEFAULT_WIFI false #define CFG_DEFAULT_VIEW SCREEN_GAMELIST #define CFG_DEFAULT_SHOWQUICKWITCHERUI true +#define CFG_DEFAULT_SHOWQUICKWITCHERUIGAMES true #define CFG_DEFAULT_WIFI_DIAG false #define CFG_DEFAULT_SHOWTOOLS true #define CFG_DEFAULT_BLUETOOTH false @@ -264,6 +272,15 @@ void CFG_setShowTools(bool show); // Show/hide collections in the main menu. bool CFG_getShowCollections(void); void CFG_setShowCollections(bool show); +// Show/hide collections promotion in the main menu. +bool CFG_getShowCollectionsPromotion(void); +void CFG_setShowCollectionsPromotion(bool show); +// Sort collections entries for roms. +bool CFG_getSortCollectionsEntries(void); +void CFG_setSortCollectionsEntries(bool show); +// Use Nested Collections map.txt for roms. +bool CFG_getUseCollectionsNestedMap(void); +void CFG_setUseCollectionsNestedMap(bool show); // Show/hide game art in the main menu. bool CFG_getShowGameArt(void); void CFG_setShowGameArt(bool show); @@ -307,6 +324,9 @@ void CFG_setDefaultView(int view); // Quick switcher UI painting on/off bool CFG_getShowQuickswitcherUI(void); void CFG_setShowQuickswitcherUI(bool on); +// Quick switcher UI Games painting on/off +bool CFG_getShowQuickswitcherUIGames(void); +void CFG_setShowQuickswitcherUIGames(bool on); // WiFi diagnostic logging on/off bool CFG_getWifiDiagnostics(void); void CFG_setWifiDiagnostics(bool on); diff --git a/workspace/all/nextui/nextui.c b/workspace/all/nextui/nextui.c index c2d921059..28fde3490 100644 --- a/workspace/all/nextui/nextui.c +++ b/workspace/all/nextui/nextui.c @@ -242,10 +242,27 @@ static void getUniqueName(Entry* entry, char* out_name) { static void Directory_index(Directory* self) { int is_collection = prefixMatch(COLLECTIONS_PATH, self->path); int skip_index = exactMatch(FAUX_RECENT_PATH, self->path) || is_collection; // not alphabetized - + Hash* map = NULL; char map_path[256]; - sprintf(map_path, "%s/map.txt", is_collection ? COLLECTIONS_PATH : self->path); + char base_path[256]; + + strcpy(base_path, self->path); + char* tmp = strrchr(base_path, '/') + 1; + tmp[0] = '\0'; + + sprintf(map_path, "%s/map.txt", is_collection ? COLLECTIONS_PATH : self->path); // previous logic + + if (is_collection && CFG_getUseCollectionsNestedMap()){ + if(suffixMatch(".txt", self->path)){ + sprintf(map_path, "%smap.txt", base_path); + } else { + sprintf(map_path, "%s/map.txt", self->path); + } + + if (!exists(map_path)) + sprintf(map_path, "%s/map.txt", COLLECTIONS_PATH); + } if (exists(map_path)) { FILE* file = fopen(map_path, "r"); @@ -294,7 +311,7 @@ static void Directory_index(Directory* self) { Array_free(self->entries); self->entries = entries; } - if (resort) EntryArray_sort(self->entries); + if ((resort && !is_collection) || (resort && is_collection && CFG_getSortCollectionsEntries())) EntryArray_sort(self->entries); } } @@ -371,7 +388,7 @@ static Directory* Directory_new(char* path, int selected) { self->entries = getRecents(); } else if (exactMatch(path, ROMS_PATH)) { - self->entries = getRoms(); + self->entries = getRoot(); } else if (!exactMatch(path, COLLECTIONS_PATH) && prefixMatch(COLLECTIONS_PATH, path) && suffixMatch(".txt", path)) { self->entries = getCollection(path); @@ -815,7 +832,8 @@ static Array* getQuickEntries(void) { Array_push(entries, Entry_new(COLLECTIONS_PATH, ENTRY_DIR)); // Not sure we need this, its just a button press away (B) - Array_push(entries, Entry_newNamed(ROMS_PATH, ENTRY_DIR, "Games")); + if(CFG_getShowQuickswitcherUIGames()) + Array_push(entries, Entry_newNamed(ROMS_PATH, ENTRY_DIR, "Games")); // Add tools if applicable if (hasTools() && !simple_mode) { @@ -861,7 +879,7 @@ static Array* getRoot(void) { // Handle collections if (hasCollections() && CFG_getShowCollections()) { - if (entries->count) { + if (entries->count || !CFG_getShowCollectionsPromotion()) { Array_push(root, Entry_new(COLLECTIONS_PATH, ENTRY_DIR)); } else { // No visible systems, promote collections to root Array *collections = getCollections(); diff --git a/workspace/all/settings/settings.cpp b/workspace/all/settings/settings.cpp index 5717b7790..149575676 100644 --- a/workspace/all/settings/settings.cpp +++ b/workspace/all/settings/settings.cpp @@ -345,6 +345,18 @@ int main(int argc, char *argv[]) []() -> std::any { return CFG_getShowCollections(); }, [](const std::any &value) { CFG_setShowCollections(std::any_cast(value)); }, []() { CFG_setShowCollections(CFG_DEFAULT_SHOWCOLLECTIONS);}}, + new MenuItem{ListItemType::Generic, "Show Collections Promotion", "Show \"Collections\" menu entries in root game list\nOnly occurs when all Game folders are hidden.", {false, true}, on_off, + []() -> std::any { return CFG_getShowCollectionsPromotion(); }, + [](const std::any &value) { CFG_setShowCollectionsPromotion(std::any_cast(value)); }, + []() { CFG_setShowCollectionsPromotion(CFG_DEFAULT_SHOWCOLLECTIONSPROMOTION);}}, + new MenuItem{ListItemType::Generic, "Sort Collections Entries", "Sort \"Collections\" entries alphabetically.\nOtherwise uses order listed in Collection file.", {false, true}, on_off, + []() -> std::any { return CFG_getSortCollectionsEntries(); }, + [](const std::any &value) { CFG_setSortCollectionsEntries(std::any_cast(value)); }, + []() { CFG_setSortCollectionsEntries(CFG_DEFAULT_SORTCOLLECTIONSENTRIES);}}, + new MenuItem{ListItemType::Generic, "Use Collections Nested Map", "Use map.txt contained within \"Collections\" subfolders.\nFalls back to map.txt in root Collections folder if not found.", {false, true}, on_off, + []() -> std::any { return CFG_getUseCollectionsNestedMap(); }, + [](const std::any &value) { CFG_setUseCollectionsNestedMap(std::any_cast(value)); }, + []() { CFG_setUseCollectionsNestedMap(CFG_DEFAULT_USECOLLECTIONSNESTEDMAP);}}, new MenuItem{ListItemType::Generic, "Show game art", "Show game artwork in the main menu", {false, true}, on_off, []() -> std::any { return CFG_getShowGameArt(); }, [](const std::any &value) @@ -359,6 +371,10 @@ int main(int argc, char *argv[]) []() -> std::any{ return CFG_getShowQuickswitcherUI(); }, [](const std::any &value){ CFG_setShowQuickswitcherUI(std::any_cast(value)); }, []() { CFG_setShowQuickswitcherUI(CFG_DEFAULT_SHOWQUICKWITCHERUI);}}, + new MenuItem{ListItemType::Generic, "Show Quickswitcher UI Games Icon", "Show/hide Quickswitcher UI Games Icon.\nWhen hidden, the Games Icon won't display.", {false, true}, on_off, + []() -> std::any{ return CFG_getShowQuickswitcherUIGames(); }, + [](const std::any &value){ CFG_setShowQuickswitcherUIGames(std::any_cast(value)); }, + []() { CFG_setShowQuickswitcherUIGames(CFG_DEFAULT_SHOWQUICKWITCHERUIGAMES);}}, // not needed anymore // new MenuItem{ListItemType::Generic, "Game switcher scaling", "The scaling algorithm used to display the savegame image.", scaling, scaling_strings, []() -> std::any // { return CFG_getGameSwitcherScaling(); }, From c891fe48747346880ab267568363b790d6297b95 Mon Sep 17 00:00:00 2001 From: TSOLfan Date: Mon, 10 Aug 2026 13:56:00 -0400 Subject: [PATCH 2/2] feat: Collate subfolders within merged platform folders --- workspace/all/common/config.c | 22 ++++++++++++++ workspace/all/common/config.h | 5 ++++ workspace/all/nextui/nextui.c | 45 +++++++++++++++++++++++++++-- workspace/all/settings/settings.cpp | 4 +++ 4 files changed, 73 insertions(+), 3 deletions(-) diff --git a/workspace/all/common/config.c b/workspace/all/common/config.c index 91e01c659..727447de0 100644 --- a/workspace/all/common/config.c +++ b/workspace/all/common/config.c @@ -43,6 +43,7 @@ void CFG_defaults(NextUISettings *cfg) .thumbRadius = CFG_DEFAULT_THUMBRADIUS, .gameArtWidth = CFG_DEFAULT_GAMEARTWIDTH, .showFolderNamesAtRoot = CFG_DEFAULT_SHOWFOLDERNAMESATROOT, + .collateSubfolders = CFG_DEFAULT_COLLATESUBFOLDERS, .inputPromptStyle = CFG_DEFAULT_INPUT_PROMPT_STYLE, .paletteName = CFG_DEFAULT_PALETTE_NAME, .customColors = {CFG_DEFAULT_COLOR1, CFG_DEFAULT_COLOR2, CFG_DEFAULT_COLOR3, @@ -313,6 +314,11 @@ void CFG_init(FontLoad_callback_t cb, ColorSet_callback_t ccb) CFG_setShowFolderNamesAtRoot((bool)temp_value); continue; } + if (sscanf(line, "collateSubfolders=%i", &temp_value) == 1) + { + CFG_setCollateSubfolders((bool)temp_value); + continue; + } if (sscanf(line, "suspendTimeout=%i", &temp_value) == 1) { CFG_setSuspendTimeoutSecs(temp_value); @@ -722,6 +728,17 @@ void CFG_setShowFolderNamesAtRoot(bool show) CFG_sync(); } +bool CFG_getCollateSubfolders(void) +{ + return settings.collateSubfolders; +} + +void CFG_setCollateSubfolders(bool show) +{ + settings.collateSubfolders = show; + CFG_sync(); +} + uint32_t CFG_getScreenTimeoutSecs(void) { return settings.screenTimeoutSecs; @@ -1480,6 +1497,10 @@ void CFG_get(const char *key, char *value) { sprintf(value, "%i", CFG_getShowFolderNamesAtRoot()); } + else if (strcmp(key, "collateSubfolders") == 0) + { + sprintf(value, "%i", CFG_getCollateSubfolders()); + } else if (strcmp(key, "screentimeout") == 0) { sprintf(value, "%i", CFG_getScreenTimeoutSecs()); @@ -1709,6 +1730,7 @@ void CFG_sync(void) fprintf(file, "usecollectionsnestedmap=%i\n", settings.useCollectionsNestedMap); fprintf(file, "gameart=%i\n", settings.showGameArt); fprintf(file, "showfoldernamesatroot=%i\n", settings.showFolderNamesAtRoot); + fprintf(file, "collateSubfolders=%i\n", settings.collateSubfolders); fprintf(file, "screentimeout=%i\n", settings.screenTimeoutSecs); fprintf(file, "suspendTimeout=%i\n", settings.suspendTimeoutSecs); fprintf(file, "powerOffProtection=%i\n", settings.powerOffProtection); diff --git a/workspace/all/common/config.h b/workspace/all/common/config.h index 1a23cd526..4577f3318 100644 --- a/workspace/all/common/config.h +++ b/workspace/all/common/config.h @@ -140,6 +140,7 @@ typedef struct bool useCollectionsNestedMap; bool showGameArt; bool showFolderNamesAtRoot; + bool collateSubfolders; bool romsUseFolderBackground; bool showQuickSwitcherUi; bool showQuickSwitcherUiGames; @@ -232,6 +233,7 @@ typedef struct #define CFG_DEFAULT_USECOLLECTIONSNESTEDMAP false #define CFG_DEFAULT_SHOWGAMEART true #define CFG_DEFAULT_SHOWFOLDERNAMESATROOT true +#define CFG_DEFAULT_COLLATESUBFOLDERS false #define CFG_DEFAULT_GAMESWITCHERSCALING GFX_SCALE_FULLSCREEN #define CFG_DEFAULT_SCREENTIMEOUTSECS 60 #define CFG_DEFAULT_SUSPENDTIMEOUTSECS 30 @@ -416,6 +418,9 @@ void CFG_setGameArtWidth(double zeroToOne); // Show/hide folder names at root directory. bool CFG_getShowFolderNamesAtRoot(void); void CFG_setShowFolderNamesAtRoot(bool show); +// Collate subfolders within merged platform folders. +bool CFG_getCollateSubfolders(void); +void CFG_setCollateSubfolders(bool show); // WiFi on/off (if available) bool CFG_getWifi(void); void CFG_setWifi(bool on); diff --git a/workspace/all/nextui/nextui.c b/workspace/all/nextui/nextui.c index d8cd69e42..3f110ff41 100644 --- a/workspace/all/nextui/nextui.c +++ b/workspace/all/nextui/nextui.c @@ -1067,9 +1067,24 @@ static int isConsoleDir(char* path) { static Array* getEntries(char* path){ Array* entries = Array_new(); - if (isConsoleDir(path)) { // top-level console folder, might collate + char base_path[256]; + strcpy(base_path, path); + char* sub_path = ""; + + // Separate the top-level console folder from any subfolders if Collate Subfolders is active + if (CFG_getCollateSubfolders() && prefixMatch(ROMS_PATH, path)) { + char* slash = strchr(path + strlen(ROMS_PATH) + 1, '/'); + if (slash) { + base_path[slash - path] = '\0'; // Sample base_path result "Roms/Handheld (GB)" + sub_path = slash; // Sample sub_path result "/Nintendo" + } + } + + int collated = 0; + if (isConsoleDir(base_path)) { // top-level console folder, might collate + collated = 1; char collated_path[256]; - strcpy(collated_path, path); + strcpy(collated_path, base_path); char* tmp = strrchr(collated_path, '('); // 1 because we want to keep the opening parenthesis to avoid collating "Game Boy Color" and "Game Boy Advance" into "Game Boy" // but conditional so we can continue to support a bare tag name as a folder name @@ -1088,7 +1103,11 @@ static Array* getEntries(char* path){ strcpy(tmp, dp->d_name); if (!prefixMatch(collated_path, full_path)) continue; - addEntries(entries, full_path); + + // Re-attach sub_path to search identical subfolders across all collated directories + char target_path[512]; + sprintf(target_path, "%s%s", full_path, sub_path); + addEntries(entries, target_path); } closedir(dh); } @@ -1096,6 +1115,26 @@ static Array* getEntries(char* path){ else addEntries(entries, path); // just a subfolder EntryArray_sort(entries); + + // Deduplicate only if Collate Subfolders is active and any Subfolders were actually collated + if (collated && CFG_getCollateSubfolders()) { + int unique_count = 0; + for (int i = 0; i < entries->count; i++) { + Entry* entry = entries->items[i]; + + if (unique_count > 0 && + entry->type == ENTRY_DIR && + ((Entry*)entries->items[unique_count - 1])->type == ENTRY_DIR && + exactMatch(((Entry*)entries->items[unique_count - 1])->name, entry->name)) { + + Entry_free(entry); // Remove the duplicate subfolder entry + } else { + entries->items[unique_count++] = entry; // Keep a unique entry + } + } + entries->count = unique_count; + } + return entries; } diff --git a/workspace/all/settings/settings.cpp b/workspace/all/settings/settings.cpp index 27f1db477..6207602b0 100644 --- a/workspace/all/settings/settings.cpp +++ b/workspace/all/settings/settings.cpp @@ -457,6 +457,10 @@ int main(int argc, char *argv[]) []() -> std::any { return CFG_getShowFolderNamesAtRoot(); }, [](const std::any &value) { CFG_setShowFolderNamesAtRoot(std::any_cast(value)); }, []() { CFG_setShowFolderNamesAtRoot(CFG_DEFAULT_SHOWFOLDERNAMESATROOT);}}); + appearanceItems.push_back(new MenuItem{ListItemType::Generic, "Collate Subfolders", "Merge duplicate subfolders in already merged platform folders.", {false, true}, on_off, + []() -> std::any { return CFG_getCollateSubfolders(); }, + [](const std::any &value) { CFG_setCollateSubfolders(std::any_cast(value)); }, + []() { CFG_setCollateSubfolders(CFG_DEFAULT_COLLATESUBFOLDERS);}}); appearanceItems.push_back(new MenuItem{ListItemType::Generic, "Show Recents", "Show \"Recently Played\" menu entry in game list.", {false, true}, on_off, []() -> std::any { return CFG_getShowRecents(); }, [](const std::any &value) { CFG_setShowRecents(std::any_cast(value)); },