diff --git a/workspace/all/common/config.c b/workspace/all/common/config.c index e1ec3ba7b..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, @@ -57,6 +58,9 @@ 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, @@ -280,6 +284,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); @@ -295,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); @@ -386,6 +410,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); @@ -699,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; @@ -842,6 +882,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; @@ -992,6 +1065,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; @@ -1392,6 +1476,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) { @@ -1401,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()); @@ -1469,6 +1569,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())); @@ -1621,8 +1725,12 @@ 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, "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); @@ -1641,6 +1749,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); @@ -1695,6 +1804,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); @@ -1715,6 +1827,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 cdd577866..4577f3318 100644 --- a/workspace/all/common/config.h +++ b/workspace/all/common/config.h @@ -135,10 +135,15 @@ typedef struct bool showRecents; bool showTools; bool showCollections; + bool showCollectionsPromotion; + bool sortCollectionsEntries; + bool useCollectionsNestedMap; bool showGameArt; bool showFolderNamesAtRoot; + bool collateSubfolders; bool romsUseFolderBackground; bool showQuickSwitcherUi; + bool showQuickSwitcherUiGames; int defaultView; // Mute switch @@ -223,8 +228,12 @@ typedef struct #define CFG_DEFAULT_SHOWMENUTRANSITIONS TRANSITION_SNAPPY #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_COLLATESUBFOLDERS false #define CFG_DEFAULT_GAMESWITCHERSCALING GFX_SCALE_FULLSCREEN #define CFG_DEFAULT_SCREENTIMEOUTSECS 60 #define CFG_DEFAULT_SUSPENDTIMEOUTSECS 30 @@ -241,6 +250,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 @@ -356,6 +366,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); @@ -399,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); @@ -408,6 +430,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 5ed6d612e..3f110ff41 100644 --- a/workspace/all/nextui/nextui.c +++ b/workspace/all/nextui/nextui.c @@ -245,7 +245,24 @@ static void Directory_index(Directory* self) { 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(); @@ -1049,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 @@ -1070,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); } @@ -1078,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 7a3eeda49..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)); }, @@ -465,6 +469,22 @@ int main(int argc, char *argv[]) []() -> std::any { return CFG_getShowTools(); }, [](const std::any &value) { CFG_setShowTools(std::any_cast(value)); }, []() { CFG_setShowTools(CFG_DEFAULT_SHOWTOOLS);}}); + appearanceItems.push_back(new MenuItem{ListItemType::Generic, "Show Collections", "Show \"Collections\" menu entry in game list.", {false, true}, on_off, + []() -> std::any { return CFG_getShowCollections(); }, + [](const std::any &value) { CFG_setShowCollections(std::any_cast(value)); }, + []() { CFG_setShowCollections(CFG_DEFAULT_SHOWCOLLECTIONS);}}); + appearanceItems.push_back(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);}}); + appearanceItems.push_back(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);}}); + appearanceItems.push_back(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);}}); appearanceItems.push_back(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) { CFG_setShowGameArt(std::any_cast(value)); },