mirror of
https://github.com/libretro/RetroArch.git
synced 2024-11-27 02:00:41 +00:00
29f011acef
* explore_load_icons - fill_pathname_slash is equivalent to usage of strlen here, no need to call strlen
1886 lines
65 KiB
C
1886 lines
65 KiB
C
/* RetroArch - A frontend for libretro.
|
|
* Copyright (C) 2011-2020 - Daniel De Matteis
|
|
* Copyright (C) 2020 - Psyraven
|
|
*
|
|
* RetroArch is free software: you can redistribute it and/or modify it under the terms
|
|
* of the GNU General Public License as published by the Free Software Found-
|
|
* ation, either version 3 of the License, or (at your option) any later version.
|
|
*
|
|
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
|
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
|
* PURPOSE. See the GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with RetroArch.
|
|
* If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <stddef.h>
|
|
|
|
#include <compat/strcasestr.h>
|
|
#include <compat/strl.h>
|
|
#include <array/rbuf.h>
|
|
#include <array/rhmap.h>
|
|
#include <formats/rjson.h>
|
|
#include <formats/rjson_helpers.h>
|
|
#include <retro_endianness.h>
|
|
|
|
#include "menu_driver.h"
|
|
#include "menu_cbs.h"
|
|
#include "../retroarch.h"
|
|
#include "../configuration.h"
|
|
#include "../file_path_special.h"
|
|
#include "../playlist.h"
|
|
#include "../verbosity.h"
|
|
#include "../libretro-db/libretrodb.h"
|
|
#include "../tasks/tasks_internal.h"
|
|
|
|
/* Explore */
|
|
enum
|
|
{
|
|
EXPLORE_BY_DEVELOPER = 0,
|
|
EXPLORE_BY_PUBLISHER,
|
|
EXPLORE_BY_RELEASEYEAR,
|
|
EXPLORE_BY_PLAYERCOUNT,
|
|
EXPLORE_BY_GENRE,
|
|
EXPLORE_BY_ACHIEVEMENTS,
|
|
EXPLORE_BY_CATEGORY,
|
|
EXPLORE_BY_LANGUAGE,
|
|
EXPLORE_BY_CONSOLE_EXCLUSIVE,
|
|
EXPLORE_BY_PLATFORM_EXCLUSIVE,
|
|
EXPLORE_BY_RUMBLE,
|
|
EXPLORE_BY_SCORE,
|
|
EXPLORE_BY_MEDIA,
|
|
EXPLORE_BY_CONTROLS,
|
|
EXPLORE_BY_ARTSTYLE,
|
|
EXPLORE_BY_GAMEPLAY,
|
|
EXPLORE_BY_NARRATIVE,
|
|
EXPLORE_BY_PACING,
|
|
EXPLORE_BY_PERSPECTIVE,
|
|
EXPLORE_BY_SETTING,
|
|
EXPLORE_BY_VISUAL,
|
|
EXPLORE_BY_VEHICULAR,
|
|
EXPLORE_BY_ORIGIN,
|
|
EXPLORE_BY_REGION,
|
|
EXPLORE_BY_FRANCHISE,
|
|
EXPLORE_BY_TAGS,
|
|
EXPLORE_BY_SYSTEM,
|
|
EXPLORE_CAT_COUNT,
|
|
|
|
EXPLORE_OP_EQUAL = 0,
|
|
EXPLORE_OP_MIN,
|
|
EXPLORE_OP_MAX,
|
|
EXPLORE_OP_RANGE,
|
|
|
|
EXPLORE_ICONS_OFF = 0,
|
|
EXPLORE_ICONS_CONTENT = 1,
|
|
EXPLORE_ICONS_SYSTEM_CATEGORY = 2,
|
|
|
|
EXPLORE_TYPE_ADDITIONALFILTER = FILE_TYPE_RDB, /* database icon */
|
|
EXPLORE_TYPE_VIEW = FILE_TYPE_PLAIN, /* file icon */
|
|
EXPLORE_TYPE_FILTERNULL = MENU_SETTINGS_LAST,
|
|
EXPLORE_TYPE_SEARCH,
|
|
EXPLORE_TYPE_SHOWALL,
|
|
EXPLORE_TYPE_FIRSTCATEGORY,
|
|
EXPLORE_TYPE_FIRSTITEM = EXPLORE_TYPE_FIRSTCATEGORY + EXPLORE_CAT_COUNT
|
|
};
|
|
|
|
/* Arena allocator */
|
|
typedef struct ex_arena
|
|
{
|
|
char *ptr;
|
|
char *end;
|
|
char **blocks;
|
|
} ex_arena;
|
|
|
|
typedef struct
|
|
{
|
|
uint32_t idx;
|
|
char str[1];
|
|
} explore_string_t;
|
|
|
|
typedef struct
|
|
{
|
|
const struct playlist_entry* playlist_entry;
|
|
explore_string_t *by[EXPLORE_CAT_COUNT];
|
|
explore_string_t **split;
|
|
#ifdef EXPLORE_SHOW_ORIGINAL_TITLE
|
|
char* original_title;
|
|
#endif
|
|
} explore_entry_t;
|
|
|
|
struct explore_state
|
|
{
|
|
ex_arena arena;
|
|
explore_string_t **by[EXPLORE_CAT_COUNT];
|
|
explore_entry_t *entries;
|
|
playlist_t **playlists;
|
|
uintptr_t *icons;
|
|
const char *label_explore_item_str;
|
|
|
|
char title[1024];
|
|
bool has_unknown[EXPLORE_CAT_COUNT];
|
|
unsigned show_icons;
|
|
|
|
unsigned view_levels;
|
|
char view_search[1024];
|
|
uint8_t view_op[EXPLORE_CAT_COUNT];
|
|
bool view_use_split[EXPLORE_CAT_COUNT];
|
|
unsigned view_cats[EXPLORE_CAT_COUNT];
|
|
explore_string_t* view_match[EXPLORE_CAT_COUNT];
|
|
uint32_t view_idx_min[EXPLORE_CAT_COUNT];
|
|
uint32_t view_idx_max[EXPLORE_CAT_COUNT];
|
|
};
|
|
|
|
static const struct
|
|
{
|
|
const char* rdbkey;
|
|
enum msg_hash_enums name_enum, by_enum;
|
|
bool use_split, is_company, is_numeric, is_boolean;
|
|
}
|
|
explore_by_info[EXPLORE_CAT_COUNT] =
|
|
{
|
|
{ "developer", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_DEVELOPER, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_DEVELOPER, true, true, false, false },
|
|
{ "publisher", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_PUBLISHER, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_PUBLISHER, true, true, false, false },
|
|
{ "releaseyear", MENU_ENUM_LABEL_VALUE_EXPLORE_CATEGORY_RELEASE_YEAR, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_RELEASE_YEAR, false, false, true, false },
|
|
{ "users", MENU_ENUM_LABEL_VALUE_EXPLORE_CATEGORY_PLAYER_COUNT, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_PLAYER_COUNT, false, false, true, false },
|
|
{ "genre", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_GENRE, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_GENRE, true, false, false, false },
|
|
{ "achievements", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_ACHIEVEMENTS, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_ACHIEVEMENTS, false, false, false, true },
|
|
{ "category", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_CATEGORY, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_CATEGORY, true, false, false, false },
|
|
{ "language", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_LANGUAGE, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_LANGUAGE, true, false, false, false },
|
|
{ "console_exclusive", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_CONSOLE_EXCLUSIVE, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_CONSOLE_EXCLUSIVE, false, false, false, true },
|
|
{ "platform_exclusive", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_PLATFORM_EXCLUSIVE, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_PLATFORM_EXCLUSIVE, false, false, false, true },
|
|
{ "rumble", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_RUMBLE, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_RUMBLE, false, false, false, true },
|
|
{ "score", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_SCORE, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_SCORE, true, false, false, false },
|
|
{ "media", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_MEDIA, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_MEDIA, true, false, false, false },
|
|
{ "controls", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_CONTROLS, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_CONTROLS, true, false, false, false },
|
|
{ "artstyle", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_ARTSTYLE, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_ARTSTYLE, true, false, false, false },
|
|
{ "gameplay", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_GAMEPLAY, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_GAMEPLAY, true, false, false, false },
|
|
{ "narrative", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_NARRATIVE, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_NARRATIVE, true, false, false, false },
|
|
{ "pacing", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_PACING, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_PACING, true, false, false, false },
|
|
{ "perspective", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_PERSPECTIVE, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_PERSPECTIVE, true, false, false, false },
|
|
{ "setting", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_SETTING, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_SETTING, true, false, false, false },
|
|
{ "visual", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_VISUAL, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_VISUAL, true, false, false, false },
|
|
{ "vehicular", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_VEHICULAR, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_VEHICULAR, true, false, false, false },
|
|
{ "origin", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_ORIGIN, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_ORIGIN, false, false, false, false },
|
|
{ "region", MENU_ENUM_LABEL_VALUE_EXPLORE_CATEGORY_REGION, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_REGION, false, false, false, false },
|
|
{ "franchise", MENU_ENUM_LABEL_VALUE_RDB_ENTRY_FRANCHISE, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_FRANCHISE, false, false, false, false },
|
|
{ "tags", MENU_ENUM_LABEL_VALUE_EXPLORE_CATEGORY_TAG, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_TAG, true, false, false, false },
|
|
{ "system", MENU_ENUM_LABEL_VALUE_CORE_INFO_SYSTEM_NAME, MENU_ENUM_LABEL_VALUE_EXPLORE_BY_SYSTEM_NAME, false, false, false, false },
|
|
};
|
|
|
|
/* TODO/FIXME - static global */
|
|
static explore_state_t* explore_state;
|
|
|
|
#if defined(_MSC_VER)
|
|
#define EX_ALIGNOF(type) ((int)__alignof(type))
|
|
#else
|
|
#define EX_ALIGNOF(type) ((int)__alignof__(type))
|
|
#endif
|
|
|
|
#define EX_ARENA_ALIGNMENT 8
|
|
#define EX_ARENA_BLOCK_SIZE (64 * 1024)
|
|
#define EX_ARENA_ALIGN_UP(n, a) (((n) + (a) - 1) & ~((a) - 1))
|
|
|
|
static void ex_arena_grow(ex_arena *arena, size_t min_size)
|
|
{
|
|
size_t size = EX_ARENA_ALIGN_UP(
|
|
MAX(min_size, EX_ARENA_BLOCK_SIZE), EX_ARENA_ALIGNMENT);
|
|
arena->ptr = (char *)malloc(size);
|
|
arena->end = arena->ptr + size;
|
|
RBUF_PUSH(arena->blocks, arena->ptr);
|
|
}
|
|
|
|
static void *ex_arena_alloc(ex_arena *arena, size_t size)
|
|
{
|
|
void *ptr = NULL;
|
|
|
|
if (size > (size_t)(arena->end - arena->ptr))
|
|
ex_arena_grow(arena, size);
|
|
|
|
ptr = arena->ptr;
|
|
arena->ptr = (char *)
|
|
EX_ARENA_ALIGN_UP((uintptr_t)(arena->ptr + size), EX_ARENA_ALIGNMENT);
|
|
return ptr;
|
|
}
|
|
|
|
static void ex_arena_free(ex_arena *arena)
|
|
{
|
|
char **it;
|
|
|
|
for (it = arena->blocks; it != RBUF_END(arena->blocks); it++)
|
|
free(*it);
|
|
|
|
RBUF_FREE(arena->blocks);
|
|
arena->ptr = NULL;
|
|
arena->end = NULL;
|
|
arena->blocks = NULL;
|
|
}
|
|
|
|
/* Hash function */
|
|
static uint32_t ex_hash32_nocase_filtered(
|
|
const unsigned char* str, size_t len,
|
|
unsigned char f_first, unsigned char f_last)
|
|
{
|
|
const unsigned char *end = NULL;
|
|
uint32_t hash = (uint32_t)0x811c9dc5;
|
|
for (end = str + len; str != end;)
|
|
{
|
|
unsigned char c = *(str++);
|
|
if (c >= f_first && c <= f_last)
|
|
hash = ((hash * (uint32_t)0x01000193)
|
|
^ (uint32_t)((c >= 'A' && c <= 'Z')
|
|
? (c | 0x20) : c));
|
|
}
|
|
if (hash)
|
|
return hash;
|
|
return 1;
|
|
}
|
|
|
|
static int explore_qsort_func_nums(const void *a_, const void *b_)
|
|
{
|
|
const char *a = (*(const explore_string_t**)a_)->str;
|
|
const char *b = (*(const explore_string_t**)b_)->str;
|
|
return atoi(a) - atoi(b);
|
|
}
|
|
|
|
static int explore_qsort_func_strings(const void *a_, const void *b_)
|
|
{
|
|
const char *a = (*(const explore_string_t**)a_)->str;
|
|
const char *b = (*(const explore_string_t**)b_)->str;
|
|
int a0 = TOLOWER(a[0]), b0 = TOLOWER(b[0]);
|
|
return (a0 != b0 ? (a0 - b0) : strcasecmp(a, b));
|
|
}
|
|
|
|
static int explore_qsort_func_entries(const void *a_, const void *b_)
|
|
{
|
|
const char *a = ((const explore_entry_t*)a_)->playlist_entry->label;
|
|
const char *b = ((const explore_entry_t*)b_)->playlist_entry->label;
|
|
int a0 = TOLOWER(a[0]), b0 = TOLOWER(b[0]);
|
|
if (a0 != b0)
|
|
return a0 - b0;
|
|
return strcasecmp(a, b);
|
|
}
|
|
|
|
static int explore_qsort_func_menulist(const void *a_, const void *b_)
|
|
{
|
|
const struct item_file *a = (const struct item_file*)a_;
|
|
const struct item_file *b = (const struct item_file*)b_;
|
|
if (a->path[0] != b->path[0])
|
|
return (unsigned char)a->path[0] - (unsigned char)b->path[0];
|
|
return strcasecmp(a->path, b->path);
|
|
}
|
|
|
|
static int explore_check_company_suffix(const char* p, bool search_reverse)
|
|
{
|
|
int p0, p0_lc, p1, p1_lc, p2, p2_lc;
|
|
if (search_reverse)
|
|
{
|
|
p -= (p[-1] == '.' ? 4 : 3);
|
|
if (p[-1] != ' ')
|
|
return 0;
|
|
}
|
|
if (p[0] == '\0' || p[1] == '\0' || p[2] == '\0')
|
|
return 0;
|
|
p0 = p[0];
|
|
p1 = p[1];
|
|
p2 = p[2];
|
|
p0_lc = TOLOWER(p0);
|
|
p1_lc = TOLOWER(p1);
|
|
p2_lc = TOLOWER(p2);
|
|
if ( (p0_lc == 'i' && p1_lc == 'n' && p2_lc == 'c') /*, Inc */
|
|
|| (p0_lc == 'l' && p1_lc == 't' && p2_lc == 'd') /*, Ltd */
|
|
|| (p0_lc == 't' && p1_lc == 'h' && p2_lc == 'e') /*, The */
|
|
)
|
|
return (p[3] == '.' ? 4 : 3);
|
|
return 0;
|
|
}
|
|
|
|
static void explore_add_unique_string(
|
|
explore_state_t *state,
|
|
explore_string_t** maps[EXPLORE_CAT_COUNT], explore_entry_t *e,
|
|
unsigned cat, const char *str,
|
|
explore_string_t ***split_buf)
|
|
{
|
|
bool is_company;
|
|
const char *p;
|
|
const char *p_next;
|
|
if (!str || !*str)
|
|
{
|
|
state->has_unknown[cat] = true;
|
|
return;
|
|
}
|
|
|
|
if (!explore_by_info[cat].use_split)
|
|
split_buf = NULL;
|
|
is_company = explore_by_info[cat].is_company;
|
|
|
|
for (p = str + 1;; p++)
|
|
{
|
|
size_t len = 0;
|
|
uint32_t hash = 0;
|
|
explore_string_t* entry = NULL;
|
|
|
|
if (*p != '/' && *p != ',' && *p != '|' && *p != '\0')
|
|
continue;
|
|
|
|
if (!split_buf && *p != '\0')
|
|
continue;
|
|
|
|
p_next = p;
|
|
while (*str == ' ')
|
|
str++;
|
|
while (p[-1] == ' ')
|
|
p--;
|
|
|
|
if (p == str)
|
|
{
|
|
if (*p == '\0')
|
|
return;
|
|
continue;
|
|
}
|
|
|
|
if (is_company && p - str > 5)
|
|
{
|
|
p -= explore_check_company_suffix(p, true);
|
|
while (p[-1] == ' ')
|
|
p--;
|
|
}
|
|
|
|
len = p - str;
|
|
hash = ex_hash32_nocase_filtered(
|
|
(unsigned char*)str, len, '0', 255);
|
|
entry = RHMAP_GET(maps[cat], hash);
|
|
|
|
if (!entry)
|
|
{
|
|
entry = (explore_string_t*)
|
|
ex_arena_alloc(&state->arena,
|
|
sizeof(explore_string_t) + len);
|
|
memcpy(entry->str, str, len);
|
|
entry->str[len] = '\0';
|
|
RBUF_PUSH(state->by[cat], entry);
|
|
RHMAP_SET(maps[cat], hash, entry);
|
|
}
|
|
|
|
if (!e->by[cat])
|
|
e->by[cat] = entry;
|
|
else
|
|
RBUF_PUSH(*split_buf, entry);
|
|
|
|
if (*p_next == '\0')
|
|
return;
|
|
if (is_company && *p_next == ',')
|
|
{
|
|
p = p_next + 1;
|
|
while (*p == ' ')
|
|
p++;
|
|
p += explore_check_company_suffix(p, false);
|
|
while (*p == ' ')
|
|
p++;
|
|
if (*p == '\0')
|
|
return;
|
|
if (*p == '/' || *p == ',' || *p == '|')
|
|
p_next = p;
|
|
}
|
|
p = p_next;
|
|
str = p + 1;
|
|
}
|
|
}
|
|
|
|
static void explore_unload_icons(explore_state_t *state)
|
|
{
|
|
unsigned i;
|
|
if (!state)
|
|
return;
|
|
for (i = 0; i != RBUF_LEN(state->icons); i++)
|
|
if (state->icons[i])
|
|
video_driver_texture_unload(&state->icons[i]);
|
|
}
|
|
|
|
static void explore_load_icons(explore_state_t *state)
|
|
{
|
|
char path[PATH_MAX_LENGTH];
|
|
size_t i, pathlen, system_count;
|
|
if (!state)
|
|
return;
|
|
|
|
if ((system_count = RBUF_LEN(state->by[EXPLORE_BY_SYSTEM])) <= 0)
|
|
return;
|
|
|
|
/* unload any icons that could exist from a previous call to this */
|
|
explore_unload_icons(state);
|
|
|
|
/* RBUF_RESIZE leaves memory uninitialised,
|
|
have to zero it 'manually' */
|
|
RBUF_RESIZE(state->icons, system_count);
|
|
memset(state->icons, 0, RBUF_SIZEOF(state->icons));
|
|
|
|
fill_pathname_application_special(path, sizeof(path),
|
|
APPLICATION_SPECIAL_DIRECTORY_ASSETS_SYSICONS);
|
|
if (string_is_empty(path))
|
|
return;
|
|
|
|
pathlen = fill_pathname_slash(path, sizeof(path));
|
|
|
|
for (i = 0; i != system_count; i++)
|
|
{
|
|
struct texture_image ti;
|
|
size_t _len = pathlen;
|
|
_len += strlcpy(path + pathlen,
|
|
state->by[EXPLORE_BY_SYSTEM][i]->str,
|
|
sizeof(path) - pathlen);
|
|
strlcpy(path + _len, ".png", sizeof(path) - _len);
|
|
if (!path_is_valid(path))
|
|
continue;
|
|
|
|
ti.width = 0;
|
|
ti.height = 0;
|
|
ti.pixels = NULL;
|
|
ti.supports_rgba = video_driver_supports_rgba();
|
|
|
|
if (!image_texture_load(&ti, path))
|
|
continue;
|
|
|
|
if (ti.pixels)
|
|
video_driver_texture_load(&ti,
|
|
TEXTURE_FILTER_MIPMAP_LINEAR, &state->icons[i]);
|
|
|
|
image_texture_free(&ti);
|
|
}
|
|
}
|
|
|
|
explore_state_t *menu_explore_build_list(const char *directory_playlist,
|
|
const char *directory_database)
|
|
{
|
|
unsigned i;
|
|
char tmp[PATH_MAX_LENGTH];
|
|
struct explore_source
|
|
{
|
|
const struct playlist_entry *source;
|
|
uint32_t entry_index, meta_count;
|
|
};
|
|
struct explore_rdb
|
|
{
|
|
libretrodb_t *handle;
|
|
struct explore_source *playlist_crcs;
|
|
struct explore_source *playlist_names;
|
|
size_t count;
|
|
char systemname[NAME_MAX_LENGTH];
|
|
}
|
|
*rdbs = NULL;
|
|
int *rdb_indices = NULL;
|
|
explore_string_t **cat_maps[EXPLORE_CAT_COUNT] = {NULL};
|
|
explore_string_t **split_buf = NULL;
|
|
libretro_vfs_implementation_dir *dir = NULL;
|
|
|
|
explore_state_t *state = (explore_state_t*)calloc(1, sizeof(*state));
|
|
|
|
if (!state)
|
|
return NULL;
|
|
|
|
state->label_explore_item_str =
|
|
msg_hash_to_str(MENU_ENUM_LABEL_EXPLORE_ITEM);
|
|
|
|
/* Index all playlists */
|
|
for (dir = retro_vfs_opendir_impl(directory_playlist, false); dir;)
|
|
{
|
|
playlist_config_t playlist_config;
|
|
size_t j, used_entries = 0;
|
|
playlist_t *playlist = NULL;
|
|
const char *fext = NULL;
|
|
const char *fname = NULL;
|
|
uint32_t fhash = 0;
|
|
|
|
playlist_config.path[0] = '\0';
|
|
playlist_config.base_content_directory[0] = '\0';
|
|
playlist_config.capacity = 0;
|
|
playlist_config.old_format = false;
|
|
playlist_config.compress = false;
|
|
playlist_config.fuzzy_archive_match = false;
|
|
playlist_config.autofix_paths = false;
|
|
|
|
if (!retro_vfs_readdir_impl(dir))
|
|
{
|
|
retro_vfs_closedir_impl(dir);
|
|
break;
|
|
}
|
|
|
|
fname = retro_vfs_dirent_get_name_impl(dir);
|
|
if (fname)
|
|
fext = strrchr(fname, '.');
|
|
|
|
if (!fext || strcasecmp(fext, ".lpl"))
|
|
continue;
|
|
|
|
fill_pathname_join_special(playlist_config.path,
|
|
directory_playlist, fname, sizeof(playlist_config.path));
|
|
playlist_config.capacity = COLLECTION_SIZE;
|
|
playlist = playlist_init(&playlist_config);
|
|
|
|
fhash = ex_hash32_nocase_filtered(
|
|
(unsigned char*)fname, fext - fname, '0', 255);
|
|
|
|
for (j = 0; j < playlist_size(playlist); j++)
|
|
{
|
|
int rdb_num;
|
|
uint32_t entry_crc32;
|
|
struct explore_source src = { NULL, (uint32_t)-1, 0 };
|
|
struct explore_rdb* rdb = NULL;
|
|
const struct playlist_entry *entry = NULL;
|
|
const char *db_name = fname;
|
|
const char *db_ext = fext;
|
|
uint32_t rdb_hash = fhash;
|
|
playlist_get_index(playlist, j, &entry);
|
|
|
|
/* We also could build label from file name, for now it's required */
|
|
if (!entry->label || !*entry->label)
|
|
continue;
|
|
|
|
/* For auto scanned playlists the entry db_name matches the
|
|
* lpl file name and we can just use that */
|
|
if (entry->db_name && *entry->db_name
|
|
&& strcasecmp(entry->db_name, fname))
|
|
{
|
|
db_name = entry->db_name;
|
|
db_ext = strrchr(db_name, '.');
|
|
if (!db_ext)
|
|
db_ext = db_name + strlen(db_name);
|
|
rdb_hash = ex_hash32_nocase_filtered(
|
|
(unsigned char*)db_name, db_ext - db_name, '0', 255);
|
|
}
|
|
|
|
rdb_num = RHMAP_GET(rdb_indices, rdb_hash);
|
|
if (!rdb_num)
|
|
{
|
|
size_t systemname_len;
|
|
struct explore_rdb newrdb;
|
|
char *ext_path = NULL;
|
|
|
|
newrdb.handle = libretrodb_new();
|
|
newrdb.count = 0;
|
|
newrdb.playlist_crcs = NULL;
|
|
newrdb.playlist_names = NULL;
|
|
|
|
systemname_len = db_ext - db_name;
|
|
if (systemname_len >= sizeof(newrdb.systemname))
|
|
systemname_len = sizeof(newrdb.systemname)-1;
|
|
memcpy(newrdb.systemname, db_name, systemname_len);
|
|
newrdb.systemname[systemname_len] = '\0';
|
|
|
|
fill_pathname_join_special(
|
|
tmp, directory_database, db_name, sizeof(tmp));
|
|
|
|
/* Replace the extension - change 'lpl' to 'rdb' */
|
|
if (( ext_path = path_get_extension_mutable(tmp))
|
|
&& ext_path[0] == '.'
|
|
&& ext_path[1] == 'l'
|
|
&& ext_path[2] == 'p'
|
|
&& ext_path[3] == 'l')
|
|
{
|
|
ext_path[1] = 'r';
|
|
ext_path[2] = 'd';
|
|
ext_path[3] = 'b';
|
|
}
|
|
|
|
if (libretrodb_open(tmp, newrdb.handle, false) != 0)
|
|
{
|
|
/* Invalid RDB file */
|
|
libretrodb_free(newrdb.handle);
|
|
RHMAP_SET(rdb_indices, rdb_hash, -1);
|
|
continue;
|
|
}
|
|
|
|
RBUF_PUSH(rdbs, newrdb);
|
|
rdb_num = (int)RBUF_LEN(rdbs);
|
|
RHMAP_SET(rdb_indices, rdb_hash, rdb_num);
|
|
}
|
|
|
|
if ((uintptr_t)rdb_num == (uintptr_t)-1)
|
|
continue;
|
|
|
|
rdb = &rdbs[rdb_num - 1];
|
|
rdb->count++;
|
|
entry_crc32 = (uint32_t)strtoul(
|
|
(entry->crc32 ? entry->crc32 : ""), NULL, 16);
|
|
src.source = entry;
|
|
if (entry_crc32)
|
|
{
|
|
RHMAP_SET(rdb->playlist_crcs, entry_crc32, src);
|
|
}
|
|
else
|
|
{
|
|
RHMAP_SET_STR(rdb->playlist_names, entry->label, src);
|
|
}
|
|
used_entries++;
|
|
}
|
|
|
|
if (used_entries)
|
|
RBUF_PUSH(state->playlists, playlist);
|
|
else
|
|
playlist_free(playlist);
|
|
}
|
|
|
|
/* Loop through all RDBs referenced in the playlists
|
|
* and load meta data strings */
|
|
for (i = 0; i != RBUF_LEN(rdbs); i++)
|
|
{
|
|
struct rmsgpack_dom_value item;
|
|
struct explore_rdb* rdb = &rdbs[i];
|
|
libretrodb_cursor_t *cur = libretrodb_cursor_new();
|
|
bool more =
|
|
(
|
|
libretrodb_cursor_open(rdb->handle, cur, NULL) == 0
|
|
&& libretrodb_cursor_read_item(cur, &item) == 0);
|
|
|
|
for (; more; more = (rmsgpack_dom_value_free(&item),
|
|
libretrodb_cursor_read_item(cur, &item) == 0))
|
|
{
|
|
unsigned k, l, cat;
|
|
explore_entry_t* e;
|
|
const char *fields[EXPLORE_CAT_COUNT];
|
|
char numeric_buf[EXPLORE_CAT_COUNT][16];
|
|
uint32_t crc32 = 0;
|
|
uint32_t meta_count = 0;
|
|
char *name = NULL;
|
|
#ifdef EXPLORE_SHOW_ORIGINAL_TITLE
|
|
char *original_title = NULL;
|
|
#endif
|
|
struct explore_source* src = NULL;
|
|
|
|
if (item.type != RDT_MAP)
|
|
continue;
|
|
|
|
for (k = 0; k < EXPLORE_CAT_COUNT; k++)
|
|
fields[k] = NULL;
|
|
|
|
for (k = 0; k < item.val.map.len; k++)
|
|
{
|
|
const char *key_str = NULL;
|
|
struct rmsgpack_dom_value *key = &item.val.map.items[k].key;
|
|
struct rmsgpack_dom_value *val = &item.val.map.items[k].value;
|
|
if (!key || !val || key->type != RDT_STRING)
|
|
continue;
|
|
|
|
key_str = key->val.string.buff;
|
|
if (string_is_equal(key_str, "crc"))
|
|
{
|
|
switch (val->val.binary.len)
|
|
{
|
|
case 1:
|
|
crc32 = *(uint8_t*)val->val.binary.buff;
|
|
break;
|
|
case 2:
|
|
crc32 = swap_if_little16(*(uint16_t*)val->val.binary.buff);
|
|
break;
|
|
case 4:
|
|
crc32 = swap_if_little32(*(uint32_t*)val->val.binary.buff);
|
|
break;
|
|
default:
|
|
crc32 = 0;
|
|
break;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
else if (string_is_equal(key_str, "name"))
|
|
{
|
|
name = val->val.string.buff;
|
|
continue;
|
|
}
|
|
#ifdef EXPLORE_SHOW_ORIGINAL_TITLE
|
|
else if (string_is_equal(key_str, "original_title"))
|
|
{
|
|
original_title = val->val.string.buff;
|
|
continue;
|
|
}
|
|
#endif
|
|
|
|
for (cat = 0; cat != EXPLORE_CAT_COUNT; cat++)
|
|
{
|
|
if (!string_is_equal(key_str, explore_by_info[cat].rdbkey))
|
|
continue;
|
|
|
|
meta_count++;
|
|
if (explore_by_info[cat].is_numeric)
|
|
{
|
|
if (val->type >= RDT_STRING)
|
|
break;
|
|
snprintf(numeric_buf[cat],
|
|
sizeof(numeric_buf[cat]),
|
|
"%d", (int)val->val.int_);
|
|
fields[cat] = numeric_buf[cat];
|
|
break;
|
|
}
|
|
if (explore_by_info[cat].is_boolean)
|
|
{
|
|
if (val->type >= RDT_STRING)
|
|
break;
|
|
fields[cat] = msg_hash_to_str(val->val.int_ ?
|
|
MENU_ENUM_LABEL_VALUE_YES : MENU_ENUM_LABEL_VALUE_NO);
|
|
break;
|
|
}
|
|
if (val->type != RDT_STRING)
|
|
break;
|
|
fields[cat] = val->val.string.buff;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (crc32)
|
|
{
|
|
ptrdiff_t idx = RHMAP_IDX(rdb->playlist_crcs, crc32);
|
|
src = (idx != -1 ? &rdb->playlist_crcs[idx] : NULL);
|
|
}
|
|
if (!src && name)
|
|
{
|
|
ptrdiff_t idx = RHMAP_IDX_STR(rdb->playlist_names, name);
|
|
src = (idx != -1 ? &rdb->playlist_names[idx] : NULL);
|
|
}
|
|
if (!src)
|
|
continue;
|
|
if (src->entry_index != (uint32_t)-1 && src->meta_count >= meta_count)
|
|
continue;
|
|
|
|
if (src->entry_index == (uint32_t)-1)
|
|
{
|
|
src->entry_index = (uint32_t)RBUF_LEN(state->entries);
|
|
RBUF_RESIZE(state->entries, src->entry_index + 1);
|
|
}
|
|
e = &state->entries[src->entry_index];
|
|
src->meta_count = meta_count;
|
|
e->playlist_entry = src->source;
|
|
for (l = 0; l < EXPLORE_CAT_COUNT; l++)
|
|
e->by[l] = NULL;
|
|
e->split = NULL;
|
|
#ifdef EXPLORE_SHOW_ORIGINAL_TITLE
|
|
e->original_title = NULL;
|
|
#endif
|
|
|
|
fields[EXPLORE_BY_SYSTEM] = rdb->systemname;
|
|
|
|
for (cat = 0; cat != EXPLORE_CAT_COUNT; cat++)
|
|
{
|
|
explore_add_unique_string(state,
|
|
cat_maps, e, cat,
|
|
fields[cat], &split_buf);
|
|
}
|
|
|
|
#ifdef EXPLORE_SHOW_ORIGINAL_TITLE
|
|
if (original_title && *original_title)
|
|
{
|
|
size_t len = strlen(original_title) + 1;
|
|
e->original_title = (char*)
|
|
ex_arena_alloc(&state->arena, len);
|
|
memcpy(e->original_title, original_title, len);
|
|
}
|
|
#endif
|
|
|
|
if (RBUF_LEN(split_buf))
|
|
{
|
|
size_t len;
|
|
|
|
RBUF_PUSH(split_buf, NULL); /* terminator */
|
|
len = RBUF_SIZEOF(split_buf);
|
|
e->split = (explore_string_t **)
|
|
ex_arena_alloc(&state->arena, len);
|
|
memcpy(e->split, split_buf, len);
|
|
RBUF_CLEAR(split_buf);
|
|
}
|
|
|
|
/* if all entries have found connections, we can leave early */
|
|
if (--rdb->count == 0)
|
|
{
|
|
rmsgpack_dom_value_free(&item);
|
|
break;
|
|
}
|
|
}
|
|
|
|
libretrodb_cursor_close(cur);
|
|
libretrodb_cursor_free(cur);
|
|
libretrodb_close(rdb->handle);
|
|
libretrodb_free(rdb->handle);
|
|
RHMAP_FREE(rdb->playlist_crcs);
|
|
RHMAP_FREE(rdb->playlist_names);
|
|
}
|
|
RBUF_FREE(split_buf);
|
|
RHMAP_FREE(rdb_indices);
|
|
RBUF_FREE(rdbs);
|
|
|
|
for (i = 0; i != EXPLORE_CAT_COUNT; i++)
|
|
{
|
|
uint32_t idx;
|
|
size_t len = RBUF_LEN(state->by[i]);
|
|
|
|
if (state->by[i])
|
|
qsort(state->by[i], len, sizeof(*state->by[i]),
|
|
(explore_by_info[i].is_numeric ?
|
|
explore_qsort_func_nums : explore_qsort_func_strings));
|
|
|
|
for (idx = 0; idx != len; idx++)
|
|
state->by[i][idx]->idx = idx;
|
|
|
|
RHMAP_FREE(cat_maps[i]);
|
|
}
|
|
/* NULL is not a valid value as a first argument for qsort */
|
|
if (state->entries)
|
|
qsort(state->entries,
|
|
RBUF_LEN(state->entries),
|
|
sizeof(*state->entries), explore_qsort_func_entries);
|
|
return state;
|
|
}
|
|
|
|
static int explore_action_get_title(
|
|
const char *path, const char *label,
|
|
unsigned menu_type, char *s, size_t len)
|
|
{
|
|
strlcpy(s, (explore_state ? explore_state->title :
|
|
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_EXPLORE_TAB)), len);
|
|
return 0;
|
|
}
|
|
|
|
static void explore_append_title(explore_state_t *state,
|
|
const char* fmt, ...)
|
|
{
|
|
va_list ap;
|
|
size_t len = strlen(state->title);
|
|
va_start(ap, fmt);
|
|
vsnprintf(state->title + len,
|
|
sizeof(state->title) - len, fmt, ap);
|
|
va_end(ap);
|
|
}
|
|
|
|
static int explore_action_sublabel_spacer(
|
|
file_list_t *list, unsigned type, unsigned i,
|
|
const char *label, const char *path, char *s, size_t len)
|
|
{
|
|
#ifdef HAVE_OZONE
|
|
const char *menu_driver = menu_driver_ident();
|
|
/* Only add a blank 'spacer' sublabel when
|
|
* using Ozone
|
|
* > In XMB/GLUI it upsets the vertical layout
|
|
* > In RGUI it does nothing other than
|
|
* unnecessarily blank out the fallback
|
|
* core title text in the sublabel area */
|
|
if (string_is_equal(menu_driver, "ozone"))
|
|
{
|
|
s[0] = ' ';
|
|
s[1] = '\0';
|
|
}
|
|
#endif
|
|
return 1; /* 1 means it'll never change and can be cached */
|
|
}
|
|
|
|
static int explore_action_ok(const char *path, const char *label,
|
|
unsigned type, size_t idx, size_t entry_idx)
|
|
{
|
|
const char* explore_tab = msg_hash_to_str(MENU_ENUM_LABEL_EXPLORE_TAB);
|
|
if (type >= EXPLORE_TYPE_FIRSTITEM || type == EXPLORE_TYPE_FILTERNULL)
|
|
{
|
|
struct menu_state *menu_st = menu_state_get_ptr();
|
|
menu_list_t *menu_list = menu_st->entries.list;
|
|
file_list_t *menu_stack = MENU_LIST_GET(menu_list, 0);
|
|
unsigned prev_type = menu_stack->list[menu_stack->size - 1].type;
|
|
unsigned cat = (prev_type - EXPLORE_TYPE_FIRSTCATEGORY);
|
|
if (cat < EXPLORE_CAT_COUNT)
|
|
{
|
|
explore_state_t *state = explore_state;
|
|
unsigned lvl, lvl_max = state->view_levels;
|
|
for (lvl = 0; lvl != lvl_max; lvl++)
|
|
if (state->view_cats[lvl] == cat)
|
|
break;
|
|
if (lvl == lvl_max)
|
|
{
|
|
/* new matching filter */
|
|
state->view_op[lvl] = EXPLORE_OP_EQUAL;
|
|
state->view_use_split[lvl] = explore_by_info[cat].use_split;
|
|
state->view_cats[lvl] = cat;
|
|
state->view_match[lvl] = (type == EXPLORE_TYPE_FILTERNULL ?
|
|
NULL : state->by[cat][type-EXPLORE_TYPE_FIRSTITEM]);
|
|
state->view_levels++;
|
|
}
|
|
else
|
|
{
|
|
/* switch from match to filter range */
|
|
explore_string_t* r1 = state->view_match[lvl];
|
|
explore_string_t* r2 = state->by[cat][type-EXPLORE_TYPE_FIRSTITEM];
|
|
state->view_op[lvl] = EXPLORE_OP_RANGE;
|
|
state->view_idx_min[lvl] = (r1->idx < r2->idx ? r1->idx : r2->idx);
|
|
state->view_idx_max[lvl] = (r1->idx < r2->idx ? r2->idx : r1->idx);
|
|
}
|
|
}
|
|
}
|
|
filebrowser_clear_type();
|
|
return generic_action_ok_displaylist_push(explore_tab,
|
|
NULL, explore_tab, type, idx, entry_idx, ACTION_OK_DL_PUSH_DEFAULT);
|
|
}
|
|
|
|
static int explore_cancel(const char *path,
|
|
const char *label, unsigned type, size_t idx)
|
|
{
|
|
struct menu_state *menu_st = menu_state_get_ptr();
|
|
menu_list_t *menu_list = menu_st->entries.list;
|
|
file_list_t *menu_stack = MENU_LIST_GET(menu_list, 0);
|
|
unsigned closed_type = menu_stack->list[menu_stack->size - 1].type;
|
|
if (closed_type >= EXPLORE_TYPE_FIRSTITEM ||
|
|
closed_type == EXPLORE_TYPE_FILTERNULL)
|
|
{
|
|
/* popping one filter, check if popping filter range or match */
|
|
unsigned prev_type = menu_stack->list[menu_stack->size - 2].type;
|
|
unsigned cat = (prev_type - EXPLORE_TYPE_FIRSTCATEGORY);
|
|
unsigned lvl, lvl_max = explore_state->view_levels;
|
|
for (lvl = 0; lvl != lvl_max; lvl++)
|
|
{
|
|
if (explore_state->view_cats[lvl] != cat) continue;
|
|
if (explore_state->view_op[lvl] == EXPLORE_OP_EQUAL)
|
|
explore_state->view_levels--;
|
|
else
|
|
{
|
|
explore_state->view_op[lvl] = EXPLORE_OP_EQUAL;
|
|
if (!explore_state->view_match[lvl])
|
|
explore_state->view_match[lvl] =
|
|
explore_state->by[cat][explore_state->view_idx_min[lvl]];
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
else if (closed_type == EXPLORE_TYPE_SEARCH)
|
|
explore_state->view_search[0] = '\0';
|
|
return action_cancel_pop_default(path, label, type, idx);
|
|
}
|
|
|
|
void explore_menu_entry(file_list_t *list, explore_state_t *state,
|
|
const char *path, unsigned type, int (*action_ok)(const char *path,
|
|
const char *label, unsigned type, size_t idx, size_t entry_idx))
|
|
{
|
|
menu_file_list_cbs_t *cbs;
|
|
if (!menu_entries_append(list, path, state->label_explore_item_str,
|
|
MENU_ENUM_LABEL_EXPLORE_ITEM, type, 0, 0, NULL))
|
|
return;
|
|
cbs = ((menu_file_list_cbs_t*)list->list[list->size-1].actiondata);
|
|
if (!cbs)
|
|
return;
|
|
cbs->action_ok = action_ok;
|
|
cbs->action_cancel = explore_cancel;
|
|
}
|
|
|
|
static void explore_action_find_complete(void *userdata, const char *line)
|
|
{
|
|
menu_input_dialog_end();
|
|
if (line && *line)
|
|
{
|
|
strlcpy(explore_state->view_search, line,
|
|
sizeof(explore_state->view_search));
|
|
explore_action_ok(NULL, NULL, EXPLORE_TYPE_SEARCH, 0, 0);
|
|
}
|
|
}
|
|
|
|
static int explore_action_ok_find(const char *path, const char *label, unsigned type, size_t idx, size_t entry_idx)
|
|
{
|
|
menu_input_ctx_line_t line;
|
|
line.label = msg_hash_to_str(MENU_ENUM_LABEL_VALUE_SEARCH);
|
|
line.label_setting = NULL;
|
|
line.type = 0;
|
|
line.idx = 0;
|
|
line.cb = explore_action_find_complete;
|
|
menu_input_dialog_start(&line);
|
|
return 0;
|
|
}
|
|
|
|
static const char* explore_get_view_path(struct menu_state *menu_st, menu_list_t *menu_list,
|
|
file_list_t *menu_stack)
|
|
{
|
|
struct item_file *cur = (struct item_file *)&menu_stack->list[menu_stack->size - 1];
|
|
|
|
/* check if we are opening a saved view from the horizontal/tabs menu */
|
|
if (cur->type == MENU_SETTING_HORIZONTAL_MENU)
|
|
{
|
|
const menu_ctx_driver_t *driver_ctx = menu_st->driver_ctx;
|
|
if (driver_ctx->list_get_entry)
|
|
{
|
|
size_t selection = driver_ctx->list_get_selection ? driver_ctx->list_get_selection(menu_st->userdata) : 0;
|
|
size_t size = driver_ctx->list_get_size ? driver_ctx->list_get_size(menu_st->userdata, MENU_LIST_TABS) : 0;
|
|
if (selection > 0 && size > 0)
|
|
{
|
|
struct item_file *item = NULL;
|
|
/* Label contains the path and path contains the label */
|
|
if ((item = (struct item_file*)driver_ctx->list_get_entry(menu_st->userdata, MENU_LIST_HORIZONTAL,
|
|
(unsigned)(selection - (size +1)))))
|
|
return item->label;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* check if we are opening a saved view via Content > Playlists */
|
|
if ( (cur->type == MENU_EXPLORE_TAB)
|
|
&& cur->path
|
|
&& !string_is_equal(cur->path,
|
|
msg_hash_to_str(MENU_ENUM_LABEL_GOTO_EXPLORE))
|
|
)
|
|
return cur->path;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static void explore_on_edit_views(enum msg_hash_enums msg)
|
|
{
|
|
struct menu_state *menu_st = menu_state_get_ptr();
|
|
if (menu_st->driver_ctx->environ_cb)
|
|
menu_st->driver_ctx->environ_cb(MENU_ENVIRON_RESET_HORIZONTAL_LIST,
|
|
NULL, menu_st->userdata);
|
|
|
|
runloop_msg_queue_push(msg_hash_to_str(msg),
|
|
1, 180, true, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
|
|
}
|
|
|
|
static int explore_action_ok_deleteview(const char *path, const char *label, unsigned type, size_t idx, size_t entry_idx)
|
|
{
|
|
struct menu_state *menu_st = menu_state_get_ptr();
|
|
menu_list_t *menu_list = menu_st->entries.list;
|
|
file_list_t *menu_stack = MENU_LIST_GET(menu_list, 0);
|
|
|
|
filestream_delete(explore_get_view_path(menu_st, menu_list, menu_stack));
|
|
explore_on_edit_views(MENU_ENUM_LABEL_EXPLORE_VIEW_DELETED);
|
|
|
|
/* if we're at the top of the menu we can't cancel so just refresh
|
|
what becomes selected after MENU_ENVIRON_RESET_HORIZONTAL_LIST. */
|
|
if (menu_stack->size == 1)
|
|
menu_st->flags |= MENU_ST_FLAG_ENTRIES_NEED_REFRESH;
|
|
|
|
return explore_cancel(path, label, type, idx);
|
|
}
|
|
|
|
static void explore_action_saveview_complete(void *userdata, const char *name)
|
|
{
|
|
size_t _len;
|
|
int count = 0, op;
|
|
char lvwpath[PATH_MAX_LENGTH];
|
|
intfstream_t *file = NULL;
|
|
rjsonwriter_t* w = NULL;
|
|
explore_state_t *state = explore_state;
|
|
settings_t *settings = NULL;
|
|
|
|
menu_input_dialog_end();
|
|
if (!name || !*name)
|
|
return;
|
|
|
|
settings = config_get_ptr();
|
|
_len = fill_pathname_join_special(lvwpath,
|
|
settings->paths.directory_playlist, name, sizeof(lvwpath));
|
|
strlcpy(lvwpath + _len, ".lvw", sizeof(lvwpath) - _len);
|
|
|
|
if (filestream_exists(lvwpath))
|
|
{
|
|
runloop_msg_queue_push(msg_hash_to_str(MENU_ENUM_LABEL_EXPLORE_VIEW_EXISTS),
|
|
1, 360, true, NULL,
|
|
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_ERROR);
|
|
return;
|
|
}
|
|
|
|
file = intfstream_open_file(lvwpath,
|
|
RETRO_VFS_FILE_ACCESS_WRITE, RETRO_VFS_FILE_ACCESS_HINT_NONE);
|
|
if (!file)
|
|
{
|
|
RARCH_ERR("[explore view] Failed to write json file %s.\n", lvwpath);
|
|
return;
|
|
}
|
|
|
|
w = rjsonwriter_open_stream(file);
|
|
|
|
rjsonwriter_add_start_object(w);
|
|
|
|
if (state->view_search[0])
|
|
{
|
|
rjsonwriter_add_newline(w);
|
|
rjsonwriter_add_tabs(w, 1);
|
|
rjsonwriter_add_string(w, "filter_name");
|
|
rjsonwriter_add_colon(w);
|
|
rjsonwriter_add_space(w);
|
|
rjsonwriter_add_string(w, state->view_search);
|
|
count++;
|
|
}
|
|
|
|
for (op = EXPLORE_OP_EQUAL; op <= EXPLORE_OP_MAX; op++)
|
|
{
|
|
unsigned i, n;
|
|
for (i = n = 0; i != state->view_levels; i++)
|
|
{
|
|
uint8_t vop = state->view_op[i];
|
|
unsigned vcat = state->view_cats[i];
|
|
explore_string_t **by = state->by[vcat];
|
|
if (vop != op && (vop != EXPLORE_OP_RANGE || op == EXPLORE_OP_EQUAL))
|
|
continue;
|
|
|
|
if (n++ == 0)
|
|
{
|
|
if (count++)
|
|
rjsonwriter_add_comma(w);
|
|
rjsonwriter_add_newline(w);
|
|
rjsonwriter_add_tabs(w, 1);
|
|
rjsonwriter_add_string(w,
|
|
(op == EXPLORE_OP_EQUAL ? "filter_equal" :
|
|
(op == EXPLORE_OP_MIN ? "filter_min" :
|
|
(op == EXPLORE_OP_MAX ? "filter_max" : ""))));
|
|
rjsonwriter_add_colon(w);
|
|
rjsonwriter_add_space(w);
|
|
rjsonwriter_add_start_object(w);
|
|
}
|
|
if (n > 1)
|
|
rjsonwriter_add_comma(w);
|
|
rjsonwriter_add_newline(w);
|
|
rjsonwriter_add_tabs(w, 2);
|
|
rjsonwriter_add_string(w, explore_by_info[vcat].rdbkey);
|
|
rjsonwriter_add_colon(w);
|
|
rjsonwriter_add_space(w);
|
|
rjsonwriter_add_string(w,
|
|
((op == EXPLORE_OP_EQUAL && !state->view_match[i]) ? "" :
|
|
(op == EXPLORE_OP_EQUAL ? state->view_match[i]->str :
|
|
(op == EXPLORE_OP_MIN ? by[state->view_idx_min[i]]->str :
|
|
(op == EXPLORE_OP_MAX ? by[state->view_idx_max[i]]->str :
|
|
"")))));
|
|
}
|
|
if (n)
|
|
{
|
|
rjsonwriter_add_newline(w);
|
|
rjsonwriter_add_tabs(w, 1);
|
|
rjsonwriter_add_end_object(w);
|
|
}
|
|
}
|
|
rjsonwriter_add_newline(w);
|
|
rjsonwriter_add_end_object(w);
|
|
rjsonwriter_add_newline(w);
|
|
rjsonwriter_free(w);
|
|
intfstream_close(file);
|
|
free(file);
|
|
|
|
explore_on_edit_views(MENU_ENUM_LABEL_EXPLORE_VIEW_SAVED);
|
|
}
|
|
|
|
static int explore_action_ok_saveview(const char *path, const char *label, unsigned type, size_t idx, size_t entry_idx)
|
|
{
|
|
menu_input_ctx_line_t line;
|
|
line.label = msg_hash_to_str(MENU_ENUM_LABEL_EXPLORE_NEW_VIEW);
|
|
line.label_setting = NULL;
|
|
line.type = 0;
|
|
line.idx = 0;
|
|
line.cb = explore_action_saveview_complete;
|
|
menu_input_dialog_start(&line);
|
|
return 0;
|
|
}
|
|
|
|
static void explore_load_view(explore_state_t *state, const char* path)
|
|
{
|
|
unsigned cat;
|
|
rjson_t* json;
|
|
intfstream_t *file;
|
|
enum rjson_type type;
|
|
uint8_t op = ((uint8_t)-1);
|
|
|
|
state->view_levels = 0;
|
|
state->view_search[0] = '\0';
|
|
|
|
if (!(file = intfstream_open_file(path,
|
|
RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE)))
|
|
return;
|
|
|
|
json = rjson_open_stream(file);
|
|
|
|
/* Configure parser */
|
|
rjson_set_options(json,
|
|
RJSON_OPTION_ALLOW_UTF8BOM|RJSON_OPTION_ALLOW_COMMENTS);
|
|
|
|
while ((type = rjson_next(json)) != RJSON_DONE && type != RJSON_ERROR)
|
|
{
|
|
unsigned int depth = rjson_get_context_depth(json);
|
|
if (depth == 1 && type == RJSON_STRING)
|
|
{
|
|
const char* key = rjson_get_string(json, NULL);
|
|
if ( string_is_equal(key, "filter_name")
|
|
&& rjson_next(json) == RJSON_STRING)
|
|
strlcpy(state->view_search,
|
|
rjson_get_string(json, NULL),
|
|
sizeof(state->view_search));
|
|
else if ( string_is_equal(key, "filter_equal")
|
|
&& rjson_next(json) == RJSON_OBJECT)
|
|
op = EXPLORE_OP_EQUAL;
|
|
else if ( string_is_equal(key, "filter_min")
|
|
&& rjson_next(json) == RJSON_OBJECT)
|
|
op = EXPLORE_OP_MIN;
|
|
else if ( string_is_equal(key, "filter_max")
|
|
&& rjson_next(json) == RJSON_OBJECT)
|
|
op = EXPLORE_OP_MAX;
|
|
}
|
|
else if ( (depth == 2)
|
|
&& (type == RJSON_STRING)
|
|
&& (op != ((uint8_t)-1)))
|
|
{
|
|
const char* key = rjson_get_string(json, NULL);
|
|
for (cat = 0; cat != EXPLORE_CAT_COUNT; cat++)
|
|
if (string_is_equal(key, explore_by_info[cat].rdbkey))
|
|
break;
|
|
if (cat == EXPLORE_CAT_COUNT)
|
|
rjson_next(json); /* skip value */
|
|
else
|
|
{
|
|
explore_string_t **entries = state->by[cat];
|
|
const char* value = NULL;
|
|
unsigned lvl, lvl_max = state->view_levels;
|
|
uint8_t valid_op = ((uint8_t)-1);
|
|
type = rjson_next(json);
|
|
if ( type == RJSON_STRING
|
|
|| type == RJSON_NUMBER)
|
|
value = rjson_get_string(json, NULL);
|
|
if (value && !*value)
|
|
value = NULL;
|
|
|
|
for (lvl = 0; lvl != lvl_max; lvl++)
|
|
if (state->view_cats[lvl] == cat)
|
|
break;
|
|
|
|
if (!value && state->has_unknown[cat] && op == EXPLORE_OP_EQUAL)
|
|
{
|
|
state->view_match[lvl] = NULL;
|
|
valid_op = EXPLORE_OP_EQUAL;
|
|
}
|
|
else if (value && entries)
|
|
{
|
|
int cmp;
|
|
/* use existing qsort function for binary search */
|
|
explore_string_t *evalue = (explore_string_t *)
|
|
(value - offsetof(explore_string_t, str));
|
|
uint32_t i, ifrom, ito, imax = (uint32_t)RBUF_LEN(entries);
|
|
int (*compare_func)(const void *, const void *) =
|
|
(explore_by_info[cat].is_numeric
|
|
? explore_qsort_func_nums
|
|
: explore_qsort_func_strings);
|
|
|
|
/* binary search index where entry <= value */
|
|
for (i = 0, ifrom = 0, ito = imax, cmp = 1; ito && cmp;)
|
|
{
|
|
size_t remain = ito % 2;
|
|
ito = ito / 2;
|
|
i = ifrom + ito;
|
|
cmp = compare_func(&evalue, &entries[i]);
|
|
if (cmp >= 0)
|
|
ifrom = (uint32_t)(i + remain);
|
|
}
|
|
|
|
if (op == EXPLORE_OP_EQUAL && !cmp)
|
|
{
|
|
state->view_match[lvl] = entries[i];
|
|
valid_op = EXPLORE_OP_EQUAL;
|
|
}
|
|
else if (op == EXPLORE_OP_MIN)
|
|
{
|
|
state->view_idx_min[lvl] = (cmp ? i + 1 : i);
|
|
valid_op =
|
|
((lvl != lvl_max && state->view_op[lvl] == EXPLORE_OP_MAX)
|
|
? EXPLORE_OP_RANGE : EXPLORE_OP_MIN);
|
|
}
|
|
else if (op == EXPLORE_OP_MAX)
|
|
{
|
|
state->view_idx_max[lvl] = i;
|
|
valid_op =
|
|
((lvl != lvl_max && state->view_op[lvl] == EXPLORE_OP_MIN)
|
|
? EXPLORE_OP_RANGE : EXPLORE_OP_MAX);
|
|
}
|
|
}
|
|
if (valid_op != ((uint8_t)-1))
|
|
{
|
|
state->view_op [lvl] = valid_op;
|
|
state->view_cats [lvl] = cat;
|
|
state->view_use_split[lvl] = explore_by_info[cat].use_split;
|
|
if (lvl == lvl_max)
|
|
state->view_levels++;
|
|
}
|
|
}
|
|
}
|
|
else if (depth == 1 && type == RJSON_OBJECT_END)
|
|
op = ((uint8_t)-1);
|
|
}
|
|
rjson_free(json);
|
|
intfstream_close(file);
|
|
free(file);
|
|
}
|
|
|
|
unsigned menu_displaylist_explore(file_list_t *list, settings_t *settings)
|
|
{
|
|
unsigned i;
|
|
char tmp[512];
|
|
struct explore_state *state = explore_state;
|
|
struct menu_state *menu_st = menu_state_get_ptr();
|
|
menu_handle_t *menu = menu_st->driver_data;
|
|
menu_list_t *menu_list = menu_st->entries.list;
|
|
file_list_t *menu_stack = MENU_LIST_GET(menu_list, 0);
|
|
struct item_file *stack_top = menu_stack->list;
|
|
size_t depth = menu_stack->size;
|
|
unsigned current_type = (depth > 0 ? stack_top[depth - 1].type : 0);
|
|
unsigned previous_type = (depth > 1 ? stack_top[depth - 2].type : 0);
|
|
unsigned current_cat = current_type - EXPLORE_TYPE_FIRSTCATEGORY;
|
|
|
|
/* overwrite the menu title function with our custom one */
|
|
/* depth 1 is never popped so we can only do this on sub menus */
|
|
if (depth > 1)
|
|
((menu_file_list_cbs_t*)stack_top[depth - 1].actiondata)
|
|
->action_get_title = explore_action_get_title;
|
|
|
|
if (!state)
|
|
{
|
|
if (!menu_explore_init_in_progress(NULL))
|
|
task_push_menu_explore_init(
|
|
settings->paths.directory_playlist,
|
|
settings->paths.path_content_database);
|
|
|
|
menu_entries_append(list,
|
|
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_EXPLORE_INITIALISING_LIST),
|
|
msg_hash_to_str(MENU_ENUM_LABEL_EXPLORE_INITIALISING_LIST),
|
|
MENU_ENUM_LABEL_EXPLORE_INITIALISING_LIST,
|
|
FILE_TYPE_NONE, 0, 0, NULL);
|
|
|
|
return (unsigned)list->size;
|
|
}
|
|
|
|
/* check if we are opening a saved view */
|
|
if (current_type == MENU_SETTING_HORIZONTAL_MENU || current_type == MENU_EXPLORE_TAB)
|
|
{
|
|
const char* view_path = explore_get_view_path(menu_st, menu_list, menu_stack);
|
|
if (view_path)
|
|
{
|
|
explore_load_view(state, view_path);
|
|
current_type = EXPLORE_TYPE_VIEW;
|
|
}
|
|
}
|
|
|
|
/* clear any filter remaining from showing a view on the horizontal menu */
|
|
if (current_type == MENU_EXPLORE_TAB)
|
|
{
|
|
state->view_levels = 0;
|
|
state->view_search[0] = '\0';
|
|
}
|
|
|
|
/* clear title string */
|
|
state->title[0] = '\0';
|
|
|
|
/* append filtered categories to title */
|
|
for (i = 0; i != state->view_levels; i++)
|
|
{
|
|
unsigned cat = state->view_cats[i];
|
|
explore_string_t **entries = state->by[cat];
|
|
explore_string_t *match = state->view_match[i];
|
|
explore_append_title(state, "%s%s: ", (i ? " / " : ""),
|
|
msg_hash_to_str(explore_by_info[cat].name_enum));
|
|
switch (state->view_op[i])
|
|
{
|
|
case EXPLORE_OP_EQUAL:
|
|
explore_append_title(state, "%s", (match
|
|
? match->str
|
|
: msg_hash_to_str(MENU_ENUM_LABEL_VALUE_UNKNOWN)));
|
|
break;
|
|
case EXPLORE_OP_MIN:
|
|
explore_append_title(state, "%s - %s",
|
|
entries[state->view_idx_min[i]]->str,
|
|
entries[RBUF_LEN(entries)-1]->str);
|
|
break;
|
|
case EXPLORE_OP_MAX:
|
|
explore_append_title(state, "%s - %s",
|
|
entries[0]->str,
|
|
entries[state->view_idx_max[i]]->str);
|
|
break;
|
|
case EXPLORE_OP_RANGE:
|
|
explore_append_title(state, "%s - %s",
|
|
entries[state->view_idx_min[i]]->str,
|
|
entries[state->view_idx_max[i]]->str);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* append string search to title */
|
|
if (*state->view_search)
|
|
explore_append_title(state, "%s%s: '%s'",
|
|
(state->view_levels ? " / " : ""),
|
|
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_RDB_ENTRY_NAME),
|
|
state->view_search);
|
|
|
|
state->show_icons = (current_cat == EXPLORE_BY_SYSTEM)
|
|
? EXPLORE_ICONS_SYSTEM_CATEGORY
|
|
: (current_type >= EXPLORE_TYPE_FIRSTITEM)
|
|
? EXPLORE_ICONS_CONTENT
|
|
: EXPLORE_ICONS_OFF;
|
|
|
|
if ( current_type == MENU_EXPLORE_TAB
|
|
|| current_type == EXPLORE_TYPE_ADDITIONALFILTER)
|
|
{
|
|
/* Explore top or selecting an additional filter category */
|
|
unsigned cat;
|
|
bool is_top = (current_type == MENU_EXPLORE_TAB);
|
|
if (is_top)
|
|
strlcpy(state->title,
|
|
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_EXPLORE_TAB),
|
|
sizeof(state->title));
|
|
else
|
|
explore_append_title(state, " / %s",
|
|
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_EXPLORE_ADDITIONAL_FILTER));
|
|
|
|
if (!*state->view_search)
|
|
{
|
|
/* Start navigation from first item */
|
|
if (menu_st->selection_ptr < 1)
|
|
menu_st->selection_ptr = 1;
|
|
|
|
explore_menu_entry(
|
|
list, state,
|
|
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_EXPLORE_SEARCH_NAME),
|
|
EXPLORE_TYPE_SEARCH, explore_action_ok_find);
|
|
if (list->size)
|
|
((menu_file_list_cbs_t*)list->list[list->size-1].actiondata)->action_sublabel = explore_action_sublabel_spacer;
|
|
}
|
|
|
|
for (cat = 0; cat < EXPLORE_CAT_COUNT; cat++)
|
|
{
|
|
explore_string_t **entries = state->by[cat];
|
|
if (!RBUF_LEN(entries))
|
|
continue;
|
|
|
|
/* don't list already filtered categories unless it can be filtered by range */
|
|
for (i = 0; i != state->view_levels; i++)
|
|
if (state->view_cats[i] == cat)
|
|
break;
|
|
|
|
if (i == state->view_levels || (i == state->view_levels - 1
|
|
&& state->view_op[i] == EXPLORE_OP_EQUAL
|
|
&& !explore_by_info[cat].is_boolean
|
|
&& RBUF_LEN(state->by[cat]) > 1))
|
|
{
|
|
size_t tmplen = strlcpy(tmp,
|
|
msg_hash_to_str(explore_by_info[cat].by_enum), sizeof(tmp));
|
|
|
|
if (is_top)
|
|
{
|
|
if (explore_by_info[cat].is_numeric)
|
|
snprintf(tmp + tmplen,
|
|
sizeof(tmp) - tmplen,
|
|
" (%s - %s)",
|
|
entries[0]->str,
|
|
entries[RBUF_LEN(entries) - 1]->str);
|
|
else if (!explore_by_info[cat].is_boolean)
|
|
{
|
|
tmplen += strlcpy (tmp + tmplen, " (", sizeof(tmp) - tmplen);
|
|
tmplen += snprintf(tmp + tmplen, sizeof(tmp) - tmplen,
|
|
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_EXPLORE_ITEMS_COUNT),
|
|
(unsigned)RBUF_LEN(entries));
|
|
strlcpy(tmp + tmplen, ")", sizeof(tmp) - tmplen);
|
|
}
|
|
}
|
|
else if (i != state->view_levels)
|
|
{
|
|
tmplen += strlcpy(tmp + tmplen, " (", sizeof(tmp) - tmplen);
|
|
tmplen += strlcpy(tmp + tmplen,
|
|
msg_hash_to_str(MENU_ENUM_LABEL_EXPLORE_RANGE_FILTER),
|
|
sizeof(tmp) - tmplen);
|
|
strlcpy(tmp + tmplen, ")", sizeof(tmp) - tmplen);
|
|
}
|
|
|
|
explore_menu_entry(list, state,
|
|
tmp, cat + EXPLORE_TYPE_FIRSTCATEGORY, explore_action_ok);
|
|
}
|
|
}
|
|
|
|
if (is_top)
|
|
{
|
|
if (list->size)
|
|
((menu_file_list_cbs_t*)list->list[list->size-1].actiondata)->action_sublabel =
|
|
explore_action_sublabel_spacer;
|
|
explore_menu_entry(list, state,
|
|
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_EXPLORE_SHOW_ALL),
|
|
EXPLORE_TYPE_SHOWALL, explore_action_ok);
|
|
}
|
|
}
|
|
else if ((!state->view_levels && !*state->view_search
|
|
&& current_cat < EXPLORE_CAT_COUNT))
|
|
{
|
|
/* Unfiltered list of all items in a selected explore by category */
|
|
explore_string_t **entries = state->by[current_cat];
|
|
size_t i_last = RBUF_LEN(entries) - 1;
|
|
for (i = 0; i <= i_last; i++)
|
|
explore_menu_entry(list, state,
|
|
entries[i]->str, EXPLORE_TYPE_FIRSTITEM + i, explore_action_ok);
|
|
|
|
if (state->has_unknown[current_cat])
|
|
{
|
|
if (list->size)
|
|
((menu_file_list_cbs_t*)list->list[list->size-1].actiondata)->action_sublabel =
|
|
explore_action_sublabel_spacer;
|
|
explore_menu_entry(list, state,
|
|
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_UNKNOWN),
|
|
EXPLORE_TYPE_FILTERNULL, explore_action_ok);
|
|
}
|
|
|
|
explore_append_title(state, "%s",
|
|
msg_hash_to_str(explore_by_info[current_cat].by_enum));
|
|
}
|
|
else if (current_type < EXPLORE_TYPE_FIRSTITEM
|
|
|| (previous_type >= EXPLORE_TYPE_FIRSTCATEGORY
|
|
&& previous_type < EXPLORE_TYPE_FIRSTITEM))
|
|
{
|
|
size_t first_list_entry;
|
|
unsigned view_levels = state->view_levels;
|
|
char* view_search = state->view_search;
|
|
uint8_t* view_op = state->view_op;
|
|
bool* view_use_split = state->view_use_split;
|
|
unsigned* view_cats = state->view_cats;
|
|
explore_string_t** view_match = state->view_match;
|
|
uint32_t* view_idx_min = state->view_idx_min;
|
|
uint32_t* view_idx_max = state->view_idx_max;
|
|
explore_entry_t* entries = state->entries, *e, *eend;
|
|
bool* map_filtered_category = NULL;
|
|
bool has_search = !!*view_search;
|
|
bool is_show_all = (!view_levels && !has_search);
|
|
bool is_filtered_category = (current_cat < EXPLORE_CAT_COUNT);
|
|
bool filtered_category_have_unknown = false;
|
|
|
|
if (is_show_all)
|
|
explore_append_title(state,
|
|
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_EXPLORE_ALL));
|
|
|
|
if (is_filtered_category)
|
|
{
|
|
/* List filtered items in a selected explore by category */
|
|
if (!view_levels || view_cats[view_levels - 1] != current_cat)
|
|
explore_append_title(state, " / %s",
|
|
msg_hash_to_str(explore_by_info[current_cat].by_enum));
|
|
else
|
|
{
|
|
/* List all items again when setting a range filter */
|
|
explore_append_title(state, " (%s)",
|
|
msg_hash_to_str(MENU_ENUM_LABEL_EXPLORE_RANGE_FILTER));
|
|
view_levels--;
|
|
}
|
|
}
|
|
else if (current_type == EXPLORE_TYPE_VIEW)
|
|
{
|
|
/* Start navigation from first item */
|
|
if (menu_st->selection_ptr < 1)
|
|
menu_st->selection_ptr = 1;
|
|
|
|
/* Show a saved view */
|
|
state->show_icons = EXPLORE_ICONS_CONTENT;
|
|
explore_menu_entry(list, state,
|
|
msg_hash_to_str(MENU_ENUM_LABEL_EXPLORE_DELETE_VIEW),
|
|
EXPLORE_TYPE_VIEW, explore_action_ok_deleteview);
|
|
if (list->size)
|
|
((menu_file_list_cbs_t*)list->list[list->size-1].actiondata)->action_sublabel =
|
|
explore_action_sublabel_spacer;
|
|
}
|
|
else
|
|
{
|
|
/* Start navigation from first item */
|
|
if (menu_st->selection_ptr < 2)
|
|
menu_st->selection_ptr = 2;
|
|
|
|
/* Game list */
|
|
state->show_icons = EXPLORE_ICONS_CONTENT;
|
|
explore_menu_entry(list, state,
|
|
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_EXPLORE_ADD_ADDITIONAL_FILTER),
|
|
EXPLORE_TYPE_ADDITIONALFILTER, explore_action_ok);
|
|
explore_menu_entry(list, state,
|
|
msg_hash_to_str(MENU_ENUM_LABEL_EXPLORE_SAVE_VIEW),
|
|
EXPLORE_TYPE_VIEW, explore_action_ok_saveview);
|
|
if (list->size)
|
|
((menu_file_list_cbs_t*)list->list[list->size-1].actiondata)->action_sublabel =
|
|
explore_action_sublabel_spacer;
|
|
}
|
|
|
|
first_list_entry = list->size;
|
|
for (e = entries, eend = RBUF_END(entries); e != eend; e++)
|
|
{
|
|
for (i = 0; i != view_levels; i++)
|
|
{
|
|
explore_string_t* eby = e->by[view_cats[i]];
|
|
switch (view_op[i])
|
|
{
|
|
case EXPLORE_OP_EQUAL:
|
|
if (view_match[i] == eby)
|
|
continue;
|
|
if (view_use_split[i] && e->split)
|
|
{
|
|
explore_string_t** split = e->split;
|
|
do
|
|
{
|
|
if (*split == view_match[i])
|
|
break;
|
|
} while (*(++split));
|
|
if (*split)
|
|
continue;
|
|
}
|
|
goto SKIP_ENTRY;
|
|
case EXPLORE_OP_MIN:
|
|
if (eby && eby->idx >= view_idx_min[i])
|
|
continue;
|
|
goto SKIP_ENTRY;
|
|
case EXPLORE_OP_MAX:
|
|
if (eby && eby->idx <= view_idx_max[i])
|
|
continue;
|
|
goto SKIP_ENTRY;
|
|
case EXPLORE_OP_RANGE:
|
|
if (eby && eby->idx >= view_idx_min[i]
|
|
&& eby->idx <= view_idx_max[i])
|
|
continue;
|
|
goto SKIP_ENTRY;
|
|
}
|
|
}
|
|
|
|
if (has_search && !strcasestr(e->playlist_entry->label, view_search))
|
|
goto SKIP_ENTRY;
|
|
|
|
if (is_filtered_category)
|
|
{
|
|
explore_string_t* str = e->by[current_cat];
|
|
if (!str)
|
|
{
|
|
filtered_category_have_unknown = true;
|
|
continue;
|
|
}
|
|
if (RHMAP_HAS(map_filtered_category, str->idx + 1))
|
|
continue;
|
|
RHMAP_SET(map_filtered_category, str->idx + 1, true);
|
|
explore_menu_entry(list, state, str->str,
|
|
EXPLORE_TYPE_FIRSTITEM + str->idx, explore_action_ok);
|
|
}
|
|
#ifdef EXPLORE_SHOW_ORIGINAL_TITLE
|
|
else if (e->original_title)
|
|
explore_menu_entry(list, state, e->original_title,
|
|
EXPLORE_TYPE_FIRSTITEM + (e - entries), explore_action_ok);
|
|
#endif
|
|
else
|
|
explore_menu_entry(list, state, e->playlist_entry->label,
|
|
(unsigned)(EXPLORE_TYPE_FIRSTITEM + (e - entries)), explore_action_ok);
|
|
SKIP_ENTRY:;
|
|
}
|
|
|
|
if (is_filtered_category)
|
|
qsort(list->list, list->size, sizeof(*list->list),
|
|
explore_qsort_func_menulist);
|
|
|
|
explore_append_title(state,
|
|
" (%u)", (unsigned)(list->size - first_list_entry));
|
|
|
|
if (is_filtered_category && filtered_category_have_unknown)
|
|
{
|
|
if (list->size)
|
|
((menu_file_list_cbs_t*)list->list[list->size-1].actiondata)->action_sublabel =
|
|
explore_action_sublabel_spacer;
|
|
explore_menu_entry(list, state,
|
|
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_UNKNOWN),
|
|
EXPLORE_TYPE_FILTERNULL, explore_action_ok);
|
|
}
|
|
|
|
RHMAP_FREE(map_filtered_category);
|
|
}
|
|
else
|
|
{
|
|
/* Content page of selected game */
|
|
int pl_idx;
|
|
const struct playlist_entry *pl_entry =
|
|
state->entries[current_type - EXPLORE_TYPE_FIRSTITEM].playlist_entry;
|
|
|
|
strlcpy(state->title,
|
|
pl_entry->label, sizeof(state->title));
|
|
|
|
for (pl_idx = 0; pl_idx != (int)RBUF_LEN(state->playlists); pl_idx++)
|
|
{
|
|
menu_displaylist_info_t info;
|
|
const struct playlist_entry *pl_first = NULL;
|
|
playlist_t *pl =
|
|
state->playlists[pl_idx];
|
|
|
|
playlist_get_index(pl, 0, &pl_first);
|
|
|
|
if ( (pl_entry < pl_first)
|
|
|| (pl_entry >= pl_first + playlist_size(pl)))
|
|
continue;
|
|
|
|
/* Fake all the state so the content screen
|
|
* and information screen think we're viewing via playlist */
|
|
menu_displaylist_info_init(&info);
|
|
playlist_set_cached_external(pl);
|
|
menu->rpl_entry_selection_ptr = (unsigned)(pl_entry - pl_first);
|
|
strlcpy(menu->deferred_path,
|
|
pl_entry->path, sizeof(menu->deferred_path));
|
|
info.list = list;
|
|
menu_displaylist_ctl(DISPLAYLIST_HORIZONTAL_CONTENT_ACTIONS, &info,
|
|
settings);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return (unsigned)list->size;
|
|
}
|
|
|
|
uintptr_t menu_explore_get_entry_icon(unsigned type)
|
|
{
|
|
unsigned i;
|
|
if ( !explore_state
|
|
|| !explore_state->show_icons
|
|
|| (type < EXPLORE_TYPE_FIRSTITEM))
|
|
return 0;
|
|
|
|
i = (type - EXPLORE_TYPE_FIRSTITEM);
|
|
if (explore_state->show_icons == EXPLORE_ICONS_CONTENT)
|
|
{
|
|
explore_entry_t* e = &explore_state->entries[i];
|
|
if (e < RBUF_END(explore_state->entries))
|
|
return explore_state->icons[e->by[EXPLORE_BY_SYSTEM]->idx];
|
|
}
|
|
else if (explore_state->show_icons == EXPLORE_ICONS_SYSTEM_CATEGORY)
|
|
{
|
|
if (i < RBUF_LEN(explore_state->icons))
|
|
return explore_state->icons[i];
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
ssize_t menu_explore_get_entry_playlist_index(unsigned type,
|
|
playlist_t **playlist, const struct playlist_entry **playlist_entry,
|
|
file_list_t *list, size_t *list_pos, size_t *list_size)
|
|
{
|
|
int pl_idx;
|
|
explore_entry_t* entry;
|
|
|
|
if ( !explore_state
|
|
|| (type < EXPLORE_TYPE_FIRSTITEM)
|
|
|| (explore_state->show_icons != EXPLORE_ICONS_CONTENT))
|
|
return -1;
|
|
|
|
entry = &explore_state->entries[type - EXPLORE_TYPE_FIRSTITEM];
|
|
if ( (entry >= RBUF_END(explore_state->entries))
|
|
|| !entry->playlist_entry)
|
|
return -1;
|
|
|
|
for (pl_idx = 0; pl_idx != (int)RBUF_LEN(explore_state->playlists); pl_idx++)
|
|
{
|
|
const struct playlist_entry* pl_first = NULL;
|
|
playlist_t *pl = explore_state->playlists[pl_idx];
|
|
|
|
playlist_get_index(pl, 0, &pl_first);
|
|
|
|
if ( (entry->playlist_entry < pl_first)
|
|
|| (entry->playlist_entry >= pl_first + playlist_size(pl)))
|
|
continue;
|
|
|
|
if (playlist)
|
|
*playlist = pl;
|
|
if (playlist_entry)
|
|
*playlist_entry = entry->playlist_entry;
|
|
|
|
/* correct numbers of list pos and list size */
|
|
if (list && list_pos && list_size)
|
|
while (*list_size && list->list[list->size-*list_size].type < EXPLORE_TYPE_FIRSTITEM)
|
|
{ (*list_size)--; (*list_pos)--; }
|
|
|
|
/* Playlist needs to get cached for on-demand thumbnails */
|
|
playlist_set_cached_external(pl);
|
|
return (ssize_t)(entry->playlist_entry - pl_first);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
ssize_t menu_explore_set_playlist_thumbnail(unsigned type,
|
|
gfx_thumbnail_path_data_t *thumbnail_path_data)
|
|
{
|
|
const char *db_name;
|
|
explore_entry_t* entry;
|
|
ssize_t playlist_index = -1;
|
|
playlist_t *playlist = NULL;
|
|
|
|
if ( !explore_state
|
|
|| (type < EXPLORE_TYPE_FIRSTITEM)
|
|
|| (explore_state->show_icons != EXPLORE_ICONS_CONTENT))
|
|
return -1;
|
|
|
|
entry = &explore_state->entries[type - EXPLORE_TYPE_FIRSTITEM];
|
|
if (entry >= RBUF_END(explore_state->entries))
|
|
return -1;
|
|
|
|
db_name = entry->by[EXPLORE_BY_SYSTEM]->str;
|
|
if (!string_is_empty(db_name))
|
|
playlist_index = menu_explore_get_entry_playlist_index(
|
|
type, &playlist, NULL, NULL, NULL, NULL);
|
|
|
|
if (playlist_index >= 0 && playlist)
|
|
{
|
|
gfx_thumbnail_set_system(thumbnail_path_data, db_name, playlist);
|
|
gfx_thumbnail_set_content_playlist(thumbnail_path_data,
|
|
playlist, playlist_index);
|
|
return playlist_index;
|
|
}
|
|
|
|
gfx_thumbnail_set_content_playlist(thumbnail_path_data, NULL, 0);
|
|
return -1;
|
|
}
|
|
|
|
bool menu_explore_is_content_list(void)
|
|
{
|
|
struct menu_state *menu_st = menu_state_get_ptr();
|
|
menu_list_t *menu_list = menu_st->entries.list;
|
|
file_list_t *menu_stack = MENU_LIST_GET(menu_list, 0);
|
|
if (explore_state)
|
|
return (explore_state->show_icons == EXPLORE_ICONS_CONTENT);
|
|
return explore_get_view_path(menu_st, menu_list, menu_stack) != NULL;
|
|
}
|
|
|
|
void menu_explore_context_init(void)
|
|
{
|
|
if (explore_state)
|
|
explore_load_icons(explore_state);
|
|
}
|
|
|
|
void menu_explore_context_deinit(void)
|
|
{
|
|
if (explore_state)
|
|
explore_unload_icons(explore_state);
|
|
}
|
|
|
|
void menu_explore_free_state(explore_state_t *state)
|
|
{
|
|
unsigned i;
|
|
if (!state)
|
|
return;
|
|
for (i = 0; i != EXPLORE_CAT_COUNT; i++)
|
|
RBUF_FREE(state->by[i]);
|
|
|
|
RBUF_FREE(state->entries);
|
|
|
|
for (i = 0; i != RBUF_LEN(state->playlists); i++)
|
|
playlist_free(state->playlists[i]);
|
|
RBUF_FREE(state->playlists);
|
|
|
|
explore_unload_icons(state);
|
|
RBUF_FREE(state->icons);
|
|
|
|
ex_arena_free(&state->arena);
|
|
}
|
|
|
|
void menu_explore_free(void)
|
|
{
|
|
if (!explore_state)
|
|
return;
|
|
|
|
menu_explore_free_state(explore_state);
|
|
free(explore_state);
|
|
explore_state = NULL;
|
|
}
|
|
|
|
void menu_explore_set_state(explore_state_t *state)
|
|
{
|
|
if (!state)
|
|
return;
|
|
|
|
if (explore_state)
|
|
menu_explore_free();
|
|
|
|
/* needs to be done now on the main thread */
|
|
explore_load_icons(state);
|
|
|
|
explore_state = state;
|
|
}
|