mirror of
https://github.com/libretro/RetroArch
synced 2025-03-28 19:20:35 +00:00
Merge pull request #9045 from jdgleaver/playlist-reset-cores
Playlist Management: Add 'Reset Core Associations' option
This commit is contained in:
commit
024d75d568
@ -175,6 +175,7 @@ OBJ += frontend/frontend.o \
|
||||
tasks/task_file_transfer.o \
|
||||
tasks/task_image.o \
|
||||
tasks/task_audio_mixer.o \
|
||||
tasks/task_playlist_manager.o \
|
||||
$(LIBRETRO_COMM_DIR)/encodings/encoding_utf.o \
|
||||
$(LIBRETRO_COMM_DIR)/encodings/encoding_crc32.o \
|
||||
$(LIBRETRO_COMM_DIR)/encodings/encoding_base64.o \
|
||||
|
@ -1198,6 +1198,7 @@ DATA RUNLOOP
|
||||
#include "../tasks/task_save.c"
|
||||
#include "../tasks/task_image.c"
|
||||
#include "../tasks/task_file_transfer.c"
|
||||
#include "../tasks/task_playlist_manager.c"
|
||||
#ifdef HAVE_ZLIB
|
||||
#include "../tasks/task_decompress.c"
|
||||
#endif
|
||||
|
@ -873,6 +873,8 @@ MSG_HASH(MENU_ENUM_LABEL_PLAYLIST_MANAGER_SETTINGS,
|
||||
"playlist_manager_settings")
|
||||
MSG_HASH(MENU_ENUM_LABEL_PLAYLIST_MANAGER_DEFAULT_CORE,
|
||||
"playlist_manager_default_core")
|
||||
MSG_HASH(MENU_ENUM_LABEL_PLAYLIST_MANAGER_RESET_CORES,
|
||||
"playlist_manager_reset_cores")
|
||||
MSG_HASH(MENU_ENUM_LABEL_PLAYLIST_SETTINGS_BEGIN,
|
||||
"playlist_settings_begin")
|
||||
MSG_HASH(MENU_ENUM_LABEL_POINTER_ENABLE,
|
||||
|
@ -2164,6 +2164,22 @@ MSG_HASH(
|
||||
MENU_ENUM_SUBLABEL_PLAYLIST_MANAGER_DEFAULT_CORE,
|
||||
"Specify core to use when launching content via a playlist entry that does not have an existing core association."
|
||||
)
|
||||
MSG_HASH(
|
||||
MENU_ENUM_LABEL_VALUE_PLAYLIST_MANAGER_RESET_CORES,
|
||||
"Reset Core Associations"
|
||||
)
|
||||
MSG_HASH(
|
||||
MENU_ENUM_SUBLABEL_PLAYLIST_MANAGER_RESET_CORES,
|
||||
"Remove existing core associations for all playlist entries."
|
||||
)
|
||||
MSG_HASH(
|
||||
MSG_PLAYLIST_MANAGER_RESETTING_CORES,
|
||||
"Resetting cores: "
|
||||
)
|
||||
MSG_HASH(
|
||||
MSG_PLAYLIST_MANAGER_CORES_RESET,
|
||||
"Cores reset: "
|
||||
)
|
||||
MSG_HASH(
|
||||
MENU_ENUM_LABEL_VALUE_POINTER_ENABLE,
|
||||
"Touch Support"
|
||||
|
@ -5517,6 +5517,25 @@ static int action_ok_pl_entry_content_thumbnails(const char *path,
|
||||
}
|
||||
#endif
|
||||
|
||||
static int action_ok_playlist_reset_cores(const char *path,
|
||||
const char *label, unsigned type, size_t idx, size_t entry_idx)
|
||||
{
|
||||
playlist_t *playlist = playlist_get_cached();
|
||||
const char *playlist_path = NULL;
|
||||
|
||||
if (!playlist)
|
||||
return -1;
|
||||
|
||||
playlist_path = playlist_get_conf_path(playlist);
|
||||
|
||||
if (string_is_empty(playlist_path))
|
||||
return -1;
|
||||
|
||||
task_push_pl_manager_reset_cores(playlist_path);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int is_rdb_entry(enum msg_hash_enums enum_idx)
|
||||
{
|
||||
switch (enum_idx)
|
||||
@ -5868,6 +5887,9 @@ static int menu_cbs_init_bind_ok_compare_label(menu_file_list_cbs_t *cbs,
|
||||
case MENU_ENUM_LABEL_PLAYLIST_MANAGER_SETTINGS:
|
||||
BIND_ACTION_OK(cbs, action_ok_push_playlist_manager_settings);
|
||||
break;
|
||||
case MENU_ENUM_LABEL_PLAYLIST_MANAGER_RESET_CORES:
|
||||
BIND_ACTION_OK(cbs, action_ok_playlist_reset_cores);
|
||||
break;
|
||||
case MENU_ENUM_LABEL_RECORDING_SETTINGS:
|
||||
BIND_ACTION_OK(cbs, action_ok_push_recording_settings_list);
|
||||
break;
|
||||
|
@ -130,6 +130,7 @@ default_sublabel_macro(action_bind_sublabel_directory_settings_list, MENU_
|
||||
default_sublabel_macro(action_bind_sublabel_playlist_settings_list, MENU_ENUM_SUBLABEL_PLAYLIST_SETTINGS)
|
||||
default_sublabel_macro(action_bind_sublabel_playlist_manager_list, MENU_ENUM_SUBLABEL_PLAYLIST_MANAGER_LIST)
|
||||
default_sublabel_macro(action_bind_sublabel_playlist_manager_default_core, MENU_ENUM_SUBLABEL_PLAYLIST_MANAGER_DEFAULT_CORE)
|
||||
default_sublabel_macro(action_bind_sublabel_playlist_manager_reset_cores, MENU_ENUM_SUBLABEL_PLAYLIST_MANAGER_RESET_CORES)
|
||||
default_sublabel_macro(action_bind_sublabel_network_settings_list, MENU_ENUM_SUBLABEL_NETWORK_SETTINGS)
|
||||
default_sublabel_macro(action_bind_sublabel_network_on_demand_thumbnails, MENU_ENUM_SUBLABEL_NETWORK_ON_DEMAND_THUMBNAILS)
|
||||
default_sublabel_macro(action_bind_sublabel_user_settings_list, MENU_ENUM_SUBLABEL_USER_SETTINGS)
|
||||
@ -2391,6 +2392,9 @@ int menu_cbs_init_bind_sublabel(menu_file_list_cbs_t *cbs,
|
||||
case MENU_ENUM_LABEL_PLAYLIST_MANAGER_DEFAULT_CORE:
|
||||
BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_playlist_manager_default_core);
|
||||
break;
|
||||
case MENU_ENUM_LABEL_PLAYLIST_MANAGER_RESET_CORES:
|
||||
BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_playlist_manager_reset_cores);
|
||||
break;
|
||||
case MENU_ENUM_LABEL_USER_INTERFACE_SETTINGS:
|
||||
BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_user_interface_settings_list);
|
||||
break;
|
||||
|
@ -2504,7 +2504,8 @@ static void materialui_list_insert(void *userdata,
|
||||
node->texture_switch2_set = true;
|
||||
}
|
||||
else if (string_is_equal(label, msg_hash_to_str(MENU_ENUM_LABEL_RENAME_ENTRY)) ||
|
||||
string_is_equal(label, msg_hash_to_str(MENU_ENUM_LABEL_RESET_CORE_ASSOCIATION)))
|
||||
string_is_equal(label, msg_hash_to_str(MENU_ENUM_LABEL_RESET_CORE_ASSOCIATION)) ||
|
||||
string_is_equal(label, msg_hash_to_str(MENU_ENUM_LABEL_PLAYLIST_MANAGER_RESET_CORES)))
|
||||
{
|
||||
node->texture_switch2_index = MUI_TEXTURE_RENAME;
|
||||
node->texture_switch2_set = true;
|
||||
|
@ -40,6 +40,7 @@ menu_texture_item ozone_entries_icon_get_texture(ozone_handle_t *ozone,
|
||||
case MENU_ENUM_LABEL_ADD_TO_FAVORITES_PLAYLIST:
|
||||
return ozone->icons_textures[OZONE_ENTRIES_ICONS_TEXTURE_ADD_FAVORITE];
|
||||
case MENU_ENUM_LABEL_RESET_CORE_ASSOCIATION:
|
||||
case MENU_ENUM_LABEL_PLAYLIST_MANAGER_RESET_CORES:
|
||||
return ozone->icons_textures[OZONE_ENTRIES_ICONS_TEXTURE_UNDO];
|
||||
case MENU_ENUM_LABEL_CORE_INPUT_REMAPPING_OPTIONS:
|
||||
return ozone->icons_textures[OZONE_ENTRIES_ICONS_TEXTURE_INPUT_REMAPPING_OPTIONS];
|
||||
|
@ -2127,6 +2127,7 @@ static uintptr_t stripes_icon_get_id(stripes_handle_t *stripes,
|
||||
case MENU_ENUM_LABEL_ADD_TO_FAVORITES_PLAYLIST:
|
||||
return stripes->textures.list[STRIPES_TEXTURE_ADD_FAVORITE];
|
||||
case MENU_ENUM_LABEL_RESET_CORE_ASSOCIATION:
|
||||
case MENU_ENUM_LABEL_PLAYLIST_MANAGER_RESET_CORES:
|
||||
return stripes->textures.list[STRIPES_TEXTURE_RENAME];
|
||||
case MENU_ENUM_LABEL_CORE_INPUT_REMAPPING_OPTIONS:
|
||||
return stripes->textures.list[STRIPES_TEXTURE_INPUT_REMAPPING_OPTIONS];
|
||||
|
@ -2323,6 +2323,7 @@ static uintptr_t xmb_icon_get_id(xmb_handle_t *xmb,
|
||||
case MENU_ENUM_LABEL_UNDO_LOAD_STATE:
|
||||
case MENU_ENUM_LABEL_UNDO_SAVE_STATE:
|
||||
case MENU_ENUM_LABEL_RESET_CORE_ASSOCIATION:
|
||||
case MENU_ENUM_LABEL_PLAYLIST_MANAGER_RESET_CORES:
|
||||
return xmb->textures.list[XMB_TEXTURE_UNDO];
|
||||
case MENU_ENUM_LABEL_CORE_INPUT_REMAPPING_OPTIONS:
|
||||
return xmb->textures.list[XMB_TEXTURE_INPUT_REMAPPING_OPTIONS];
|
||||
|
@ -2631,8 +2631,14 @@ static bool menu_displaylist_parse_playlist_manager_settings(
|
||||
MENU_ENUM_LABEL_PLAYLIST_MANAGER_DEFAULT_CORE,
|
||||
MENU_SETTING_PLAYLIST_MANAGER_DEFAULT_CORE, 0, 0);
|
||||
|
||||
/* Reset core associations */
|
||||
menu_entries_append_enum(info->list,
|
||||
msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PLAYLIST_MANAGER_RESET_CORES),
|
||||
msg_hash_to_str(MENU_ENUM_LABEL_PLAYLIST_MANAGER_RESET_CORES),
|
||||
MENU_ENUM_LABEL_PLAYLIST_MANAGER_RESET_CORES,
|
||||
FILE_TYPE_PLAYLIST_ENTRY, 0, 0);
|
||||
|
||||
/* TODO: Add
|
||||
* - Reset core associations
|
||||
* - Remove invalid entries */
|
||||
|
||||
return true;
|
||||
|
@ -1815,6 +1815,11 @@ enum msg_hash_enums
|
||||
MENU_LABEL(PLAYLIST_MANAGER_LIST),
|
||||
MENU_LABEL(PLAYLIST_MANAGER_SETTINGS),
|
||||
MENU_LABEL(PLAYLIST_MANAGER_DEFAULT_CORE),
|
||||
MENU_LABEL(PLAYLIST_MANAGER_RESET_CORES),
|
||||
|
||||
MSG_PLAYLIST_MANAGER_RESETTING_CORES,
|
||||
MSG_PLAYLIST_MANAGER_CORES_RESET,
|
||||
|
||||
MENU_LABEL(CORE_UPDATER_SETTINGS),
|
||||
MENU_LABEL(LAKKA_SERVICES),
|
||||
MENU_LABEL(SHADER_APPLY_CHANGES),
|
||||
|
315
tasks/task_playlist_manager.c
Normal file
315
tasks/task_playlist_manager.c
Normal file
@ -0,0 +1,315 @@
|
||||
/* RetroArch - A frontend for libretro.
|
||||
* Copyright (C) 2011-2017 - Daniel De Matteis
|
||||
* Copyright (C) 2014-2017 - Jean-André Santoni
|
||||
* Copyright (C) 2016-2019 - Brad Parker
|
||||
*
|
||||
* 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 <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <ctype.h>
|
||||
|
||||
#include <string/stdstring.h>
|
||||
#include <file/file_path.h>
|
||||
|
||||
#include "tasks_internal.h"
|
||||
|
||||
#include "../msg_hash.h"
|
||||
#include "../file_path_special.h"
|
||||
#include "../playlist.h"
|
||||
|
||||
#ifndef COLLECTION_SIZE
|
||||
#define COLLECTION_SIZE 99999
|
||||
#endif
|
||||
|
||||
enum pl_manager_status
|
||||
{
|
||||
PL_MANAGER_BEGIN = 0,
|
||||
PL_MANAGER_ITERATE_ENTRY,
|
||||
PL_MANAGER_END
|
||||
};
|
||||
|
||||
typedef struct pl_manager_handle
|
||||
{
|
||||
char *playlist_path;
|
||||
char *playlist_name;
|
||||
playlist_t *playlist;
|
||||
size_t list_size;
|
||||
size_t list_index;
|
||||
enum pl_manager_status status;
|
||||
} pl_manager_handle_t;
|
||||
|
||||
/**************************/
|
||||
/* Reset Associated Cores */
|
||||
/**************************/
|
||||
|
||||
static void free_pl_manager_handle(pl_manager_handle_t *pl_manager)
|
||||
{
|
||||
if (!pl_manager)
|
||||
return;
|
||||
|
||||
if (!string_is_empty(pl_manager->playlist_path))
|
||||
{
|
||||
free(pl_manager->playlist_path);
|
||||
pl_manager->playlist_path = NULL;
|
||||
}
|
||||
|
||||
if (!string_is_empty(pl_manager->playlist_name))
|
||||
{
|
||||
free(pl_manager->playlist_name);
|
||||
pl_manager->playlist_name = NULL;
|
||||
}
|
||||
|
||||
if (pl_manager->playlist)
|
||||
{
|
||||
playlist_free(pl_manager->playlist);
|
||||
pl_manager->playlist = NULL;
|
||||
}
|
||||
|
||||
free(pl_manager);
|
||||
pl_manager = NULL;
|
||||
}
|
||||
|
||||
static void task_pl_manager_reset_cores_handler(retro_task_t *task)
|
||||
{
|
||||
pl_manager_handle_t *pl_manager = NULL;
|
||||
|
||||
if (!task)
|
||||
goto task_finished;
|
||||
|
||||
pl_manager = (pl_manager_handle_t*)task->state;
|
||||
|
||||
if (!pl_manager)
|
||||
goto task_finished;
|
||||
|
||||
if (task_get_cancelled(task))
|
||||
goto task_finished;
|
||||
|
||||
switch (pl_manager->status)
|
||||
{
|
||||
case PL_MANAGER_BEGIN:
|
||||
{
|
||||
/* Load playlist */
|
||||
if (!path_is_valid(pl_manager->playlist_path))
|
||||
goto task_finished;
|
||||
|
||||
pl_manager->playlist = playlist_init(pl_manager->playlist_path, COLLECTION_SIZE);
|
||||
|
||||
if (!pl_manager->playlist)
|
||||
goto task_finished;
|
||||
|
||||
pl_manager->list_size = playlist_size(pl_manager->playlist);
|
||||
|
||||
if (pl_manager->list_size < 1)
|
||||
goto task_finished;
|
||||
|
||||
/* All good - can start iterating */
|
||||
pl_manager->status = PL_MANAGER_ITERATE_ENTRY;
|
||||
}
|
||||
break;
|
||||
case PL_MANAGER_ITERATE_ENTRY:
|
||||
{
|
||||
const struct playlist_entry *entry = NULL;
|
||||
|
||||
/* Get current entry */
|
||||
playlist_get_index(
|
||||
pl_manager->playlist, pl_manager->list_index, &entry);
|
||||
|
||||
if (entry)
|
||||
{
|
||||
struct playlist_entry update_entry = {0};
|
||||
char task_title[PATH_MAX_LENGTH];
|
||||
char detect_string[PATH_MAX_LENGTH];
|
||||
|
||||
task_title[0] = '\0';
|
||||
detect_string[0] = '\0';
|
||||
|
||||
/* Update progress display */
|
||||
task_free_title(task);
|
||||
|
||||
strlcpy(
|
||||
task_title, msg_hash_to_str(MSG_PLAYLIST_MANAGER_RESETTING_CORES),
|
||||
sizeof(task_title));
|
||||
|
||||
if (!string_is_empty(entry->label))
|
||||
strlcat(task_title, entry->label, sizeof(task_title));
|
||||
else if (!string_is_empty(entry->path))
|
||||
{
|
||||
char entry_name[PATH_MAX_LENGTH];
|
||||
entry_name[0] = '\0';
|
||||
|
||||
fill_pathname_base_noext(entry_name, entry->path, sizeof(entry_name));
|
||||
strlcat(task_title, entry_name, sizeof(task_title));
|
||||
}
|
||||
|
||||
task_set_title(task, strdup(task_title));
|
||||
task_set_progress(task, (pl_manager->list_index * 100) / pl_manager->list_size);
|
||||
|
||||
/* Reset core association */
|
||||
strlcpy(detect_string, file_path_str(FILE_PATH_DETECT), sizeof(detect_string));
|
||||
|
||||
update_entry.core_path = detect_string;
|
||||
update_entry.core_name = detect_string;
|
||||
|
||||
playlist_update(
|
||||
pl_manager->playlist, pl_manager->list_index, &update_entry);
|
||||
}
|
||||
|
||||
/* Increment entry index */
|
||||
pl_manager->list_index++;
|
||||
if (pl_manager->list_index >= pl_manager->list_size)
|
||||
pl_manager->status = PL_MANAGER_END;
|
||||
}
|
||||
break;
|
||||
case PL_MANAGER_END:
|
||||
{
|
||||
playlist_t *cached_playlist = playlist_get_cached();
|
||||
char task_title[PATH_MAX_LENGTH];
|
||||
|
||||
task_title[0] = '\0';
|
||||
|
||||
/* Save playlist changes to disk */
|
||||
playlist_write_file(pl_manager->playlist);
|
||||
|
||||
/* If this is the currently cached playlist, then
|
||||
* it must be re-cached (otherwise changes will be
|
||||
* lost if the currently cached playlist is saved
|
||||
* to disk for any reason...) */
|
||||
if (cached_playlist)
|
||||
{
|
||||
if (string_is_equal(pl_manager->playlist_path, playlist_get_conf_path(cached_playlist)))
|
||||
{
|
||||
playlist_free_cached();
|
||||
playlist_init_cached(pl_manager->playlist_path, COLLECTION_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
/* Update progress display */
|
||||
task_free_title(task);
|
||||
|
||||
strlcpy(
|
||||
task_title, msg_hash_to_str(MSG_PLAYLIST_MANAGER_CORES_RESET),
|
||||
sizeof(task_title));
|
||||
strlcat(task_title, pl_manager->playlist_name, sizeof(task_title));
|
||||
|
||||
task_set_title(task, strdup(task_title));
|
||||
task_set_progress(task, 100);
|
||||
|
||||
goto task_finished;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
task_set_progress(task, 100);
|
||||
goto task_finished;
|
||||
break;
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
task_finished:
|
||||
|
||||
if (task)
|
||||
task_set_finished(task, true);
|
||||
|
||||
free_pl_manager_handle(pl_manager);
|
||||
}
|
||||
|
||||
static bool task_pl_manager_reset_cores_finder(retro_task_t *task, void *user_data)
|
||||
{
|
||||
pl_manager_handle_t *pl_manager = NULL;
|
||||
|
||||
if (!task || !user_data)
|
||||
return false;
|
||||
|
||||
if (task->handler != task_pl_manager_reset_cores_handler)
|
||||
return false;
|
||||
|
||||
pl_manager = (pl_manager_handle_t*)task->state;
|
||||
if (!pl_manager)
|
||||
return false;
|
||||
|
||||
return string_is_equal((const char*)user_data, pl_manager->playlist_path);
|
||||
}
|
||||
|
||||
bool task_push_pl_manager_reset_cores(const char *playlist_path)
|
||||
{
|
||||
task_finder_data_t find_data;
|
||||
char playlist_name[PATH_MAX_LENGTH];
|
||||
char task_title[PATH_MAX_LENGTH];
|
||||
retro_task_t *task = task_init();
|
||||
pl_manager_handle_t *pl_manager = (pl_manager_handle_t*)calloc(1, sizeof(pl_manager_handle_t));
|
||||
|
||||
playlist_name[0] = '\0';
|
||||
task_title[0] = '\0';
|
||||
|
||||
/* Sanity check */
|
||||
if (!task || !pl_manager)
|
||||
goto error;
|
||||
|
||||
if (string_is_empty(playlist_path))
|
||||
goto error;
|
||||
|
||||
fill_pathname_base_noext(playlist_name, playlist_path, sizeof(playlist_name));
|
||||
|
||||
if (string_is_empty(playlist_name))
|
||||
goto error;
|
||||
|
||||
/* Concurrent management of the same playlist
|
||||
* is not allowed */
|
||||
find_data.func = task_pl_manager_reset_cores_finder;
|
||||
find_data.userdata = (void*)playlist_path;
|
||||
|
||||
if (task_queue_find(&find_data))
|
||||
goto error;
|
||||
|
||||
/* Configure task */
|
||||
strlcpy(
|
||||
task_title, msg_hash_to_str(MSG_PLAYLIST_MANAGER_RESETTING_CORES),
|
||||
sizeof(task_title));
|
||||
strlcat(task_title, playlist_name, sizeof(task_title));
|
||||
|
||||
task->handler = task_pl_manager_reset_cores_handler;
|
||||
task->state = pl_manager;
|
||||
task->title = strdup(task_title);
|
||||
task->alternative_look = true;
|
||||
task->progress = 0;
|
||||
|
||||
/* Configure handle */
|
||||
pl_manager->playlist_path = strdup(playlist_path);
|
||||
pl_manager->playlist_name = strdup(playlist_name);
|
||||
pl_manager->playlist = NULL;
|
||||
pl_manager->list_size = 0;
|
||||
pl_manager->list_index = 0;
|
||||
pl_manager->status = PL_MANAGER_BEGIN;
|
||||
|
||||
task_queue_push(task);
|
||||
|
||||
return true;
|
||||
|
||||
error:
|
||||
|
||||
if (task)
|
||||
{
|
||||
free(task);
|
||||
task = NULL;
|
||||
}
|
||||
|
||||
if (pl_manager)
|
||||
{
|
||||
free(pl_manager);
|
||||
pl_manager = NULL;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
@ -73,6 +73,8 @@ bool task_push_pl_entry_thumbnail_download(
|
||||
|
||||
#endif
|
||||
|
||||
bool task_push_pl_manager_reset_cores(const char *playlist_path);
|
||||
|
||||
bool task_push_image_load(const char *fullpath,
|
||||
bool supports_rgba, unsigned upscale_threshold,
|
||||
retro_task_callback_t cb, void *userdata);
|
||||
|
Loading…
x
Reference in New Issue
Block a user