mirror of
https://github.com/DarkMatterCore/nxdumptool.git
synced 2024-11-26 04:02:11 +00:00
[ci skip] DumpOptionsFrame: add GetOutputFilePath method
Other changes include: * DumpOptionsFrame: update contructors to also take a base output path string. * GameCardImageDumpOptionsFrame: simplify constructor by letting it take care of retrieving the title on its own. * nxdt_utils: remove utilsCreateOutputDirectories() function -- we'll be using utilsCreateDirectoryTree() anyway, so it's okay.
This commit is contained in:
parent
17ec42d812
commit
1181a95d17
9 changed files with 83 additions and 86 deletions
|
@ -152,10 +152,6 @@ void utilsGenerateFormattedSizeString(double size, char *dst, size_t dst_size);
|
||||||
/// Returns false if there's an error.
|
/// Returns false if there's an error.
|
||||||
bool utilsGetFileSystemStatsByPath(const char *path, u64 *out_total, u64 *out_free);
|
bool utilsGetFileSystemStatsByPath(const char *path, u64 *out_total, u64 *out_free);
|
||||||
|
|
||||||
/// Creates output directories in the specified device.
|
|
||||||
/// If 'device' is NULL, output directories will be created on the SD card.
|
|
||||||
void utilsCreateOutputDirectories(const char *device);
|
|
||||||
|
|
||||||
/// Returns true if a file exists.
|
/// Returns true if a file exists.
|
||||||
bool utilsCheckIfFileExists(const char *path);
|
bool utilsCheckIfFileExists(const char *path);
|
||||||
|
|
||||||
|
|
|
@ -81,12 +81,12 @@
|
||||||
#define HBMENU_BASE_PATH "/switch/"
|
#define HBMENU_BASE_PATH "/switch/"
|
||||||
#define APP_BASE_PATH HBMENU_BASE_PATH APP_TITLE "/"
|
#define APP_BASE_PATH HBMENU_BASE_PATH APP_TITLE "/"
|
||||||
|
|
||||||
#define GAMECARD_PATH APP_BASE_PATH "Gamecard/"
|
#define GAMECARD_SUBDIR "Gamecard"
|
||||||
#define HFS_PATH APP_BASE_PATH "HFS/"
|
#define HFS_SUBDIR "HFS"
|
||||||
#define NSP_PATH APP_BASE_PATH "NSP/"
|
#define NSP_SUBDIR "NSP"
|
||||||
#define TICKET_PATH APP_BASE_PATH "Ticket/"
|
#define TICKET_SUBDIR "Ticket"
|
||||||
#define NCA_PATH APP_BASE_PATH "NCA/"
|
#define NCA_SUBDIR "NCA"
|
||||||
#define NCA_FS_PATH APP_BASE_PATH "NCA FS/"
|
#define NCA_FS_SUBDIR "NCA FS"
|
||||||
|
|
||||||
#define CONFIG_FILE_NAME APP_TITLE "_config.json"
|
#define CONFIG_FILE_NAME APP_TITLE "_config.json"
|
||||||
#define DEFAULT_CONFIG_PATH "romfs:/default_config.json"
|
#define DEFAULT_CONFIG_PATH "romfs:/default_config.json"
|
||||||
|
|
|
@ -80,7 +80,7 @@ namespace nxdt::tasks
|
||||||
/* Runs in the background thread. */
|
/* Runs in the background thread. */
|
||||||
bool doInBackground(const std::string& path, const std::string& url, const bool& force_https) override final
|
bool doInBackground(const std::string& path, const std::string& url, const bool& force_https) override final
|
||||||
{
|
{
|
||||||
/* If the process fails or if it's cancelled, httpDownloadFile() will take care of closing the incomplete output file and delete it. */
|
/* If the process fails or if it's cancelled, httpDownloadFile() will take care of closing the incomplete output file and deleting it. */
|
||||||
return httpDownloadFile(path.c_str(), url.c_str(), force_https, DownloadFileTask::HttpProgressCallback, this);
|
return httpDownloadFile(path.c_str(), url.c_str(), force_https, DownloadFileTask::HttpProgressCallback, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ namespace nxdt::views
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
RootView *root_view = nullptr;
|
RootView *root_view = nullptr;
|
||||||
std::string raw_filename{}, extension{};
|
std::string storage_prefix{}, base_output_path{}, raw_filename{}, extension{};
|
||||||
|
|
||||||
brls::List *list = nullptr;
|
brls::List *list = nullptr;
|
||||||
brls::InputListItem *filename = nullptr;
|
brls::InputListItem *filename = nullptr;
|
||||||
|
@ -45,24 +45,23 @@ namespace nxdt::views
|
||||||
|
|
||||||
bool finalized = false;
|
bool finalized = false;
|
||||||
|
|
||||||
void Initialize(std::string& title, brls::Image *icon);
|
void Initialize(const std::string& title, brls::Image *icon);
|
||||||
|
|
||||||
std::string SanitizeUserFileName(void);
|
std::string SanitizeUserFileName(void);
|
||||||
|
|
||||||
void UpdateOutputStorages(const nxdt::tasks::UmsDeviceVector& ums_devices);
|
void UpdateOutputStorages(const nxdt::tasks::UmsDeviceVector& ums_devices);
|
||||||
|
void UpdateStoragePrefix(u32 selected);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
DumpOptionsFrame(RootView *root_view, std::string& title, std::string& raw_filename, std::string extension);
|
DumpOptionsFrame(RootView *root_view, const std::string& title, const std::string& base_output_path, const std::string& raw_filename, const std::string& extension);
|
||||||
DumpOptionsFrame(RootView *root_view, std::string& title, brls::Image *icon, std::string& raw_filename, std::string extension);
|
DumpOptionsFrame(RootView *root_view, const std::string& title, brls::Image *icon, const std::string& base_output_path, const std::string& raw_filename, const std::string& extension);
|
||||||
~DumpOptionsFrame();
|
~DumpOptionsFrame();
|
||||||
|
|
||||||
bool onCancel(void) override final;
|
bool onCancel(void) override final;
|
||||||
|
|
||||||
void addView(brls::View *view, bool fill = false);
|
void addView(brls::View *view, bool fill = false);
|
||||||
|
|
||||||
std::string GetFileName(void);
|
const std::string GetOutputFilePath(void);
|
||||||
|
|
||||||
std::string GetOutputStoragePrefix(void);
|
|
||||||
|
|
||||||
ALWAYS_INLINE brls::GenericEvent::Subscription RegisterButtonListener(brls::GenericEvent::Callback cb)
|
ALWAYS_INLINE brls::GenericEvent::Subscription RegisterButtonListener(brls::GenericEvent::Callback cb)
|
||||||
{
|
{
|
||||||
|
|
|
@ -38,7 +38,7 @@ namespace nxdt::views
|
||||||
brls::SelectListItem *checksum_lookup_method = nullptr;
|
brls::SelectListItem *checksum_lookup_method = nullptr;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
GameCardImageDumpOptionsFrame(RootView *root_view, std::string title, std::string raw_filename);
|
GameCardImageDumpOptionsFrame(RootView *root_view, std::string raw_filename);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -87,19 +87,6 @@ static const u32 g_sizeSuffixesCount = MAX_ELEMENTS(g_sizeSuffixes);
|
||||||
static const char g_illegalFileSystemChars[] = "\\/:*?\"<>|";
|
static const char g_illegalFileSystemChars[] = "\\/:*?\"<>|";
|
||||||
static const size_t g_illegalFileSystemCharsLength = (MAX_ELEMENTS(g_illegalFileSystemChars) - 1);
|
static const size_t g_illegalFileSystemCharsLength = (MAX_ELEMENTS(g_illegalFileSystemChars) - 1);
|
||||||
|
|
||||||
static const char *g_outputDirs[] = {
|
|
||||||
HBMENU_BASE_PATH,
|
|
||||||
APP_BASE_PATH,
|
|
||||||
GAMECARD_PATH,
|
|
||||||
HFS_PATH,
|
|
||||||
NSP_PATH,
|
|
||||||
TICKET_PATH,
|
|
||||||
NCA_PATH,
|
|
||||||
NCA_FS_PATH
|
|
||||||
};
|
|
||||||
|
|
||||||
static const size_t g_outputDirsCount = MAX_ELEMENTS(g_outputDirs);
|
|
||||||
|
|
||||||
static bool g_appUpdated = false;
|
static bool g_appUpdated = false;
|
||||||
|
|
||||||
static const SplConfigItem SplConfigItem_ExosphereApiVersion = (SplConfigItem)65000;
|
static const SplConfigItem SplConfigItem_ExosphereApiVersion = (SplConfigItem)65000;
|
||||||
|
@ -205,10 +192,6 @@ bool utilsInitializeResources(void)
|
||||||
|
|
||||||
LOG_MSG_INFO("Running under %s %s unit in %s mode.", g_isDevUnit ? "development" : "retail", utilsIsMarikoUnit() ? "Mariko" : "Erista", utilsIsAppletMode() ? "applet" : "title override");
|
LOG_MSG_INFO("Running under %s %s unit in %s mode.", g_isDevUnit ? "development" : "retail", utilsIsMarikoUnit() ? "Mariko" : "Erista", utilsIsAppletMode() ? "applet" : "title override");
|
||||||
|
|
||||||
/* Create output directories (SD card only). */
|
|
||||||
/* TODO: remove the APP_TITLE check whenever we're ready for a release. */
|
|
||||||
if (!strcasecmp(APP_TITLE, "nxdumptool")) utilsCreateOutputDirectories(NULL);
|
|
||||||
|
|
||||||
if (g_appLaunchPath)
|
if (g_appLaunchPath)
|
||||||
{
|
{
|
||||||
LOG_MSG_INFO("Launch path: \"%s\".", g_appLaunchPath);
|
LOG_MSG_INFO("Launch path: \"%s\".", g_appLaunchPath);
|
||||||
|
@ -217,6 +200,7 @@ bool utilsInitializeResources(void)
|
||||||
/* TODO: uncomment this block whenever we are ready for a release. */
|
/* TODO: uncomment this block whenever we are ready for a release. */
|
||||||
/*if (strcmp(g_appLaunchPath, NRO_PATH) != 0)
|
/*if (strcmp(g_appLaunchPath, NRO_PATH) != 0)
|
||||||
{
|
{
|
||||||
|
utilsCreateDirectoryTree(NRO_PATH, false);
|
||||||
remove(NRO_PATH);
|
remove(NRO_PATH);
|
||||||
rename(g_appLaunchPath, NRO_PATH);
|
rename(g_appLaunchPath, NRO_PATH);
|
||||||
|
|
||||||
|
@ -786,24 +770,6 @@ bool utilsGetFileSystemStatsByPath(const char *path, u64 *out_total, u64 *out_fr
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void utilsCreateOutputDirectories(const char *device)
|
|
||||||
{
|
|
||||||
size_t device_len = 0;
|
|
||||||
char path[FS_MAX_PATH] = {0};
|
|
||||||
|
|
||||||
if (device && (!(device_len = strlen(device)) || device[device_len - 1] != ':'))
|
|
||||||
{
|
|
||||||
LOG_MSG_ERROR("Invalid parameters!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for(size_t i = 0; i < g_outputDirsCount; i++)
|
|
||||||
{
|
|
||||||
sprintf(path, "%s%s", (device ? device : DEVOPTAB_SDMC_DEVICE), g_outputDirs[i]);
|
|
||||||
mkdir(path, 0744);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool utilsCheckIfFileExists(const char *path)
|
bool utilsCheckIfFileExists(const char *path)
|
||||||
{
|
{
|
||||||
if (!path || !*path) return false;
|
if (!path || !*path) return false;
|
||||||
|
|
|
@ -26,7 +26,8 @@ using namespace i18n::literals; /* For _i18n. */
|
||||||
|
|
||||||
namespace nxdt::views
|
namespace nxdt::views
|
||||||
{
|
{
|
||||||
DumpOptionsFrame::DumpOptionsFrame(RootView *root_view, std::string& title, std::string& raw_filename, std::string extension) : brls::ThumbnailFrame(), root_view(root_view), raw_filename(raw_filename), extension(extension)
|
DumpOptionsFrame::DumpOptionsFrame(RootView *root_view, const std::string& title, const std::string& base_output_path, const std::string& raw_filename, const std::string& extension) :
|
||||||
|
brls::ThumbnailFrame(), root_view(root_view), base_output_path(base_output_path), raw_filename(raw_filename), extension(extension)
|
||||||
{
|
{
|
||||||
/* Generate icon using the default image. */
|
/* Generate icon using the default image. */
|
||||||
brls::Image *icon = new brls::Image();
|
brls::Image *icon = new brls::Image();
|
||||||
|
@ -37,7 +38,8 @@ namespace nxdt::views
|
||||||
this->Initialize(title, icon);
|
this->Initialize(title, icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
DumpOptionsFrame::DumpOptionsFrame(RootView *root_view, std::string& title, brls::Image *icon, std::string& raw_filename, std::string extension) : brls::ThumbnailFrame(), root_view(root_view), raw_filename(raw_filename), extension(extension)
|
DumpOptionsFrame::DumpOptionsFrame(RootView *root_view, const std::string& title, brls::Image *icon, const std::string& base_output_path, const std::string& raw_filename, const std::string& extension) :
|
||||||
|
brls::ThumbnailFrame(), root_view(root_view), base_output_path(base_output_path), raw_filename(raw_filename), extension(extension)
|
||||||
{
|
{
|
||||||
/* Initialize the rest of the elements. */
|
/* Initialize the rest of the elements. */
|
||||||
this->Initialize(title, icon);
|
this->Initialize(title, icon);
|
||||||
|
@ -52,7 +54,7 @@ namespace nxdt::views
|
||||||
this->root_view->UnregisterUmsTaskListener(this->ums_task_sub);
|
this->root_view->UnregisterUmsTaskListener(this->ums_task_sub);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DumpOptionsFrame::Initialize(std::string& title, brls::Image *icon)
|
void DumpOptionsFrame::Initialize(const std::string& title, brls::Image *icon)
|
||||||
{
|
{
|
||||||
/* Set UI properties. */
|
/* Set UI properties. */
|
||||||
this->setTitle(title);
|
this->setTitle(title);
|
||||||
|
@ -86,11 +88,17 @@ namespace nxdt::views
|
||||||
|
|
||||||
/* Sanitize output filename for the selected storage. */
|
/* Sanitize output filename for the selected storage. */
|
||||||
this->filename->setValue(this->SanitizeUserFileName());
|
this->filename->setValue(this->SanitizeUserFileName());
|
||||||
|
|
||||||
|
/* Update the storage prefix. */
|
||||||
|
this->UpdateStoragePrefix(static_cast<u32>(selected));
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Manually update output storages vector. */
|
/* Manually update the output storages vector. */
|
||||||
this->UpdateOutputStorages(this->root_view->GetUmsDevices());
|
this->UpdateOutputStorages(this->root_view->GetUmsDevices());
|
||||||
|
|
||||||
|
/* Manually update the storage prefix. */
|
||||||
|
this->UpdateStoragePrefix(this->output_storage->getSelectedValue());
|
||||||
|
|
||||||
this->list->addView(this->output_storage);
|
this->list->addView(this->output_storage);
|
||||||
|
|
||||||
/* Subscribe to the UMS device event. */
|
/* Subscribe to the UMS device event. */
|
||||||
|
@ -180,6 +188,25 @@ namespace nxdt::views
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DumpOptionsFrame::UpdateStoragePrefix(u32 selected)
|
||||||
|
{
|
||||||
|
switch(selected)
|
||||||
|
{
|
||||||
|
case ConfigOutputStorage_SdCard:
|
||||||
|
this->storage_prefix = DEVOPTAB_SDMC_DEVICE "/";
|
||||||
|
break;
|
||||||
|
case ConfigOutputStorage_UsbHost:
|
||||||
|
this->storage_prefix = "/";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
const nxdt::tasks::UmsDeviceVector& ums_devices = this->root_view->GetUmsDevices();
|
||||||
|
this->storage_prefix = std::string(ums_devices.at(selected - ConfigOutputStorage_Count).first->name);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool DumpOptionsFrame::onCancel(void)
|
bool DumpOptionsFrame::onCancel(void)
|
||||||
{
|
{
|
||||||
/* Pop view. */
|
/* Pop view. */
|
||||||
|
@ -192,33 +219,35 @@ namespace nxdt::views
|
||||||
this->list->addView(view, fill);
|
this->list->addView(view, fill);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string DumpOptionsFrame::GetFileName(void)
|
const std::string DumpOptionsFrame::GetOutputFilePath(void)
|
||||||
{
|
{
|
||||||
return this->filename->getValue();
|
std::string output = this->storage_prefix;
|
||||||
|
u32 selected = this->output_storage->getSelectedValue();
|
||||||
|
char *sanitized_path = nullptr;
|
||||||
|
|
||||||
|
if (selected == ConfigOutputStorage_SdCard || selected >= ConfigOutputStorage_Count)
|
||||||
|
{
|
||||||
|
/* Remove the trailing path separator (if available) and append the application's base path if we're dealing with an SD card or a UMS device. */
|
||||||
|
if (output.back() == '/') output.pop_back();
|
||||||
|
output += APP_BASE_PATH;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string DumpOptionsFrame::GetOutputStoragePrefix(void)
|
/* Append a path separator, if needed. */
|
||||||
{
|
if (output.back() != '/' && this->base_output_path.front() != '/') output.push_back('/');
|
||||||
std::string prefix{};
|
|
||||||
|
|
||||||
u8 selected = static_cast<u8>(this->output_storage ? this->output_storage->getSelectedValue() : configGetInteger("output_storage"));
|
/* Append the base output path string. */
|
||||||
|
output += this->base_output_path;
|
||||||
|
|
||||||
switch(selected)
|
/* Generate the sanitized file path. */
|
||||||
{
|
sanitized_path = utilsGeneratePath(output.c_str(), this->filename->getValue().c_str(), this->extension.c_str());
|
||||||
case ConfigOutputStorage_SdCard:
|
if (!sanitized_path) throw fmt::format("Failed to generate sanitized file path.");
|
||||||
prefix = DEVOPTAB_SDMC_DEVICE "/";
|
|
||||||
break;
|
|
||||||
case ConfigOutputStorage_UsbHost:
|
|
||||||
prefix = "/";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
{
|
|
||||||
const nxdt::tasks::UmsDeviceVector& ums_devices = this->root_view->GetUmsDevices();
|
|
||||||
prefix = std::string(ums_devices.at(selected - ConfigOutputStorage_Count).first->name);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return prefix;
|
/* Update output. */
|
||||||
|
output = std::string(sanitized_path);
|
||||||
|
|
||||||
|
/* Free sanitized path. */
|
||||||
|
free(sanitized_path);
|
||||||
|
|
||||||
|
return output;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,8 @@ using namespace i18n::literals; /* For _i18n. */
|
||||||
|
|
||||||
namespace nxdt::views
|
namespace nxdt::views
|
||||||
{
|
{
|
||||||
GameCardImageDumpOptionsFrame::GameCardImageDumpOptionsFrame(RootView *root_view, std::string title, std::string raw_filename) : DumpOptionsFrame(root_view, title, raw_filename, ".xci")
|
GameCardImageDumpOptionsFrame::GameCardImageDumpOptionsFrame(RootView *root_view, std::string raw_filename) :
|
||||||
|
DumpOptionsFrame(root_view, "gamecard_tab/list/dump_card_image/label"_i18n, std::string(GAMECARD_SUBDIR), raw_filename, std::string(".xci"))
|
||||||
{
|
{
|
||||||
/* Prepend KeyArea data. */
|
/* Prepend KeyArea data. */
|
||||||
this->prepend_key_area = new brls::ToggleListItem("dump_options/prepend_key_area/label"_i18n, configGetBoolean("gamecard/prepend_key_area"), "dump_options/prepend_key_area/description"_i18n,
|
this->prepend_key_area = new brls::ToggleListItem("dump_options/prepend_key_area/label"_i18n, configGetBoolean("gamecard/prepend_key_area"), "dump_options/prepend_key_area/description"_i18n,
|
||||||
|
@ -116,10 +117,11 @@ namespace nxdt::views
|
||||||
/* Register dump button callback. */
|
/* Register dump button callback. */
|
||||||
this->RegisterButtonListener([this](brls::View *view) {
|
this->RegisterButtonListener([this](brls::View *view) {
|
||||||
/* Retrieve configuration values set by the user. */
|
/* Retrieve configuration values set by the user. */
|
||||||
//bool prepend_key_area = this->prepend_key_area_item->getToggleState();
|
//bool prepend_key_area_val = this->prepend_key_area->getToggleState();
|
||||||
//bool keep_certificate = this->keep_certificate_item->getToggleState();
|
//bool keep_certificate_val = this->keep_certificate->getToggleState();
|
||||||
bool trim_dump_val = this->trim_dump->getToggleState();
|
bool trim_dump_val = this->trim_dump->getToggleState();
|
||||||
//bool calculate_checksum = this->calculate_checksum_item->getToggleState();
|
//bool calculate_checksum_val = this->calculate_checksum->getToggleState();
|
||||||
|
//int checksum_lookup_method_val = static_cast<int>(this->checksum_lookup_method->getSelectedValue());
|
||||||
|
|
||||||
/* Get gamecard size. */
|
/* Get gamecard size. */
|
||||||
u64 gc_size = 0;
|
u64 gc_size = 0;
|
||||||
|
@ -132,6 +134,11 @@ namespace nxdt::views
|
||||||
/* Display update frame. */
|
/* Display update frame. */
|
||||||
//brls::Application::pushView(new OptionsTabUpdateApplicationFrame(), brls::ViewAnimation::SLIDE_LEFT, false);
|
//brls::Application::pushView(new OptionsTabUpdateApplicationFrame(), brls::ViewAnimation::SLIDE_LEFT, false);
|
||||||
brls::Application::notify(fmt::format("0x{:X}", gc_size));
|
brls::Application::notify(fmt::format("0x{:X}", gc_size));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
LOG_MSG_DEBUG("Output file path: %s", this->GetOutputFilePath().c_str());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -257,7 +257,7 @@ namespace nxdt::views
|
||||||
|
|
||||||
dump_card_image->getClickEvent()->subscribe([this](brls::View *view) {
|
dump_card_image->getClickEvent()->subscribe([this](brls::View *view) {
|
||||||
std::string& raw_filename = (configGetInteger("naming_convention") == TitleNamingConvention_Full ? raw_filename_full : raw_filename_id_only);
|
std::string& raw_filename = (configGetInteger("naming_convention") == TitleNamingConvention_Full ? raw_filename_full : raw_filename_id_only);
|
||||||
brls::Application::pushView(new GameCardImageDumpOptionsFrame(this->root_view, "gamecard_tab/list/dump_card_image/label"_i18n, raw_filename), brls::ViewAnimation::SLIDE_LEFT);
|
brls::Application::pushView(new GameCardImageDumpOptionsFrame(this->root_view, raw_filename), brls::ViewAnimation::SLIDE_LEFT);
|
||||||
});
|
});
|
||||||
|
|
||||||
this->list->addView(dump_card_image);
|
this->list->addView(dump_card_image);
|
||||||
|
|
Loading…
Reference in a new issue