[desktop] Data Manager, data import/export (#2700)
This adds a "Data Manager" dialog to the Tools menu. The Data Manager allows for the following operations: - Open w/ system file manager - Clear - Export - Import On any of the following directories: - Save (w/ profile selector) - UserNAND - SysNAND - Mods - Shaders TODO for the future: - "Cleanup" for each directory - TitleID -> Game name--let users clean data for a specific game if applicable Signed-off-by: crueter <crueter@eden-emu.dev> Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/2700 Reviewed-by: MaranBr <maranbr@eden-emu.dev> Reviewed-by: Lizzie <lizzie@eden-emu.dev>pull/2735/head
-
45.reuse/dep5
-
BINdist/qt_themes/colorful/icons/256x256/plus_folder.png
-
BINdist/qt_themes/colorful/icons/48x48/download.png
-
BINdist/qt_themes/colorful/icons/48x48/upload.png
-
BINdist/qt_themes/colorful/icons/48x48/user-trash.png
-
3dist/qt_themes/colorful/style.qrc
-
3dist/qt_themes/colorful_midnight_blue/style.qrc
-
3dist/qt_themes/default/default.qrc
-
BINdist/qt_themes/default/icons/256x256/eden.png
-
BINdist/qt_themes/default/icons/48x48/download.png
-
BINdist/qt_themes/default/icons/48x48/upload.png
-
BINdist/qt_themes/default/icons/48x48/user-trash.png
-
3dist/qt_themes/default_dark/style.qrc
-
BINdist/qt_themes/qdarkstyle/icons/48x48/download.png
-
BINdist/qt_themes/qdarkstyle/icons/48x48/upload.png
-
BINdist/qt_themes/qdarkstyle/icons/48x48/user-trash.png
-
3dist/qt_themes/qdarkstyle/style.qrc
-
3dist/qt_themes/qdarkstyle_midnight_blue/style.qrc
-
BINsrc/android/app/src/main/legacy/drawable/ic_icon_bg.png
-
BINsrc/android/app/src/main/legacy/drawable/ic_icon_bg_orig.png
-
BINsrc/android/app/src/main/res/drawable/ic_icon_bg.png
-
BINsrc/android/app/src/main/res/drawable/ic_icon_bg_orig.png
-
4src/frontend_common/CMakeLists.txt
-
77src/frontend_common/data_manager.cpp
-
24src/frontend_common/data_manager.h
-
27src/qt_common/CMakeLists.txt
-
3src/qt_common/externals/CMakeLists.txt
-
354src/qt_common/qt_compress.cpp
-
71src/qt_common/qt_compress.h
-
246src/qt_common/qt_content_util.cpp
-
8src/qt_common/qt_content_util.h
-
11src/qt_common/qt_frontend_util.cpp
-
29src/qt_common/qt_frontend_util.h
-
8src/qt_common/qt_game_util.cpp
-
7src/qt_common/qt_path_util.cpp
-
38src/qt_common/qt_string_lookup.h
-
24src/yuzu/CMakeLists.txt
-
5src/yuzu/compatdb.cpp
-
2src/yuzu/configuration/configure_web.cpp
-
167src/yuzu/data_dialog.cpp
-
54src/yuzu/data_dialog.h
-
148src/yuzu/data_dialog.ui
-
205src/yuzu/data_widget.ui
-
17src/yuzu/main.cpp
-
1src/yuzu/main.h
-
30src/yuzu/main.ui
-
2src/yuzu/multiplayer/chat_room.cpp
-
2src/yuzu/multiplayer/client_room.cpp
-
2src/yuzu/multiplayer/direct_connect.cpp
-
2src/yuzu/multiplayer/host_room.cpp
-
2src/yuzu/multiplayer/lobby.cpp
|
Before Width: 256 | Height: 256 | Size: 3.5 KiB After Width: 256 | Height: 256 | Size: 3.5 KiB |
|
After Width: 48 | Height: 48 | Size: 1.4 KiB |
|
After Width: 48 | Height: 48 | Size: 1.4 KiB |
|
After Width: 48 | Height: 48 | Size: 1.4 KiB |
|
Before Width: 256 | Height: 256 | Size: 4.9 KiB After Width: 256 | Height: 256 | Size: 4.9 KiB |
|
After Width: 48 | Height: 48 | Size: 853 B |
|
After Width: 48 | Height: 48 | Size: 820 B |
|
After Width: 48 | Height: 48 | Size: 584 B |
|
After Width: 48 | Height: 48 | Size: 883 B |
|
After Width: 48 | Height: 48 | Size: 853 B |
|
After Width: 48 | Height: 48 | Size: 584 B |
|
Before Width: 768 | Height: 768 | Size: 38 KiB After Width: 768 | Height: 768 | Size: 37 KiB |
|
Before Width: 1949 | Height: 1949 | Size: 438 KiB After Width: 1949 | Height: 1949 | Size: 244 KiB |
|
Before Width: 768 | Height: 768 | Size: 21 KiB After Width: 768 | Height: 768 | Size: 21 KiB |
|
Before Width: 2598 | Height: 2598 | Size: 112 KiB After Width: 2598 | Height: 2598 | Size: 112 KiB |
@ -0,0 +1,77 @@ |
|||||
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
|
||||
|
#include "data_manager.h"
|
||||
|
#include "common/assert.h"
|
||||
|
#include "common/fs/path_util.h"
|
||||
|
#include <filesystem>
|
||||
|
#include <fmt/format.h>
|
||||
|
|
||||
|
namespace FrontendCommon::DataManager { |
||||
|
|
||||
|
namespace fs = std::filesystem; |
||||
|
|
||||
|
const std::string GetDataDir(DataDir dir, const std::string &user_id) |
||||
|
{ |
||||
|
const fs::path nand_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::NANDDir); |
||||
|
|
||||
|
switch (dir) { |
||||
|
case DataDir::Saves: |
||||
|
return (nand_dir / "user" / "save" / "0000000000000000" / user_id).string(); |
||||
|
case DataDir::UserNand: |
||||
|
return (nand_dir / "user" / "Contents" / "registered").string(); |
||||
|
case DataDir::SysNand: |
||||
|
// NB: do NOT delete save
|
||||
|
// that contains profile data and other stuff
|
||||
|
return (nand_dir / "system" / "Contents" / "registered").string(); |
||||
|
case DataDir::Mods: |
||||
|
return Common::FS::GetEdenPathString(Common::FS::EdenPath::LoadDir); |
||||
|
case DataDir::Shaders: |
||||
|
return Common::FS::GetEdenPathString(Common::FS::EdenPath::ShaderDir); |
||||
|
default: |
||||
|
UNIMPLEMENTED(); |
||||
|
} |
||||
|
|
||||
|
return ""; |
||||
|
} |
||||
|
|
||||
|
u64 ClearDir(DataDir dir, const std::string &user_id) |
||||
|
{ |
||||
|
fs::path data_dir = GetDataDir(dir, user_id); |
||||
|
u64 result = fs::remove_all(data_dir); |
||||
|
|
||||
|
// mkpath at the end just so it actually exists
|
||||
|
fs::create_directories(data_dir); |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
const std::string ReadableBytesSize(u64 size) |
||||
|
{ |
||||
|
static constexpr std::array units{"B", "KiB", "MiB", "GiB", "TiB", "PiB"}; |
||||
|
if (size == 0) { |
||||
|
return "0 B"; |
||||
|
} |
||||
|
|
||||
|
const int digit_groups = (std::min) (static_cast<int>(std::log10(size) / std::log10(1024)), |
||||
|
static_cast<int>(units.size())); |
||||
|
return fmt::format("{:.1f} {}", size / std::pow(1024, digit_groups), units[digit_groups]); |
||||
|
} |
||||
|
|
||||
|
u64 DataDirSize(DataDir dir) |
||||
|
{ |
||||
|
fs::path data_dir = GetDataDir(dir); |
||||
|
u64 size = 0; |
||||
|
|
||||
|
if (!fs::exists(data_dir)) |
||||
|
return 0; |
||||
|
|
||||
|
for (const auto& entry : fs::recursive_directory_iterator(data_dir)) { |
||||
|
if (!entry.is_directory()) { |
||||
|
size += entry.file_size(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return size; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project |
||||
|
// SPDX-License-Identifier: GPL-3.0-or-later |
||||
|
|
||||
|
#ifndef DATA_MANAGER_H |
||||
|
#define DATA_MANAGER_H |
||||
|
|
||||
|
#include "common/common_types.h" |
||||
|
#include <string> |
||||
|
|
||||
|
namespace FrontendCommon::DataManager { |
||||
|
|
||||
|
enum class DataDir { Saves, UserNand, SysNand, Mods, Shaders }; |
||||
|
|
||||
|
const std::string GetDataDir(DataDir dir, const std::string &user_id = ""); |
||||
|
|
||||
|
u64 ClearDir(DataDir dir, const std::string &user_id = ""); |
||||
|
|
||||
|
const std::string ReadableBytesSize(u64 size); |
||||
|
|
||||
|
u64 DataDirSize(DataDir dir); |
||||
|
|
||||
|
}; // namespace FrontendCommon::DataManager |
||||
|
|
||||
|
#endif // DATA_MANAGER_H |
||||
@ -0,0 +1,354 @@ |
|||||
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
|
||||
|
#include "qt_compress.h"
|
||||
|
#include "quazipfileinfo.h"
|
||||
|
|
||||
|
#include <QDirIterator>
|
||||
|
|
||||
|
/** This is a modified version of JlCompress **/ |
||||
|
namespace QtCommon::Compress { |
||||
|
|
||||
|
bool compressDir(QString fileCompressed, |
||||
|
QString dir, |
||||
|
const JlCompress::Options &options, |
||||
|
QtCommon::QtProgressCallback callback) |
||||
|
{ |
||||
|
// Create zip
|
||||
|
QuaZip zip(fileCompressed); |
||||
|
QDir().mkpath(QFileInfo(fileCompressed).absolutePath()); |
||||
|
if (!zip.open(QuaZip::mdCreate)) { |
||||
|
QFile::remove(fileCompressed); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// See how big the overall fs structure is
|
||||
|
// good approx. of total progress
|
||||
|
// TODO(crueter): QDirListing impl
|
||||
|
QDirIterator iter(dir, |
||||
|
QDir::NoDotAndDotDot | QDir::Hidden | QDir::Files, |
||||
|
QDirIterator::Subdirectories); |
||||
|
|
||||
|
std::size_t total = 0; |
||||
|
while (iter.hasNext()) { |
||||
|
total += iter.nextFileInfo().size(); |
||||
|
} |
||||
|
|
||||
|
std::size_t progress = 0; |
||||
|
callback(total, progress); |
||||
|
|
||||
|
// Add the files and subdirectories
|
||||
|
if (!compressSubDir(&zip, dir, dir, options, total, progress, callback)) { |
||||
|
QFile::remove(fileCompressed); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// Close zip
|
||||
|
zip.close(); |
||||
|
if (zip.getZipError() != 0) { |
||||
|
QFile::remove(fileCompressed); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
bool compressSubDir(QuaZip *zip, |
||||
|
QString dir, |
||||
|
QString origDir, |
||||
|
const JlCompress::Options &options, |
||||
|
std::size_t total, |
||||
|
std::size_t &progress, |
||||
|
QtProgressCallback callback) |
||||
|
{ |
||||
|
// zip: object where to add the file
|
||||
|
// dir: current real directory
|
||||
|
// origDir: original real directory
|
||||
|
// (path(dir)-path(origDir)) = path inside the zip object
|
||||
|
|
||||
|
if (!zip) |
||||
|
return false; |
||||
|
if (zip->getMode() != QuaZip::mdCreate && zip->getMode() != QuaZip::mdAppend |
||||
|
&& zip->getMode() != QuaZip::mdAdd) |
||||
|
return false; |
||||
|
|
||||
|
QDir directory(dir); |
||||
|
if (!directory.exists()) |
||||
|
return false; |
||||
|
|
||||
|
|
||||
|
QDir origDirectory(origDir); |
||||
|
if (dir != origDir) { |
||||
|
QuaZipFile dirZipFile(zip); |
||||
|
std::unique_ptr<QuaZipNewInfo> qzni; |
||||
|
if (options.getDateTime().isNull()) { |
||||
|
qzni = std::make_unique<QuaZipNewInfo>(origDirectory.relativeFilePath(dir) |
||||
|
+ QLatin1String("/"), |
||||
|
dir); |
||||
|
} else { |
||||
|
qzni = std::make_unique<QuaZipNewInfo>(origDirectory.relativeFilePath(dir) |
||||
|
+ QLatin1String("/"), |
||||
|
dir, |
||||
|
options.getDateTime()); |
||||
|
} |
||||
|
if (!dirZipFile.open(QIODevice::WriteOnly, *qzni, nullptr, 0, 0)) { |
||||
|
return false; |
||||
|
} |
||||
|
dirZipFile.close(); |
||||
|
} |
||||
|
|
||||
|
// For each subfolder
|
||||
|
QFileInfoList subfiles = directory.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot |
||||
|
| QDir::Hidden | QDir::Dirs); |
||||
|
for (const auto &file : std::as_const(subfiles)) { |
||||
|
if (!compressSubDir( |
||||
|
zip, file.absoluteFilePath(), origDir, options, total, progress, callback)) { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// For each file in directory
|
||||
|
QFileInfoList files = directory.entryInfoList(QDir::Hidden | QDir::Files); |
||||
|
for (const auto &file : std::as_const(files)) { |
||||
|
// If it's not a file or it's the compressed file being created
|
||||
|
if (!file.isFile() || file.absoluteFilePath() == zip->getZipName()) |
||||
|
continue; |
||||
|
|
||||
|
// Create relative name for the compressed file
|
||||
|
QString filename = origDirectory.relativeFilePath(file.absoluteFilePath()); |
||||
|
|
||||
|
// Compress the file
|
||||
|
if (!compressFile(zip, file.absoluteFilePath(), filename, options, total, progress, callback)) { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
bool compressFile(QuaZip *zip, |
||||
|
QString fileName, |
||||
|
QString fileDest, |
||||
|
const JlCompress::Options &options, |
||||
|
std::size_t total, |
||||
|
std::size_t &progress, |
||||
|
QtCommon::QtProgressCallback callback) |
||||
|
{ |
||||
|
// zip: object where to add the file
|
||||
|
// fileName: real file name
|
||||
|
// fileDest: file name inside the zip object
|
||||
|
|
||||
|
if (!zip) |
||||
|
return false; |
||||
|
if (zip->getMode() != QuaZip::mdCreate && zip->getMode() != QuaZip::mdAppend |
||||
|
&& zip->getMode() != QuaZip::mdAdd) |
||||
|
return false; |
||||
|
|
||||
|
QuaZipFile outFile(zip); |
||||
|
if (options.getDateTime().isNull()) { |
||||
|
if (!outFile.open(QIODevice::WriteOnly, |
||||
|
QuaZipNewInfo(fileDest, fileName), |
||||
|
nullptr, |
||||
|
0, |
||||
|
options.getCompressionMethod(), |
||||
|
options.getCompressionLevel())) |
||||
|
return false; |
||||
|
} else { |
||||
|
if (!outFile.open(QIODevice::WriteOnly, |
||||
|
QuaZipNewInfo(fileDest, fileName, options.getDateTime()), |
||||
|
nullptr, |
||||
|
0, |
||||
|
options.getCompressionMethod(), |
||||
|
options.getCompressionLevel())) |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
QFileInfo input(fileName); |
||||
|
if (quazip_is_symlink(input)) { |
||||
|
// Not sure if we should use any specialized codecs here.
|
||||
|
// After all, a symlink IS just a byte array. And
|
||||
|
// this is mostly for Linux, where UTF-8 is ubiquitous these days.
|
||||
|
QString path = quazip_symlink_target(input); |
||||
|
QString relativePath = input.dir().relativeFilePath(path); |
||||
|
outFile.write(QFile::encodeName(relativePath)); |
||||
|
} else { |
||||
|
QFile inFile; |
||||
|
inFile.setFileName(fileName); |
||||
|
if (!inFile.open(QIODevice::ReadOnly)) { |
||||
|
return false; |
||||
|
} |
||||
|
if (!copyData(inFile, outFile, total, progress, callback) || outFile.getZipError() != UNZ_OK) { |
||||
|
return false; |
||||
|
} |
||||
|
inFile.close(); |
||||
|
} |
||||
|
|
||||
|
outFile.close(); |
||||
|
return outFile.getZipError() == UNZ_OK; |
||||
|
} |
||||
|
|
||||
|
bool copyData(QIODevice &inFile, |
||||
|
QIODevice &outFile, |
||||
|
std::size_t total, |
||||
|
std::size_t &progress, |
||||
|
QtProgressCallback callback) |
||||
|
{ |
||||
|
while (!inFile.atEnd()) { |
||||
|
char buf[4096]; |
||||
|
qint64 readLen = inFile.read(buf, 4096); |
||||
|
if (readLen <= 0) |
||||
|
return false; |
||||
|
if (outFile.write(buf, readLen) != readLen) |
||||
|
return false; |
||||
|
|
||||
|
progress += readLen; |
||||
|
if (!callback(total, progress)) { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
QStringList extractDir(QString fileCompressed, QString dir, QtCommon::QtProgressCallback callback) |
||||
|
{ |
||||
|
// Open zip
|
||||
|
QuaZip zip(fileCompressed); |
||||
|
return extractDir(zip, dir, callback); |
||||
|
} |
||||
|
|
||||
|
QStringList extractDir(QuaZip &zip, const QString &dir, QtCommon::QtProgressCallback callback) |
||||
|
{ |
||||
|
if (!zip.open(QuaZip::mdUnzip)) { |
||||
|
return QStringList(); |
||||
|
} |
||||
|
QString cleanDir = QDir::cleanPath(dir); |
||||
|
QDir directory(cleanDir); |
||||
|
QString absCleanDir = directory.absolutePath(); |
||||
|
if (!absCleanDir.endsWith(QLatin1Char('/'))) // It only ends with / if it's the FS root.
|
||||
|
absCleanDir += QLatin1Char('/'); |
||||
|
QStringList extracted; |
||||
|
if (!zip.goToFirstFile()) { |
||||
|
return QStringList(); |
||||
|
} |
||||
|
|
||||
|
std::size_t total = 0; |
||||
|
for (const QuaZipFileInfo64 &info : zip.getFileInfoList64()) { |
||||
|
total += info.uncompressedSize; |
||||
|
} |
||||
|
|
||||
|
std::size_t progress = 0; |
||||
|
callback(total, progress); |
||||
|
|
||||
|
do { |
||||
|
QString name = zip.getCurrentFileName(); |
||||
|
QString absFilePath = directory.absoluteFilePath(name); |
||||
|
QString absCleanPath = QDir::cleanPath(absFilePath); |
||||
|
if (!absCleanPath.startsWith(absCleanDir)) |
||||
|
continue; |
||||
|
if (!extractFile(&zip, QLatin1String(""), absFilePath, total, progress, callback)) { |
||||
|
removeFile(extracted); |
||||
|
return QStringList(); |
||||
|
} |
||||
|
extracted.append(absFilePath); |
||||
|
} while (zip.goToNextFile()); |
||||
|
|
||||
|
// Close zip
|
||||
|
zip.close(); |
||||
|
if (zip.getZipError() != 0) { |
||||
|
removeFile(extracted); |
||||
|
return QStringList(); |
||||
|
} |
||||
|
|
||||
|
return extracted; |
||||
|
} |
||||
|
|
||||
|
bool extractFile(QuaZip *zip, |
||||
|
QString fileName, |
||||
|
QString fileDest, |
||||
|
std::size_t total, |
||||
|
std::size_t &progress, |
||||
|
QtCommon::QtProgressCallback callback) |
||||
|
{ |
||||
|
// zip: object where to add the file
|
||||
|
// filename: real file name
|
||||
|
// fileincompress: file name of the compressed file
|
||||
|
|
||||
|
if (!zip) |
||||
|
return false; |
||||
|
if (zip->getMode() != QuaZip::mdUnzip) |
||||
|
return false; |
||||
|
|
||||
|
if (!fileName.isEmpty()) |
||||
|
zip->setCurrentFile(fileName); |
||||
|
QuaZipFile inFile(zip); |
||||
|
if (!inFile.open(QIODevice::ReadOnly) || inFile.getZipError() != UNZ_OK) |
||||
|
return false; |
||||
|
|
||||
|
// Check existence of resulting file
|
||||
|
QDir curDir; |
||||
|
if (fileDest.endsWith(QLatin1String("/"))) { |
||||
|
if (!curDir.mkpath(fileDest)) { |
||||
|
return false; |
||||
|
} |
||||
|
} else { |
||||
|
if (!curDir.mkpath(QFileInfo(fileDest).absolutePath())) { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
QuaZipFileInfo64 info; |
||||
|
if (!zip->getCurrentFileInfo(&info)) |
||||
|
return false; |
||||
|
|
||||
|
QFile::Permissions srcPerm = info.getPermissions(); |
||||
|
if (fileDest.endsWith(QLatin1String("/")) && QFileInfo(fileDest).isDir()) { |
||||
|
if (srcPerm != 0) { |
||||
|
QFile(fileDest).setPermissions(srcPerm); |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
if (info.isSymbolicLink()) { |
||||
|
QString target = QFile::decodeName(inFile.readAll()); |
||||
|
return QFile::link(target, fileDest); |
||||
|
} |
||||
|
|
||||
|
// Open resulting file
|
||||
|
QFile outFile; |
||||
|
outFile.setFileName(fileDest); |
||||
|
if (!outFile.open(QIODevice::WriteOnly)) |
||||
|
return false; |
||||
|
|
||||
|
// Copy data
|
||||
|
if (!copyData(inFile, outFile, total, progress, callback) || inFile.getZipError() != UNZ_OK) { |
||||
|
outFile.close(); |
||||
|
removeFile(QStringList(fileDest)); |
||||
|
return false; |
||||
|
} |
||||
|
outFile.close(); |
||||
|
|
||||
|
// Close file
|
||||
|
inFile.close(); |
||||
|
if (inFile.getZipError() != UNZ_OK) { |
||||
|
removeFile(QStringList(fileDest)); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (srcPerm != 0) { |
||||
|
outFile.setPermissions(srcPerm); |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
bool removeFile(QStringList listFile) |
||||
|
{ |
||||
|
bool ret = true; |
||||
|
// For each file
|
||||
|
for (int i = 0; i < listFile.count(); i++) { |
||||
|
// Remove
|
||||
|
ret = ret && QFile::remove(listFile.at(i)); |
||||
|
} |
||||
|
return ret; |
||||
|
} |
||||
|
|
||||
|
} // namespace QtCommon::Compress
|
||||
@ -0,0 +1,71 @@ |
|||||
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project |
||||
|
// SPDX-License-Identifier: GPL-3.0-or-later |
||||
|
|
||||
|
#pragma once |
||||
|
|
||||
|
#include <QDir> |
||||
|
#include <QString> |
||||
|
#include <JlCompress.h> |
||||
|
#include "qt_common/qt_common.h" |
||||
|
|
||||
|
/** This is a modified version of JlCompress **/ |
||||
|
namespace QtCommon::Compress { |
||||
|
|
||||
|
/** |
||||
|
* @brief Compress an entire directory and report its progress. |
||||
|
* @param fileCompressed Destination file |
||||
|
* @param dir The directory to compress |
||||
|
* @param options Compression level, etc |
||||
|
* @param callback Callback that takes in two std::size_t (total, progress) and returns false if the current operation should be cancelled. |
||||
|
*/ |
||||
|
bool compressDir(QString fileCompressed, |
||||
|
QString dir, |
||||
|
const JlCompress::Options& options = JlCompress::Options(), |
||||
|
QtCommon::QtProgressCallback callback = {}); |
||||
|
|
||||
|
// Internal // |
||||
|
bool compressSubDir(QuaZip *zip, |
||||
|
QString dir, |
||||
|
QString origDir, |
||||
|
const JlCompress::Options &options, |
||||
|
std::size_t total, |
||||
|
std::size_t &progress, |
||||
|
QtCommon::QtProgressCallback callback); |
||||
|
|
||||
|
bool compressFile(QuaZip *zip, |
||||
|
QString fileName, |
||||
|
QString fileDest, |
||||
|
const JlCompress::Options &options, |
||||
|
std::size_t total, |
||||
|
std::size_t &progress, |
||||
|
QtCommon::QtProgressCallback callback); |
||||
|
|
||||
|
bool copyData(QIODevice &inFile, |
||||
|
QIODevice &outFile, |
||||
|
std::size_t total, |
||||
|
std::size_t &progress, |
||||
|
QtCommon::QtProgressCallback callback); |
||||
|
|
||||
|
// Extract // |
||||
|
|
||||
|
/** |
||||
|
* @brief Extract a zip file and report its progress. |
||||
|
* @param fileCompressed Compressed file |
||||
|
* @param dir The directory to push the results to |
||||
|
* @param callback Callback that takes in two std::size_t (total, progress) and returns false if the current operation should be cancelled. |
||||
|
*/ |
||||
|
QStringList extractDir(QString fileCompressed, QString dir, QtCommon::QtProgressCallback callback = {}); |
||||
|
|
||||
|
// Internal // |
||||
|
QStringList extractDir(QuaZip &zip, const QString &dir, QtCommon::QtProgressCallback callback); |
||||
|
|
||||
|
bool extractFile(QuaZip *zip, |
||||
|
QString fileName, |
||||
|
QString fileDest, |
||||
|
std::size_t total, |
||||
|
std::size_t &progress, |
||||
|
QtCommon::QtProgressCallback callback); |
||||
|
|
||||
|
bool removeFile(QStringList listFile); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project |
||||
|
// SPDX-License-Identifier: GPL-3.0-or-later |
||||
|
|
||||
|
#pragma once |
||||
|
|
||||
|
#include <QString> |
||||
|
#include <frozen/string.h> |
||||
|
#include <frozen/unordered_map.h> |
||||
|
#include <qobjectdefs.h> |
||||
|
#include <qtmetamacros.h> |
||||
|
|
||||
|
namespace QtCommon::StringLookup { |
||||
|
|
||||
|
Q_NAMESPACE |
||||
|
|
||||
|
// TODO(crueter): QML interface |
||||
|
enum StringKey { |
||||
|
SavesTooltip, |
||||
|
ShadersTooltip, |
||||
|
UserNandTooltip, |
||||
|
SysNandTooltip, |
||||
|
ModsTooltip, |
||||
|
}; |
||||
|
|
||||
|
static constexpr const frozen::unordered_map<StringKey, frozen::string, 5> strings = { |
||||
|
{SavesTooltip, "Contains game save data. DO NOT REMOVE UNLESS YOU KNOW WHAT YOU'RE DOING!"}, |
||||
|
{ShadersTooltip, "Contains Vulkan and OpenGL pipeline caches. Generally safe to remove."}, |
||||
|
{UserNandTooltip, "Contains updates and DLC for games."}, |
||||
|
{SysNandTooltip, "Contains firmware and applet data."}, |
||||
|
{ModsTooltip, "Contains game mods, patches, and cheats."}, |
||||
|
}; |
||||
|
|
||||
|
static inline const QString Lookup(StringKey key) |
||||
|
{ |
||||
|
return QString::fromStdString(strings.at(key).data()); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,167 @@ |
|||||
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
|
||||
|
#include "data_dialog.h"
|
||||
|
#include "core/hle/service/acc/profile_manager.h"
|
||||
|
#include "frontend_common/data_manager.h"
|
||||
|
#include "qt_common/qt_common.h"
|
||||
|
#include "qt_common/qt_content_util.h"
|
||||
|
#include "qt_common/qt_string_lookup.h"
|
||||
|
#include "ui_data_dialog.h"
|
||||
|
|
||||
|
#include <QDesktopServices>
|
||||
|
#include <QFileDialog>
|
||||
|
#include <QFutureWatcher>
|
||||
|
#include <QProgressDialog>
|
||||
|
#include <QtConcurrentRun>
|
||||
|
|
||||
|
#include <core/frontend/applets/profile_select.h>
|
||||
|
|
||||
|
#include <applets/qt_profile_select.h>
|
||||
|
|
||||
|
DataDialog::DataDialog(QWidget *parent) |
||||
|
: QDialog(parent) |
||||
|
, ui(std::make_unique<Ui::DataDialog>()) |
||||
|
{ |
||||
|
ui->setupUi(this); |
||||
|
|
||||
|
// TODO: Should we make this a single widget that pulls data from a model?
|
||||
|
#define WIDGET(name) \
|
||||
|
ui->page->addWidget(new DataWidget(FrontendCommon::DataManager::DataDir::name, \ |
||||
|
QtCommon::StringLookup::name##Tooltip, \ |
||||
|
QStringLiteral(#name), \ |
||||
|
this)); |
||||
|
|
||||
|
WIDGET(Saves) |
||||
|
WIDGET(Shaders) |
||||
|
WIDGET(UserNand) |
||||
|
WIDGET(SysNand) |
||||
|
WIDGET(Mods) |
||||
|
|
||||
|
#undef WIDGET
|
||||
|
|
||||
|
connect(ui->labels, &QListWidget::itemSelectionChanged, this, [this]() { |
||||
|
const auto items = ui->labels->selectedItems(); |
||||
|
if (items.isEmpty()) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
ui->page->setCurrentIndex(ui->labels->row(items[0])); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
DataDialog::~DataDialog() = default; |
||||
|
|
||||
|
DataWidget::DataWidget(FrontendCommon::DataManager::DataDir data_dir, |
||||
|
QtCommon::StringLookup::StringKey tooltip, |
||||
|
const QString &exportName, |
||||
|
QWidget *parent) |
||||
|
: QWidget(parent) |
||||
|
, ui(std::make_unique<Ui::DataWidget>()) |
||||
|
, m_dir(data_dir) |
||||
|
, m_exportName(exportName) |
||||
|
{ |
||||
|
ui->setupUi(this); |
||||
|
|
||||
|
ui->tooltip->setText(QtCommon::StringLookup::Lookup(tooltip)); |
||||
|
|
||||
|
ui->clear->setIcon(QIcon::fromTheme(QStringLiteral("user-trash"))); |
||||
|
ui->open->setIcon(QIcon::fromTheme(QStringLiteral("folder"))); |
||||
|
ui->upload->setIcon(QIcon::fromTheme(QStringLiteral("upload"))); |
||||
|
ui->download->setIcon(QIcon::fromTheme(QStringLiteral("download"))); |
||||
|
|
||||
|
connect(ui->clear, &QPushButton::clicked, this, &DataWidget::clear); |
||||
|
connect(ui->open, &QPushButton::clicked, this, &DataWidget::open); |
||||
|
connect(ui->upload, &QPushButton::clicked, this, &DataWidget::upload); |
||||
|
connect(ui->download, &QPushButton::clicked, this, &DataWidget::download); |
||||
|
|
||||
|
scan(); |
||||
|
} |
||||
|
|
||||
|
void DataWidget::clear() |
||||
|
{ |
||||
|
std::string user_id{}; |
||||
|
if (m_dir == FrontendCommon::DataManager::DataDir::Saves) { |
||||
|
user_id = selectProfile(); |
||||
|
} |
||||
|
QtCommon::Content::ClearDataDir(m_dir, user_id); |
||||
|
scan(); |
||||
|
} |
||||
|
|
||||
|
void DataWidget::open() |
||||
|
{ |
||||
|
std::string user_id{}; |
||||
|
if (m_dir == FrontendCommon::DataManager::DataDir::Saves) { |
||||
|
user_id = selectProfile(); |
||||
|
} |
||||
|
QDesktopServices::openUrl(QUrl::fromLocalFile( |
||||
|
QString::fromStdString(FrontendCommon::DataManager::GetDataDir(m_dir, user_id)))); |
||||
|
} |
||||
|
|
||||
|
void DataWidget::upload() |
||||
|
{ |
||||
|
std::string user_id{}; |
||||
|
if (m_dir == FrontendCommon::DataManager::DataDir::Saves) { |
||||
|
user_id = selectProfile(); |
||||
|
} |
||||
|
QtCommon::Content::ExportDataDir(m_dir, user_id, m_exportName); |
||||
|
} |
||||
|
|
||||
|
void DataWidget::download() |
||||
|
{ |
||||
|
std::string user_id{}; |
||||
|
if (m_dir == FrontendCommon::DataManager::DataDir::Saves) { |
||||
|
user_id = selectProfile(); |
||||
|
} |
||||
|
QtCommon::Content::ImportDataDir(m_dir, user_id, std::bind(&DataWidget::scan, this)); |
||||
|
} |
||||
|
|
||||
|
void DataWidget::scan() { |
||||
|
ui->size->setText(tr("Calculating...")); |
||||
|
|
||||
|
QFutureWatcher<u64> *watcher = new QFutureWatcher<u64>(this); |
||||
|
|
||||
|
connect(watcher, &QFutureWatcher<u64>::finished, this, [=, this]() { |
||||
|
u64 size = watcher->result(); |
||||
|
ui->size->setText( |
||||
|
QString::fromStdString(FrontendCommon::DataManager::ReadableBytesSize(size))); |
||||
|
watcher->deleteLater(); |
||||
|
}); |
||||
|
|
||||
|
watcher->setFuture( |
||||
|
QtConcurrent::run([this]() { return FrontendCommon::DataManager::DataDirSize(m_dir); })); |
||||
|
} |
||||
|
|
||||
|
std::string DataWidget::selectProfile() |
||||
|
{ |
||||
|
const auto select_profile = [this] { |
||||
|
const Core::Frontend::ProfileSelectParameters parameters{ |
||||
|
.mode = Service::AM::Frontend::UiMode::UserSelector, |
||||
|
.invalid_uid_list = {}, |
||||
|
.display_options = {}, |
||||
|
.purpose = Service::AM::Frontend::UserSelectionPurpose::General, |
||||
|
}; |
||||
|
QtProfileSelectionDialog dialog(*QtCommon::system, this, parameters); |
||||
|
dialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint |
||||
|
| Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint); |
||||
|
dialog.setWindowModality(Qt::WindowModal); |
||||
|
|
||||
|
if (dialog.exec() == QDialog::Rejected) { |
||||
|
return -1; |
||||
|
} |
||||
|
|
||||
|
return dialog.GetIndex(); |
||||
|
}; |
||||
|
|
||||
|
const auto index = select_profile(); |
||||
|
if (index == -1) { |
||||
|
return ""; |
||||
|
} |
||||
|
|
||||
|
const auto uuid = QtCommon::system->GetProfileManager().GetUser(static_cast<std::size_t>(index)); |
||||
|
ASSERT(uuid); |
||||
|
|
||||
|
const auto user_id = uuid->AsU128(); |
||||
|
|
||||
|
return fmt::format("{:016X}{:016X}", user_id[1], user_id[0]); |
||||
|
} |
||||
@ -0,0 +1,54 @@ |
|||||
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project |
||||
|
// SPDX-License-Identifier: GPL-3.0-or-later |
||||
|
|
||||
|
#ifndef DATA_DIALOG_H |
||||
|
#define DATA_DIALOG_H |
||||
|
|
||||
|
#include <QDialog> |
||||
|
#include "frontend_common/data_manager.h" |
||||
|
#include "qt_common/qt_string_lookup.h" |
||||
|
|
||||
|
#include "ui_data_widget.h" |
||||
|
|
||||
|
namespace Ui { |
||||
|
class DataDialog; |
||||
|
} |
||||
|
|
||||
|
class DataDialog : public QDialog |
||||
|
{ |
||||
|
Q_OBJECT |
||||
|
|
||||
|
public: |
||||
|
explicit DataDialog(QWidget *parent = nullptr); |
||||
|
~DataDialog(); |
||||
|
|
||||
|
private: |
||||
|
std::unique_ptr<Ui::DataDialog> ui; |
||||
|
}; |
||||
|
|
||||
|
class DataWidget : public QWidget |
||||
|
{ |
||||
|
Q_OBJECT |
||||
|
public: |
||||
|
explicit DataWidget(FrontendCommon::DataManager::DataDir data_dir, |
||||
|
QtCommon::StringLookup::StringKey tooltip, |
||||
|
const QString &exportName, |
||||
|
QWidget *parent = nullptr); |
||||
|
|
||||
|
public slots: |
||||
|
void clear(); |
||||
|
void open(); |
||||
|
void upload(); |
||||
|
void download(); |
||||
|
|
||||
|
void scan(); |
||||
|
|
||||
|
private: |
||||
|
std::unique_ptr<Ui::DataWidget> ui; |
||||
|
FrontendCommon::DataManager::DataDir m_dir; |
||||
|
const QString m_exportName; |
||||
|
|
||||
|
std::string selectProfile(); |
||||
|
}; |
||||
|
|
||||
|
#endif // DATA_DIALOG_H |
||||
@ -0,0 +1,148 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<ui version="4.0"> |
||||
|
<class>DataDialog</class> |
||||
|
<widget class="QDialog" name="DataDialog"> |
||||
|
<property name="geometry"> |
||||
|
<rect> |
||||
|
<x>0</x> |
||||
|
<y>0</y> |
||||
|
<width>480</width> |
||||
|
<height>320</height> |
||||
|
</rect> |
||||
|
</property> |
||||
|
<property name="sizePolicy"> |
||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Expanding"> |
||||
|
<horstretch>0</horstretch> |
||||
|
<verstretch>0</verstretch> |
||||
|
</sizepolicy> |
||||
|
</property> |
||||
|
<property name="minimumSize"> |
||||
|
<size> |
||||
|
<width>300</width> |
||||
|
<height>320</height> |
||||
|
</size> |
||||
|
</property> |
||||
|
<property name="windowTitle"> |
||||
|
<string>Data Manager</string> |
||||
|
</property> |
||||
|
<layout class="QVBoxLayout" name="verticalLayout"> |
||||
|
<item> |
||||
|
<layout class="QHBoxLayout" name="horizontalLayout" stretch="1,1"> |
||||
|
<item> |
||||
|
<widget class="QListWidget" name="labels"> |
||||
|
<property name="sizePolicy"> |
||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Expanding"> |
||||
|
<horstretch>0</horstretch> |
||||
|
<verstretch>0</verstretch> |
||||
|
</sizepolicy> |
||||
|
</property> |
||||
|
<item> |
||||
|
<property name="text"> |
||||
|
<string>Saves</string> |
||||
|
</property> |
||||
|
</item> |
||||
|
<item> |
||||
|
<property name="text"> |
||||
|
<string>Shaders</string> |
||||
|
</property> |
||||
|
</item> |
||||
|
<item> |
||||
|
<property name="text"> |
||||
|
<string>UserNAND</string> |
||||
|
</property> |
||||
|
</item> |
||||
|
<item> |
||||
|
<property name="text"> |
||||
|
<string>SysNAND</string> |
||||
|
</property> |
||||
|
</item> |
||||
|
<item> |
||||
|
<property name="text"> |
||||
|
<string>Mods</string> |
||||
|
</property> |
||||
|
</item> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item> |
||||
|
<widget class="QStackedWidget" name="page"> |
||||
|
<property name="sizePolicy"> |
||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Expanding"> |
||||
|
<horstretch>0</horstretch> |
||||
|
<verstretch>0</verstretch> |
||||
|
</sizepolicy> |
||||
|
</property> |
||||
|
<property name="minimumSize"> |
||||
|
<size> |
||||
|
<width>275</width> |
||||
|
<height>200</height> |
||||
|
</size> |
||||
|
</property> |
||||
|
<property name="currentIndex"> |
||||
|
<number>-1</number> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
</layout> |
||||
|
</item> |
||||
|
<item> |
||||
|
<layout class="QHBoxLayout" name="horizontalLayout_2"> |
||||
|
<property name="leftMargin"> |
||||
|
<number>10</number> |
||||
|
</property> |
||||
|
<item> |
||||
|
<widget class="QLabel" name="label"> |
||||
|
<property name="text"> |
||||
|
<string>Deleting ANY data is IRREVERSABLE!</string> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item> |
||||
|
<widget class="QDialogButtonBox" name="buttonBox"> |
||||
|
<property name="orientation"> |
||||
|
<enum>Qt::Orientation::Horizontal</enum> |
||||
|
</property> |
||||
|
<property name="standardButtons"> |
||||
|
<set>QDialogButtonBox::StandardButton::Ok</set> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
</layout> |
||||
|
</item> |
||||
|
</layout> |
||||
|
</widget> |
||||
|
<resources/> |
||||
|
<connections> |
||||
|
<connection> |
||||
|
<sender>buttonBox</sender> |
||||
|
<signal>accepted()</signal> |
||||
|
<receiver>DataDialog</receiver> |
||||
|
<slot>accept()</slot> |
||||
|
<hints> |
||||
|
<hint type="sourcelabel"> |
||||
|
<x>248</x> |
||||
|
<y>254</y> |
||||
|
</hint> |
||||
|
<hint type="destinationlabel"> |
||||
|
<x>157</x> |
||||
|
<y>274</y> |
||||
|
</hint> |
||||
|
</hints> |
||||
|
</connection> |
||||
|
<connection> |
||||
|
<sender>buttonBox</sender> |
||||
|
<signal>rejected()</signal> |
||||
|
<receiver>DataDialog</receiver> |
||||
|
<slot>reject()</slot> |
||||
|
<hints> |
||||
|
<hint type="sourcelabel"> |
||||
|
<x>316</x> |
||||
|
<y>260</y> |
||||
|
</hint> |
||||
|
<hint type="destinationlabel"> |
||||
|
<x>286</x> |
||||
|
<y>274</y> |
||||
|
</hint> |
||||
|
</hints> |
||||
|
</connection> |
||||
|
</connections> |
||||
|
</ui> |
||||
@ -0,0 +1,205 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<ui version="4.0"> |
||||
|
<class>DataWidget</class> |
||||
|
<widget class="QWidget" name="DataWidget"> |
||||
|
<property name="geometry"> |
||||
|
<rect> |
||||
|
<x>0</x> |
||||
|
<y>0</y> |
||||
|
<width>275</width> |
||||
|
<height>200</height> |
||||
|
</rect> |
||||
|
</property> |
||||
|
<property name="windowTitle"> |
||||
|
<string>Form</string> |
||||
|
</property> |
||||
|
<layout class="QGridLayout" name="gridLayout"> |
||||
|
<item row="0" column="0"> |
||||
|
<layout class="QHBoxLayout" name="horizontalLayout" stretch="3,2"> |
||||
|
<item> |
||||
|
<widget class="QLabel" name="tooltip"> |
||||
|
<property name="text"> |
||||
|
<string>Tooltip</string> |
||||
|
</property> |
||||
|
<property name="alignment"> |
||||
|
<set>Qt::AlignmentFlag::AlignCenter</set> |
||||
|
</property> |
||||
|
<property name="wordWrap"> |
||||
|
<bool>true</bool> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item> |
||||
|
<widget class="QLabel" name="size"> |
||||
|
<property name="font"> |
||||
|
<font> |
||||
|
<pointsize>10</pointsize> |
||||
|
<bold>true</bold> |
||||
|
</font> |
||||
|
</property> |
||||
|
<property name="text"> |
||||
|
<string notr="true">Size</string> |
||||
|
</property> |
||||
|
<property name="alignment"> |
||||
|
<set>Qt::AlignmentFlag::AlignCenter</set> |
||||
|
</property> |
||||
|
<property name="wordWrap"> |
||||
|
<bool>true</bool> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
</layout> |
||||
|
</item> |
||||
|
<item row="1" column="0"> |
||||
|
<layout class="QHBoxLayout" name="horizontalLayout_2"> |
||||
|
<item> |
||||
|
<widget class="QPushButton" name="open"> |
||||
|
<property name="sizePolicy"> |
||||
|
<sizepolicy hsizetype="Fixed" vsizetype="Fixed"> |
||||
|
<horstretch>1</horstretch> |
||||
|
<verstretch>1</verstretch> |
||||
|
</sizepolicy> |
||||
|
</property> |
||||
|
<property name="minimumSize"> |
||||
|
<size> |
||||
|
<width>52</width> |
||||
|
<height>47</height> |
||||
|
</size> |
||||
|
</property> |
||||
|
<property name="toolTip"> |
||||
|
<string>Open with your system file manager</string> |
||||
|
</property> |
||||
|
<property name="styleSheet"> |
||||
|
<string notr="true">QPushButton { |
||||
|
max-width: 50px; |
||||
|
max-height: 45px; |
||||
|
min-width: 50px; |
||||
|
min-height: 45px; |
||||
|
}</string> |
||||
|
</property> |
||||
|
<property name="text"> |
||||
|
<string/> |
||||
|
</property> |
||||
|
<property name="iconSize"> |
||||
|
<size> |
||||
|
<width>28</width> |
||||
|
<height>28</height> |
||||
|
</size> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item> |
||||
|
<widget class="QPushButton" name="clear"> |
||||
|
<property name="sizePolicy"> |
||||
|
<sizepolicy hsizetype="Fixed" vsizetype="Fixed"> |
||||
|
<horstretch>1</horstretch> |
||||
|
<verstretch>1</verstretch> |
||||
|
</sizepolicy> |
||||
|
</property> |
||||
|
<property name="minimumSize"> |
||||
|
<size> |
||||
|
<width>52</width> |
||||
|
<height>47</height> |
||||
|
</size> |
||||
|
</property> |
||||
|
<property name="toolTip"> |
||||
|
<string>Delete all data in this directory. THIS IS 100% IRREVERSABLE!</string> |
||||
|
</property> |
||||
|
<property name="styleSheet"> |
||||
|
<string notr="true">QPushButton { |
||||
|
max-width: 50px; |
||||
|
max-height: 45px; |
||||
|
min-width: 50px; |
||||
|
min-height: 45px; |
||||
|
}</string> |
||||
|
</property> |
||||
|
<property name="text"> |
||||
|
<string/> |
||||
|
</property> |
||||
|
<property name="iconSize"> |
||||
|
<size> |
||||
|
<width>28</width> |
||||
|
<height>28</height> |
||||
|
</size> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item> |
||||
|
<widget class="QPushButton" name="upload"> |
||||
|
<property name="sizePolicy"> |
||||
|
<sizepolicy hsizetype="Fixed" vsizetype="Fixed"> |
||||
|
<horstretch>1</horstretch> |
||||
|
<verstretch>1</verstretch> |
||||
|
</sizepolicy> |
||||
|
</property> |
||||
|
<property name="minimumSize"> |
||||
|
<size> |
||||
|
<width>52</width> |
||||
|
<height>47</height> |
||||
|
</size> |
||||
|
</property> |
||||
|
<property name="toolTip"> |
||||
|
<string>Export all data in this directory. This may take a while!</string> |
||||
|
</property> |
||||
|
<property name="styleSheet"> |
||||
|
<string notr="true">QPushButton { |
||||
|
max-width: 50px; |
||||
|
max-height: 45px; |
||||
|
min-width: 50px; |
||||
|
min-height: 45px; |
||||
|
}</string> |
||||
|
</property> |
||||
|
<property name="text"> |
||||
|
<string/> |
||||
|
</property> |
||||
|
<property name="iconSize"> |
||||
|
<size> |
||||
|
<width>28</width> |
||||
|
<height>28</height> |
||||
|
</size> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
<item> |
||||
|
<widget class="QPushButton" name="download"> |
||||
|
<property name="sizePolicy"> |
||||
|
<sizepolicy hsizetype="Fixed" vsizetype="Fixed"> |
||||
|
<horstretch>1</horstretch> |
||||
|
<verstretch>1</verstretch> |
||||
|
</sizepolicy> |
||||
|
</property> |
||||
|
<property name="minimumSize"> |
||||
|
<size> |
||||
|
<width>52</width> |
||||
|
<height>47</height> |
||||
|
</size> |
||||
|
</property> |
||||
|
<property name="toolTip"> |
||||
|
<string>Import data for this directory. This may take a while, and will delete ALL EXISTING DATA!</string> |
||||
|
</property> |
||||
|
<property name="styleSheet"> |
||||
|
<string notr="true">QPushButton { |
||||
|
max-width: 50px; |
||||
|
max-height: 45px; |
||||
|
min-width: 50px; |
||||
|
min-height: 45px; |
||||
|
}</string> |
||||
|
</property> |
||||
|
<property name="text"> |
||||
|
<string/> |
||||
|
</property> |
||||
|
<property name="iconSize"> |
||||
|
<size> |
||||
|
<width>28</width> |
||||
|
<height>28</height> |
||||
|
</size> |
||||
|
</property> |
||||
|
</widget> |
||||
|
</item> |
||||
|
</layout> |
||||
|
</item> |
||||
|
</layout> |
||||
|
</widget> |
||||
|
<resources/> |
||||
|
<connections/> |
||||
|
</ui> |
||||