mirror of
https://github.com/HamletDuFromage/aio-switch-updater.git
synced 2024-12-29 10:56:01 +00:00
some more refactoring
This commit is contained in:
parent
39cc34a95d
commit
ca33039eb1
22 changed files with 255 additions and 325 deletions
|
@ -6,7 +6,7 @@ namespace JC {
|
|||
|
||||
int setColor(std::vector<int> colors);
|
||||
int backupToJSON(nlohmann::json &profiles, const char* path);
|
||||
std::tuple<std::vector<std::string>, std::vector<std::vector<int>>> getProfiles(const char* path);
|
||||
std::vector<std::pair<std::string, std::vector<int>>> getProfiles(const char* path);
|
||||
void changeJCColor(std::vector<int> values);
|
||||
nlohmann::json backupProfile();
|
||||
void backupJCColor(const char* path);
|
||||
|
@ -17,7 +17,7 @@ namespace PC {
|
|||
|
||||
int setColor(std::vector<int> colors);
|
||||
int backupToJSON(nlohmann::json &profiles, const char* path);
|
||||
std::tuple<std::vector<std::string>, std::vector<std::vector<int>>> getProfiles(const char* path);
|
||||
std::vector<std::pair<std::string, std::vector<int>>> getProfiles(const char* path);
|
||||
void changePCColor(std::vector<int> values);
|
||||
nlohmann::json backupProfile();
|
||||
void backupPCColor(const char* path);
|
||||
|
|
18
include/fs.hpp
Normal file
18
include/fs.hpp
Normal file
|
@ -0,0 +1,18 @@
|
|||
#pragma once
|
||||
|
||||
#include <switch.h>
|
||||
#include <set>
|
||||
#include <json.hpp>
|
||||
|
||||
namespace fs
|
||||
{
|
||||
|
||||
int removeDir(const char* path);
|
||||
nlohmann::json parseJsonFile(const char* path);
|
||||
void writeJsonToFile(nlohmann::json &data, const char* path);
|
||||
bool copyFile(const char *to, const char *from);
|
||||
std::string copyFiles(const char* path);
|
||||
void createTree(std::string path);
|
||||
std::set<std::string> readLineByLine(const char * path);
|
||||
|
||||
}
|
|
@ -12,36 +12,27 @@ namespace util {
|
|||
typedef char NsApplicationName[0x201];
|
||||
typedef uint8_t NsApplicationIcon[0x20000];
|
||||
|
||||
struct app
|
||||
{
|
||||
struct app {
|
||||
uint64_t tid;
|
||||
NsApplicationName name;
|
||||
NsApplicationIcon icon;
|
||||
brls::ListItem* listItem;
|
||||
};
|
||||
|
||||
void createTree(std::string path);
|
||||
void clearConsole();
|
||||
bool isArchive(const char * path);
|
||||
void downloadArchive(std::string url, archiveType type);
|
||||
void extractArchive(archiveType type, std::string tag = "0");
|
||||
std::string formatListItemTitle(const std::string &str, size_t maxScore = 140);
|
||||
std::string formatApplicationId(u64 ApplicationId);
|
||||
std::set<std::string> readLineByLine(const char * path);
|
||||
std::vector<std::string> fetchPayloads();
|
||||
void shut_down(bool reboot = false);
|
||||
void shutDown(bool reboot = false);
|
||||
int showDialogBox(std::string text, std::string opt);
|
||||
int showDialogBox(std::string text, std::string opt1, std::string opt2);
|
||||
std::string getLatestTag(const char *url);
|
||||
Result CopyFile(const char src_path[FS_MAX_PATH], const char dest_path[FS_MAX_PATH]);
|
||||
void saveVersion(std::string version, const char* path);
|
||||
std::string readVersion(const char* path);
|
||||
void cp(const char *to, const char *from);
|
||||
std::string copyFiles(const char* path);
|
||||
int removeDir(const char* path);
|
||||
bool isErista();
|
||||
void removeSysmodulesFlags(const char * directory);
|
||||
nlohmann::json parseJsonFile(const char* path);
|
||||
void writeJsonToFile(nlohmann::json &profiles, const char* path);
|
||||
|
||||
}
|
|
@ -33,12 +33,9 @@ JCPage::JCPage() : AppletFrame(true, true)
|
|||
list->addView(new brls::ListItemGroupSpacing(true));
|
||||
|
||||
auto profiles = JC::getProfiles(COLOR_PROFILES_PATH);
|
||||
std::vector<std::string> names = std::get<0>(profiles);
|
||||
int nbProfiles = names.size();
|
||||
for (int i = nbProfiles - 1; i >= 0; i--){
|
||||
std::string name = std::get<0>(profiles)[i];
|
||||
std::vector<int> value = std::get<1>(profiles)[i];
|
||||
listItem = new brls::ListItem(names[i]);
|
||||
for (int i = profiles.size() - 1; i >= 0; i--){
|
||||
std::vector<int> value = profiles[i].second;
|
||||
listItem = new brls::ListItem(profiles[i].first);
|
||||
listItem->getClickEvent()->subscribe([&, value](brls::View* view) {
|
||||
brls::StagedAppletFrame* stagedFrame = new brls::StagedAppletFrame();
|
||||
stagedFrame->setTitle("menus/joy_con/label"_i18n);
|
||||
|
|
|
@ -32,12 +32,9 @@ PCPage::PCPage() : AppletFrame(true, true)
|
|||
list->addView(new brls::ListItemGroupSpacing(true));
|
||||
|
||||
auto profiles = PC::getProfiles(PC_COLOR_PATH);
|
||||
std::vector<std::string> names = std::get<0>(profiles);
|
||||
int nbProfiles = names.size();
|
||||
for (int i = nbProfiles - 1; i >= 0; i--){
|
||||
std::string name = std::get<0>(profiles)[i];
|
||||
std::vector<int> value = std::get<1>(profiles)[i];
|
||||
listItem = new brls::ListItem(names[i]);
|
||||
for (int i = profiles.size() - 1; i >= 0; i--){
|
||||
std::vector<int> value = profiles[i].second;
|
||||
listItem = new brls::ListItem(profiles[i].first);
|
||||
listItem->getClickEvent()->subscribe([&, value](brls::View* view) {
|
||||
brls::StagedAppletFrame* stagedFrame = new brls::StagedAppletFrame();
|
||||
stagedFrame->setTitle("menus/pro_con/label"_i18n);
|
||||
|
|
|
@ -21,16 +21,15 @@ AmsTab::AmsTab() :
|
|||
operation += "menus/main/ams"_i18n;
|
||||
links = download::getLinks(AMS_URL);
|
||||
|
||||
int nbLinks = links.size();
|
||||
if(nbLinks){
|
||||
if(links.size()){
|
||||
auto hekate_link = download::getLinks(HEKATE_URL);
|
||||
std::string hekate_url = hekate_link[0].second;
|
||||
std::string text_hekate = "menus/common/download"_i18n + hekate_link[0].first;
|
||||
|
||||
for (int i = 0; i < nbLinks; i++){
|
||||
std::string url = links[i].second;
|
||||
std::string text("menus/common/download"_i18n + links[i].first + "menus/common/from"_i18n + url);
|
||||
listItem = new brls::ListItem(links[i].first);
|
||||
for (const auto& link : links){
|
||||
std::string url = link.second;
|
||||
std::string text("menus/common/download"_i18n + link.first + "menus/common/from"_i18n + url);
|
||||
listItem = new brls::ListItem(link.first);
|
||||
listItem->setHeight(LISTITEM_HEIGHT);
|
||||
listItem->getClickEvent()->subscribe([&, text, text_hekate, url, hekate_url, operation](brls::View* view) {
|
||||
brls::StagedAppletFrame* stagedFrame = new brls::StagedAppletFrame();
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
#include "app_page.hpp"
|
||||
#include <switch.h>
|
||||
#include "utils.hpp"
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include "current_cfw.hpp"
|
||||
#include "worker_page.hpp"
|
||||
#include "confirm_page.hpp"
|
||||
#include "download_cheats_page.hpp"
|
||||
#include "utils.hpp"
|
||||
#include "fs.hpp"
|
||||
|
||||
namespace i18n = brls::i18n;
|
||||
using namespace i18n::literals;
|
||||
|
@ -31,7 +32,7 @@ AppPage::AppPage(const bool cheatSlips) : AppletFrame(true, true)
|
|||
int recordCount = 0;
|
||||
size_t controlSize = 0;
|
||||
|
||||
titles = util::readLineByLine(UPDATED_TITLES_PATH);
|
||||
titles = fs::readLineByLine(UPDATED_TITLES_PATH);
|
||||
|
||||
if(!titles.empty() || cheatSlips){
|
||||
while (true)
|
||||
|
|
|
@ -4,11 +4,10 @@
|
|||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <filesystem>
|
||||
#include <tuple>
|
||||
|
||||
#include "constants.hpp"
|
||||
#include "progress_event.hpp"
|
||||
#include "utils.hpp"
|
||||
#include "fs.hpp"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
|
@ -83,7 +82,7 @@ namespace JC {
|
|||
{"R_BTN", ColorSwapper::BGRToHex(color_right.sub)}
|
||||
});
|
||||
profiles.push_back(newBackup);
|
||||
util::writeJsonToFile(profiles, path);
|
||||
fs::writeJsonToFile(profiles, path);
|
||||
return 0;
|
||||
}
|
||||
else{
|
||||
|
@ -109,17 +108,11 @@ namespace JC {
|
|||
|
||||
}
|
||||
|
||||
std::tuple<std::vector<std::string>, std::vector<std::vector<int>>> getProfiles(const char* path){
|
||||
std::vector<std::string> names;
|
||||
std::vector<std::vector<int>> colorValues;
|
||||
std::vector<std::pair<std::string, std::vector<int>>> getProfiles(const char* path){
|
||||
std::vector<std::pair<std::string, std::vector<int>>> res;
|
||||
bool properData;
|
||||
std::fstream profilesFile;
|
||||
json profilesJson;
|
||||
if(std::filesystem::exists(path)){
|
||||
profilesFile.open(path, std::fstream::in);
|
||||
profilesFile >> profilesJson;
|
||||
profilesFile.close();
|
||||
}
|
||||
json profilesJson = fs::parseJsonFile(path);
|
||||
if(profilesJson.empty()){
|
||||
profilesJson = {{
|
||||
{"L_BTN", "0A1E0A"},
|
||||
|
@ -128,7 +121,6 @@ namespace JC {
|
|||
{"R_JC", "96F5F5"},
|
||||
{"name", "Animal Crossing: New Horizons"}
|
||||
}};
|
||||
util::writeJsonToFile(profilesJson, path);
|
||||
}
|
||||
for (const auto& x : profilesJson.items()){
|
||||
std::string name = x.value()["name"];
|
||||
|
@ -146,16 +138,15 @@ namespace JC {
|
|||
}
|
||||
if(properData){
|
||||
if(name == "") name = "Unamed";
|
||||
names.push_back(name);
|
||||
colorValues.push_back({
|
||||
res.push_back(std::make_pair(name, (std::vector<int>){
|
||||
ColorSwapper::hexToBGR(values[0]),
|
||||
ColorSwapper::hexToBGR(values[1]),
|
||||
ColorSwapper::hexToBGR(values[2]),
|
||||
ColorSwapper::hexToBGR(values[3])
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
return std::make_tuple(names, colorValues);
|
||||
return res;
|
||||
}
|
||||
|
||||
void changeJCColor(std::vector<int> values){
|
||||
|
@ -204,7 +195,7 @@ namespace JC {
|
|||
|
||||
profiles.push_back(backup);
|
||||
//backup.push_back(profiles);
|
||||
util::writeJsonToFile(profiles, path);
|
||||
fs::writeJsonToFile(profiles, path);
|
||||
hiddbgExit();
|
||||
hidsysExit();
|
||||
ProgressEvent::instance().setStep(ProgressEvent::instance().getMax());
|
||||
|
@ -252,7 +243,7 @@ namespace PC {
|
|||
{"BTN", ColorSwapper::BGRToHex(color.sub)}
|
||||
});
|
||||
profiles.push_back(newBackup);
|
||||
util::writeJsonToFile(profiles, path);
|
||||
fs::writeJsonToFile(profiles, path);
|
||||
return 0;
|
||||
}
|
||||
else{
|
||||
|
@ -275,24 +266,17 @@ namespace PC {
|
|||
|
||||
}
|
||||
|
||||
std::tuple<std::vector<std::string>, std::vector<std::vector<int>>> getProfiles(const char* path){
|
||||
std::vector<std::string> names;
|
||||
std::vector<std::vector<int>> colorValues;
|
||||
std::vector<std::pair<std::string, std::vector<int>>> getProfiles(const char* path){
|
||||
std::vector<std::pair<std::string, std::vector<int>>> res;
|
||||
bool properData;
|
||||
std::fstream profilesFile;
|
||||
json profilesJson;
|
||||
if(std::filesystem::exists(path)){
|
||||
profilesFile.open(path, std::fstream::in);
|
||||
profilesFile >> profilesJson;
|
||||
profilesFile.close();
|
||||
}
|
||||
json profilesJson = fs::parseJsonFile(path);
|
||||
if(profilesJson.empty()){
|
||||
profilesJson = {{
|
||||
{"BTN", "2d2d2d"},
|
||||
{"BODY", "e6e6e6"},
|
||||
{"name", "Default black"}
|
||||
}};
|
||||
util::writeJsonToFile(profilesJson, path);
|
||||
}
|
||||
for (const auto& x : profilesJson.items()){
|
||||
std::string name = x.value()["name"];
|
||||
|
@ -308,14 +292,13 @@ namespace PC {
|
|||
}
|
||||
if(properData){
|
||||
if(name == "") name = "Unamed";
|
||||
names.push_back(name);
|
||||
colorValues.push_back({
|
||||
res.push_back(std::make_pair(name, (std::vector<int>){
|
||||
ColorSwapper::hexToBGR(values[0]),
|
||||
ColorSwapper::hexToBGR(values[1])
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
return std::make_tuple(names, colorValues);
|
||||
return res;
|
||||
}
|
||||
|
||||
void changePCColor(std::vector<int> values){
|
||||
|
@ -364,7 +347,7 @@ namespace PC {
|
|||
|
||||
profiles.push_back(backup);
|
||||
//backup.push_back(profiles);
|
||||
util::writeJsonToFile(profiles, path);
|
||||
fs::writeJsonToFile(profiles, path);
|
||||
hiddbgExit();
|
||||
hidsysExit();
|
||||
ProgressEvent::instance().setStep(ProgressEvent::instance().getMax());
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
#include "download_cheats_page.hpp"
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
#include "constants.hpp"
|
||||
#include "download.hpp"
|
||||
#include "utils.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "current_cfw.hpp"
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
|
||||
//#include <iostream>
|
||||
|
||||
namespace i18n = brls::i18n;
|
||||
|
@ -225,7 +227,7 @@ void DownloadCheatsPage::WriteCheats(uint64_t tid, std::string bid, std::string
|
|||
break;
|
||||
}
|
||||
path += tidstr + "/cheats/";
|
||||
util::createTree(path);
|
||||
fs::createTree(path);
|
||||
path += bid + ".txt";
|
||||
std::ofstream cheatFile;
|
||||
cheatFile.open(path, std::ios::app);
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
#include "download_payload_page.hpp"
|
||||
#include "utils.hpp"
|
||||
#include "confirm_page.hpp"
|
||||
#include "worker_page.hpp"
|
||||
#include "download.hpp"
|
||||
#include "utils.hpp"
|
||||
#include "fs.hpp"
|
||||
|
||||
namespace i18n = brls::i18n;
|
||||
using namespace i18n::literals;
|
||||
|
@ -18,15 +19,14 @@ DownloadPayloadPage::DownloadPayloadPage() : AppletFrame(true, true)
|
|||
list->addView(label);
|
||||
|
||||
auto links = download::getLinks(PAYLOAD_URL);
|
||||
int nbLinks = links.size();
|
||||
if(nbLinks){
|
||||
for (int i = 0; i<nbLinks; i++){
|
||||
std::string url = links[i].second;
|
||||
std::string path = std::string(BOOTLOADER_PL_PATH) + links[i].first;
|
||||
std::string text("menus/common/download"_i18n + links[i].first + "menus/common/from"_i18n + url);
|
||||
listItem = new brls::ListItem(links[i].first);
|
||||
if(links.size()){
|
||||
for (const auto& link : links){
|
||||
std::string url = link.second;
|
||||
std::string path = std::string(BOOTLOADER_PL_PATH) + link.first;
|
||||
std::string text("menus/common/download"_i18n + link.first + "menus/common/from"_i18n + url);
|
||||
listItem = new brls::ListItem(link.first);
|
||||
listItem->getClickEvent()->subscribe([&, text, url, path](brls::View* view) {
|
||||
util::createTree(BOOTLOADER_PL_PATH);
|
||||
fs::createTree(BOOTLOADER_PL_PATH);
|
||||
brls::StagedAppletFrame* stagedFrame = new brls::StagedAppletFrame();
|
||||
stagedFrame->setTitle("menus/getting_paylaod"_i18n);
|
||||
stagedFrame->addStage(
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
#include <algorithm>
|
||||
#include "extract.hpp"
|
||||
#include "utils.hpp"
|
||||
#include "fs.hpp"
|
||||
|
||||
namespace i18n = brls::i18n;
|
||||
using namespace i18n::literals;
|
||||
|
@ -29,7 +30,7 @@ ExcludePage::ExcludePage() : AppletFrame(true, true)
|
|||
int recordCount = 0;
|
||||
size_t controlSize = 0;
|
||||
|
||||
titles = util::readLineByLine(CHEATS_EXCLUDE);
|
||||
titles = fs::readLineByLine(CHEATS_EXCLUDE);
|
||||
|
||||
while (true)
|
||||
{
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
#include "extract.hpp"
|
||||
#include "utils.hpp"
|
||||
#include "download.hpp"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <sstream>
|
||||
|
@ -12,6 +10,9 @@
|
|||
#include <set>
|
||||
#include <unzipper.h>
|
||||
#include "progress_event.hpp"
|
||||
#include "utils.hpp"
|
||||
#include "download.hpp"
|
||||
#include "fs.hpp"
|
||||
|
||||
namespace i18n = brls::i18n;
|
||||
using namespace i18n::literals;
|
||||
|
@ -28,7 +29,7 @@ void extract(const char * filename, const char* workingPath, int overwriteInis){
|
|||
ProgressEvent::instance().reset();
|
||||
ProgressEvent::instance().setStep(1);
|
||||
chdir(workingPath);
|
||||
std::set<std::string> ignoreList = util::readLineByLine(FILES_IGNORE);
|
||||
std::set<std::string> ignoreList = fs::readLineByLine(FILES_IGNORE);
|
||||
std::set<std::string>::iterator it;
|
||||
zipper::Unzipper unzipper(filename);
|
||||
std::vector<zipper::ZipEntry> entries = unzipper.entries();
|
||||
|
@ -59,15 +60,11 @@ void extract(const char * filename, const char* workingPath, int overwriteInis){
|
|||
if(entries[i].name == "sept/payload.bin" || entries[i].name == "atmosphere/fusee-secondary.bin"){
|
||||
unzipper.extractEntry(entries[i].name, CONFIG_PATH_UNZIP);
|
||||
}
|
||||
else if(entries[i].name.substr(0, 13) == "hekate_ctcaer"){
|
||||
unzipper.extractEntry(entries[i].name);
|
||||
int c = 0;
|
||||
while(R_FAILED(util::CopyFile(("/" + entries[i].name).c_str(), UPDATE_BIN_PATH)) && c < 10){
|
||||
c++;
|
||||
}
|
||||
}
|
||||
else {
|
||||
unzipper.extractEntry(entries[i].name);
|
||||
if(entries[i].name.substr(0, 13) == "hekate_ctcaer") {
|
||||
fs::copyFile(("/" + entries[i].name).c_str(), UPDATE_BIN_PATH);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -81,7 +78,7 @@ void extract(const char * filename, const char* workingPath, const char* toExclu
|
|||
ProgressEvent::instance().reset();
|
||||
ProgressEvent::instance().setStep(1);
|
||||
chdir(workingPath);
|
||||
std::set<std::string> ignoreList = util::readLineByLine(FILES_IGNORE);
|
||||
std::set<std::string> ignoreList = fs::readLineByLine(FILES_IGNORE);
|
||||
ignoreList.insert(toExclude);
|
||||
std::set<std::string>::iterator it;
|
||||
zipper::Unzipper unzipper(filename);
|
||||
|
|
100
source/fs.cpp
Normal file
100
source/fs.cpp
Normal file
|
@ -0,0 +1,100 @@
|
|||
#include "fs.hpp"
|
||||
#include <borealis.hpp>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
#include "constants.hpp"
|
||||
|
||||
|
||||
namespace i18n = brls::i18n;
|
||||
using namespace i18n::literals;
|
||||
|
||||
namespace fs {
|
||||
|
||||
int removeDir(const char* path) {
|
||||
Result ret = 0;
|
||||
FsFileSystem *fs = fsdevGetDeviceFileSystem("sdmc");
|
||||
if (R_FAILED(ret = fsFsDeleteDirectoryRecursively(fs, path)))
|
||||
return ret;
|
||||
return 0;
|
||||
}
|
||||
|
||||
nlohmann::json parseJsonFile(const char* path) {
|
||||
std::ifstream file(path);
|
||||
|
||||
std::string fileContent((std::istreambuf_iterator<char>(file) ),
|
||||
(std::istreambuf_iterator<char>() ));
|
||||
|
||||
if(nlohmann::json::accept(fileContent)) return nlohmann::json::parse(fileContent);
|
||||
else return nlohmann::json::object();
|
||||
}
|
||||
|
||||
void writeJsonToFile(nlohmann::json &data, const char* path) {
|
||||
std::ofstream out(path);
|
||||
out << data.dump(4);
|
||||
out.close();
|
||||
}
|
||||
|
||||
bool copyFile(const char *from, const char *to){
|
||||
std::ifstream src(from, std::ios::binary);
|
||||
std::ofstream dst(to, std::ios::binary);
|
||||
|
||||
if (src.good() && dst.good()) {
|
||||
dst << src.rdbuf();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void createTree(std::string path){
|
||||
std::string delimiter = "/";
|
||||
size_t pos = 0;
|
||||
std::string token;
|
||||
std::string directories("");
|
||||
while ((pos = path.find(delimiter)) != std::string::npos) {
|
||||
token = path.substr(0, pos);
|
||||
directories += token + "/";
|
||||
std::filesystem::create_directory(directories);
|
||||
path.erase(0, pos + delimiter.length());
|
||||
}
|
||||
}
|
||||
|
||||
std::string copyFiles(const char* path) {
|
||||
nlohmann::ordered_json toMove;
|
||||
std::ifstream f(COPY_FILES_JSON);
|
||||
f >> toMove;
|
||||
f.close();
|
||||
std::string error = "";
|
||||
for (auto it = toMove.begin(); it != toMove.end(); ++it) {
|
||||
if(std::filesystem::exists(it.key())) {
|
||||
createTree(std::string(std::filesystem::path(it.value().get<std::string>()).parent_path()) + "/");
|
||||
copyFile(it.key().c_str(), it.value().get<std::string>().c_str());
|
||||
}
|
||||
else {
|
||||
error += it.key() + "\n";
|
||||
}
|
||||
}
|
||||
if(error == "") {
|
||||
error = "menus/common/all_done"_i18n;
|
||||
}
|
||||
else {
|
||||
error = "menus/tools/batch_copy_not_found"_i18n + error;
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
std::set<std::string> readLineByLine(const char * path){
|
||||
std::set<std::string> titles;
|
||||
std::string str;
|
||||
std::ifstream in(path);
|
||||
if(in){
|
||||
while (std::getline(in, str))
|
||||
{
|
||||
if(str.size() > 0)
|
||||
titles.insert(str);
|
||||
}
|
||||
in.close();
|
||||
}
|
||||
return titles;
|
||||
}
|
||||
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
#include <json.hpp>
|
||||
#include <fstream>
|
||||
#include "constants.hpp"
|
||||
#include "utils.hpp"
|
||||
#include "fs.hpp"
|
||||
|
||||
|
||||
namespace i18n = brls::i18n;
|
||||
|
@ -19,7 +19,7 @@ HideTabsPage::HideTabsPage() : AppletFrame(true, true) {
|
|||
);
|
||||
list->addView(label);
|
||||
|
||||
json hideStatus = util::parseJsonFile(HIDE_TABS_JSON);
|
||||
json hideStatus = fs::parseJsonFile(HIDE_TABS_JSON);
|
||||
|
||||
bool status = false;
|
||||
if(hideStatus.find("about") != hideStatus.end()) {
|
||||
|
@ -71,9 +71,7 @@ HideTabsPage::HideTabsPage() : AppletFrame(true, true) {
|
|||
updatedStatus["sigpatches"] = sigpatches->getToggleState();
|
||||
updatedStatus["firmwares"] = fws->getToggleState();
|
||||
updatedStatus["cheats"] = cheats->getToggleState();
|
||||
std::ofstream out(HIDE_TABS_JSON);
|
||||
out << updatedStatus.dump(4);
|
||||
out.close();
|
||||
fs::writeJsonToFile(updatedStatus, HIDE_TABS_JSON);
|
||||
brls::Application::popView();
|
||||
return true;
|
||||
});
|
||||
|
|
|
@ -78,12 +78,11 @@ ListDownloadTab::ListDownloadTab(const archiveType type) :
|
|||
|
||||
this->addView(description);
|
||||
|
||||
int nbLinks = links.size();
|
||||
if(nbLinks){
|
||||
for (int i = 0; i<nbLinks; i++){
|
||||
std::string url = links[i].second;
|
||||
std::string text("menus/common/download"_i18n + links[i].first + "menus/common/from"_i18n + url);
|
||||
listItem = new brls::ListItem(links[i].first);
|
||||
if(links.size()){
|
||||
for (const auto& link : links){
|
||||
std::string url = link.second;
|
||||
std::string text("menus/common/download"_i18n + link.first + "menus/common/from"_i18n + url);
|
||||
listItem = new brls::ListItem(link.first);
|
||||
listItem->setHeight(LISTITEM_HEIGHT);
|
||||
listItem->getClickEvent()->subscribe([&, text, url, type, operation](brls::View* view) {
|
||||
brls::StagedAppletFrame* stagedFrame = new brls::StagedAppletFrame();
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
//#include <stdio.h>
|
||||
//#include <stdlib.h>
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
#include <switch.h>
|
||||
#include <borealis.hpp>
|
||||
#include <json.hpp>
|
||||
#include "main_frame.hpp"
|
||||
#include "constants.hpp"
|
||||
#include "utils.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "current_cfw.hpp"
|
||||
#include "warning_page.hpp"
|
||||
#include <filesystem>
|
||||
#include "json.hpp"
|
||||
#include <constants.hpp>
|
||||
|
||||
|
||||
namespace i18n = brls::i18n;
|
||||
using namespace i18n::literals;
|
||||
|
@ -28,7 +25,7 @@ int main(int argc, char* argv[])
|
|||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
nlohmann::json languageFile = util::parseJsonFile(LANGUAGE_JSON);
|
||||
nlohmann::json languageFile = fs::parseJsonFile(LANGUAGE_JSON);
|
||||
if(languageFile.find("language") != languageFile.end())
|
||||
i18n::loadTranslations(languageFile["language"]);
|
||||
else
|
||||
|
@ -48,7 +45,7 @@ int main(int argc, char* argv[])
|
|||
splInitialize();
|
||||
romfsInit();
|
||||
|
||||
util::createTree(CONFIG_PATH);
|
||||
fs::createTree(CONFIG_PATH);
|
||||
|
||||
brls::Logger::setLogLevel(brls::LogLevel::DEBUG);
|
||||
brls::Logger::debug("Start");
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#include <json.hpp>
|
||||
#include <fstream>
|
||||
#include "utils.hpp"
|
||||
#include "fs.hpp"
|
||||
|
||||
namespace i18n = brls::i18n;
|
||||
using namespace i18n::literals;
|
||||
|
@ -22,7 +23,7 @@ MainFrame::MainFrame() : TabFrame()
|
|||
else
|
||||
this->setFooterText("v" + std::string(APP_VERSION));
|
||||
|
||||
json hideStatus = util::parseJsonFile(HIDE_TABS_JSON);
|
||||
json hideStatus = fs::parseJsonFile(HIDE_TABS_JSON);
|
||||
|
||||
bool erista = util::isErista();
|
||||
|
||||
|
|
|
@ -3,12 +3,11 @@
|
|||
#include <switch.h>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
#include <tuple>
|
||||
#include <iomanip>
|
||||
#include <json.hpp>
|
||||
#include "constants.hpp"
|
||||
#include "main_frame.hpp"
|
||||
#include <json.hpp>
|
||||
#include "utils.hpp"
|
||||
#include "fs.hpp"
|
||||
|
||||
namespace i18n = brls::i18n;
|
||||
using namespace i18n::literals;
|
||||
|
@ -23,8 +22,7 @@ NetPage::NetPage() : AppletFrame(true, true)
|
|||
nifmGetCurrentNetworkProfile (&profile);
|
||||
nifmExit();
|
||||
|
||||
int uuid = 0;
|
||||
for (int j = 0; j < 16; j++) uuid += int(profile.uuid.uuid[j]);
|
||||
int uuid = std::accumulate(profile.uuid.uuid, profile.uuid.uuid + 16, 0);
|
||||
|
||||
std::string labelText = "";
|
||||
if(uuid){
|
||||
|
@ -66,7 +64,7 @@ NetPage::NetPage() : AppletFrame(true, true)
|
|||
//dns_auto
|
||||
|
||||
if(uuid){
|
||||
json profiles = util::parseJsonFile(INTERNET_JSON);
|
||||
json profiles = fs::parseJsonFile(INTERNET_JSON);
|
||||
if(profiles.empty()) {
|
||||
profiles = {{
|
||||
{"name", "90DNS (Europe)"},
|
||||
|
|
|
@ -3,9 +3,11 @@
|
|||
#include "reboot_payload.h"
|
||||
#include "current_cfw.hpp"
|
||||
#include "utils.hpp"
|
||||
#include "fs.hpp"
|
||||
|
||||
namespace i18n = brls::i18n;
|
||||
using namespace i18n::literals;
|
||||
|
||||
PayloadPage::PayloadPage() : AppletFrame(true, true)
|
||||
{
|
||||
this->setTitle("menus/payloads/reboot_title"_i18n);
|
||||
|
@ -17,19 +19,18 @@ PayloadPage::PayloadPage() : AppletFrame(true, true)
|
|||
);
|
||||
list->addView(label);
|
||||
std::vector<std::string> payloads = util::fetchPayloads();
|
||||
int nbPayloads = payloads.size();
|
||||
for (int i = 0; i < nbPayloads; i++){
|
||||
std::string payload = payloads[i];
|
||||
listItem = new brls::ListItem(payload);
|
||||
for (const auto& payload : payloads){
|
||||
std::string payload_path = payload;
|
||||
listItem = new brls::ListItem(payload_path);
|
||||
listItem->getClickEvent()->subscribe([&, payload](brls::View* view) {
|
||||
reboot_to_payload(payload.c_str());
|
||||
brls::Application::popView();
|
||||
});
|
||||
if(CurrentCfw::running_cfw == CFW::ams){
|
||||
listItem->registerAction("menus/payloads/set_reboot_payload"_i18n, brls::Key::X, [this, payload] {
|
||||
listItem->registerAction("menus/payloads/set_reboot_payload"_i18n, brls::Key::X, [this, payload_path] {
|
||||
std::string res1;
|
||||
if(R_SUCCEEDED(util::CopyFile(payload.c_str(), REBOOT_PAYLOAD_PATH))){
|
||||
res1 += "menus/payloads/copy_success"_i18n + payload + "menus/payloads/to"_i18n + std::string(REBOOT_PAYLOAD_PATH) + "'.";
|
||||
if(fs::copyFile(payload_path.c_str(), REBOOT_PAYLOAD_PATH)){
|
||||
res1 += "menus/payloads/copy_success"_i18n + payload_path + "menus/payloads/to"_i18n + std::string(REBOOT_PAYLOAD_PATH) + "'.";
|
||||
|
||||
}
|
||||
else{
|
||||
|
@ -45,9 +46,9 @@ PayloadPage::PayloadPage() : AppletFrame(true, true)
|
|||
return true;
|
||||
});
|
||||
}
|
||||
listItem->registerAction("menus/payloads/set_reboot_payload_up"_i18n, brls::Key::Y, [this, payload] {
|
||||
listItem->registerAction("menus/payloads/set_reboot_payload"_i18n, brls::Key::Y, [this, payload] {
|
||||
std::string res2;
|
||||
if(R_SUCCEEDED(util::CopyFile(payload.c_str(), UPDATE_BIN_PATH))){
|
||||
if(fs::copyFile(payload.c_str(), UPDATE_BIN_PATH)){
|
||||
res2 += "menus/payloads/copy_success"_i18n + payload + "menus/payloads/to"_i18n + std::string(UPDATE_BIN_PATH) + "'.";
|
||||
}
|
||||
else{
|
||||
|
@ -68,14 +69,14 @@ PayloadPage::PayloadPage() : AppletFrame(true, true)
|
|||
|
||||
shutDown = new brls::ListItem("menus/common/shut_down"_i18n);
|
||||
shutDown->getClickEvent()->subscribe([](brls::View* view) {
|
||||
util::shut_down(false);
|
||||
util::shutDown(false);
|
||||
brls::Application::popView();
|
||||
});
|
||||
list->addView(shutDown);
|
||||
|
||||
reboot = new brls::ListItem("menus/payloads/reboot"_i18n);
|
||||
reboot->getClickEvent()->subscribe([](brls::View* view) {
|
||||
util::shut_down(true);
|
||||
util::shutDown(true);
|
||||
brls::Application::popView();
|
||||
});
|
||||
list->addView(reboot);
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include "net_page.hpp"
|
||||
#include "extract.hpp"
|
||||
#include "utils.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "hide_tabs_page.hpp"
|
||||
#include <json.hpp>
|
||||
#include <filesystem>
|
||||
|
@ -156,7 +157,7 @@ ToolsTab::ToolsTab(std::string tag, bool erista) : brls::List()
|
|||
chdir("/");
|
||||
std::string error = "";
|
||||
if(std::filesystem::exists(COPY_FILES_JSON)){
|
||||
error = util::copyFiles(COPY_FILES_JSON);
|
||||
error = fs::copyFiles(COPY_FILES_JSON);
|
||||
}
|
||||
else{
|
||||
error = "menus/tools/batch_copy_config_not_found"_i18n;
|
||||
|
@ -180,9 +181,9 @@ ToolsTab::ToolsTab(std::string tag, bool erista) : brls::List()
|
|||
std::filesystem::remove(FW_ZIP_PATH);
|
||||
std::filesystem::remove(CHEATS_ZIP_PATH);
|
||||
std::filesystem::remove(SIGPATCHES_ZIP_PATH);
|
||||
util::removeDir(AMS_DIRECTORY_PATH);
|
||||
util::removeDir(SEPT_DIRECTORY_PATH);
|
||||
util::removeDir(FW_DIRECTORY_PATH);
|
||||
fs::removeDir(AMS_DIRECTORY_PATH);
|
||||
fs::removeDir(SEPT_DIRECTORY_PATH);
|
||||
fs::removeDir(FW_DIRECTORY_PATH);
|
||||
brls::Dialog* dialog = new brls::Dialog("menus/common/all_done"_i18n);
|
||||
brls::GenericEvent::Callback callback = [dialog](brls::View* view) {
|
||||
dialog->close();
|
||||
|
|
170
source/utils.cpp
170
source/utils.cpp
|
@ -1,4 +1,5 @@
|
|||
#include "utils.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "current_cfw.hpp"
|
||||
#include <switch.h>
|
||||
#include "download.hpp"
|
||||
|
@ -13,19 +14,6 @@ using namespace i18n::literals;
|
|||
|
||||
namespace util {
|
||||
|
||||
void createTree(std::string path){
|
||||
std::string delimiter = "/";
|
||||
size_t pos = 0;
|
||||
std::string token;
|
||||
std::string directories("");
|
||||
while ((pos = path.find(delimiter)) != std::string::npos) {
|
||||
token = path.substr(0, pos);
|
||||
directories += token + "/";
|
||||
std::filesystem::create_directory(directories);
|
||||
path.erase(0, pos + delimiter.length());
|
||||
}
|
||||
}
|
||||
|
||||
bool isArchive(const char * path){
|
||||
std::fstream file;
|
||||
std::string fileContent;
|
||||
|
@ -38,7 +26,7 @@ bool isArchive(const char * path){
|
|||
}
|
||||
|
||||
void downloadArchive(std::string url, archiveType type){
|
||||
createTree(DOWNLOAD_PATH);
|
||||
fs::createTree(DOWNLOAD_PATH);
|
||||
AppletType at;
|
||||
switch(type){
|
||||
case archiveType::sigpatches:
|
||||
|
@ -147,13 +135,13 @@ void extractArchive(archiveType type, std::string tag){
|
|||
}
|
||||
else{
|
||||
if (std::filesystem::exists(FIRMWARE_PATH)) std::filesystem::remove_all(FIRMWARE_PATH);
|
||||
createTree(FIRMWARE_PATH);
|
||||
fs::createTree(FIRMWARE_PATH);
|
||||
extract::extract(FIRMWARE_FILENAME, FIRMWARE_PATH);
|
||||
}
|
||||
break;
|
||||
case archiveType::app:
|
||||
extract::extract(APP_FILENAME, CONFIG_PATH);
|
||||
cp(ROMFS_FORWARDER, FORWARDER_PATH);
|
||||
fs::copyFile(ROMFS_FORWARDER, FORWARDER_PATH);
|
||||
envSetNextLoad(FORWARDER_PATH, ("\"" + std::string(FORWARDER_PATH) + "\"").c_str());
|
||||
romfsExit();
|
||||
brls::Application::quit();
|
||||
|
@ -179,16 +167,14 @@ void extractArchive(archiveType type, std::string tag){
|
|||
break;
|
||||
}
|
||||
if(std::filesystem::exists(COPY_FILES_JSON))
|
||||
copyFiles(COPY_FILES_JSON);
|
||||
fs::copyFiles(COPY_FILES_JSON);
|
||||
}
|
||||
|
||||
std::string formatListItemTitle(const std::string &str, size_t maxScore) {
|
||||
size_t score = 0;
|
||||
for (size_t i = 0; i < str.length(); i++)
|
||||
{
|
||||
for (size_t i = 0; i < str.length(); i++) {
|
||||
score += std::isupper(str[i]) ? 4 : 3;
|
||||
if(score > maxScore)
|
||||
{
|
||||
if(score > maxScore) {
|
||||
return str.substr(0, i-1) + "\u2026";
|
||||
}
|
||||
}
|
||||
|
@ -199,21 +185,6 @@ std::string formatApplicationId(u64 ApplicationId){
|
|||
return fmt::format("{:016X}", ApplicationId);
|
||||
}
|
||||
|
||||
std::set<std::string> readLineByLine(const char * path){
|
||||
std::set<std::string> titles;
|
||||
std::string str;
|
||||
std::ifstream in(path);
|
||||
if(in){
|
||||
while (std::getline(in, str))
|
||||
{
|
||||
if(str.size() > 0)
|
||||
titles.insert(str);
|
||||
}
|
||||
in.close();
|
||||
}
|
||||
return titles;
|
||||
}
|
||||
|
||||
std::vector<std::string> fetchPayloads(){
|
||||
std::vector<std::string> payloadPaths;
|
||||
payloadPaths.push_back(ROOT_PATH);
|
||||
|
@ -224,7 +195,7 @@ std::vector<std::string> fetchPayloads(){
|
|||
if(std::filesystem::exists(BOOTLOADER_PL_PATH)) payloadPaths.push_back(BOOTLOADER_PL_PATH);
|
||||
if(std::filesystem::exists(SXOS_PATH)) payloadPaths.push_back(SXOS_PATH);
|
||||
std::vector<std::string> res;
|
||||
for (auto& path : payloadPaths){
|
||||
for (const auto& path : payloadPaths){
|
||||
for (const auto& entry : std::filesystem::directory_iterator(path)){
|
||||
if(entry.path().extension().string() == ".bin"){
|
||||
if(entry.path().string() != FUSEE_SECONDARY && entry.path().string() != FUSEE_MTC)
|
||||
|
@ -236,7 +207,7 @@ std::vector<std::string> fetchPayloads(){
|
|||
return res;
|
||||
}
|
||||
|
||||
void shut_down(bool reboot){
|
||||
void shutDown(bool reboot){
|
||||
bpcInitialize();
|
||||
if(reboot) bpcRebootSystem();
|
||||
else bpcShutdownSystem();
|
||||
|
@ -251,78 +222,6 @@ std::string getLatestTag(const char *url){
|
|||
return "";
|
||||
}
|
||||
|
||||
void cp(const char *from, const char *to){
|
||||
std::ifstream src(from, std::ios::binary);
|
||||
std::ofstream dst(to, std::ios::binary);
|
||||
|
||||
if (src.good() && dst.good()) {
|
||||
dst << src.rdbuf();
|
||||
}
|
||||
}
|
||||
|
||||
Result CopyFile(const char src_path[FS_MAX_PATH], const char dest_path[FS_MAX_PATH]) {
|
||||
FsFileSystem *fs;
|
||||
Result ret = 0;
|
||||
FsFile src_handle, dest_handle;
|
||||
int PREVIOUS_BROWSE_STATE = 0;
|
||||
FsFileSystem devices[4];
|
||||
devices[0] = *fsdevGetDeviceFileSystem("sdmc");
|
||||
fs = &devices[0];
|
||||
|
||||
ret = fsFsOpenFile(&devices[PREVIOUS_BROWSE_STATE], src_path, FsOpenMode_Read, &src_handle);
|
||||
if (R_FAILED(ret)) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
s64 size = 0;
|
||||
ret = fsFileGetSize(&src_handle, &size);
|
||||
if (R_FAILED(ret)){
|
||||
fsFileClose(&src_handle);
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::filesystem::remove(dest_path);
|
||||
fsFsCreateFile(fs, dest_path, size, 0);
|
||||
|
||||
ret = fsFsOpenFile(fs, dest_path, FsOpenMode_Write, &dest_handle);
|
||||
if (R_FAILED(ret)){
|
||||
fsFileClose(&src_handle);
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint64_t bytes_read = 0;
|
||||
const u64 buf_size = 0x10000;
|
||||
s64 offset = 0;
|
||||
unsigned char *buf = new unsigned char[buf_size];
|
||||
std::string filename = std::filesystem::path(src_path).filename();
|
||||
|
||||
do {
|
||||
std::memset(buf, 0, buf_size);
|
||||
|
||||
ret = fsFileRead(&src_handle, offset, buf, buf_size, FsReadOption_None, &bytes_read);
|
||||
if (R_FAILED(ret)) {
|
||||
delete[] buf;
|
||||
fsFileClose(&src_handle);
|
||||
fsFileClose(&dest_handle);
|
||||
return ret;
|
||||
}
|
||||
ret = fsFileWrite(&dest_handle, offset, buf, bytes_read, FsWriteOption_Flush);
|
||||
if (R_FAILED(ret)) {
|
||||
delete[] buf;
|
||||
fsFileClose(&src_handle);
|
||||
fsFileClose(&dest_handle);
|
||||
return ret;
|
||||
}
|
||||
|
||||
offset += bytes_read;
|
||||
} while(offset < size);
|
||||
|
||||
delete[] buf;
|
||||
fsFileClose(&src_handle);
|
||||
fsFileClose(&dest_handle);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void saveVersion(std::string version, const char* path){
|
||||
std::fstream newVersion;
|
||||
newVersion.open(path, std::fstream::out | std::fstream::trunc);
|
||||
|
@ -341,40 +240,6 @@ std::string readVersion(const char* path){
|
|||
return version;
|
||||
}
|
||||
|
||||
std::string copyFiles(const char* path) {
|
||||
nlohmann::ordered_json toMove;
|
||||
std::ifstream f(COPY_FILES_JSON);
|
||||
f >> toMove;
|
||||
f.close();
|
||||
std::string error = "";
|
||||
for (auto it = toMove.begin(); it != toMove.end(); ++it) {
|
||||
if(std::filesystem::exists(it.key())) {
|
||||
createTree(std::string(std::filesystem::path(it.value().get<std::string>()).parent_path()) + "/");
|
||||
cp(it.key().c_str(), it.value().get<std::string>().c_str());
|
||||
//std::cout << it.key() << it.value().get<std::string>() << std::endl;
|
||||
}
|
||||
else {
|
||||
error += it.key() + "\n";
|
||||
}
|
||||
}
|
||||
if(error == "") {
|
||||
error = "menus/common/all_done"_i18n;
|
||||
}
|
||||
else {
|
||||
error = "menus/tools/batch_copy_not_found"_i18n + error;
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
int removeDir(const char* path) {
|
||||
Result ret = 0;
|
||||
FsFileSystem *fs = fsdevGetDeviceFileSystem("sdmc");
|
||||
if (R_FAILED(ret = fsFsDeleteDirectoryRecursively(fs, path))) {
|
||||
return ret;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool isErista() {
|
||||
splInitialize();
|
||||
u64 hwType;
|
||||
|
@ -427,21 +292,4 @@ void removeSysmodulesFlags(const char * directory) {
|
|||
}
|
||||
}
|
||||
|
||||
nlohmann::json parseJsonFile(const char* path) {
|
||||
std::ifstream file(path);
|
||||
|
||||
std::string fileContent((std::istreambuf_iterator<char>(file) ),
|
||||
(std::istreambuf_iterator<char>() ));
|
||||
|
||||
if(nlohmann::json::accept(fileContent)) return nlohmann::json::parse(fileContent);
|
||||
else return nlohmann::json::object();
|
||||
}
|
||||
|
||||
void writeJsonToFile(nlohmann::json &profiles, const char* path) {
|
||||
std::fstream newProfiles;
|
||||
newProfiles.open(path, std::fstream::out | std::fstream::trunc);
|
||||
newProfiles << std::setw(4) << profiles << std::endl;
|
||||
newProfiles.close();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,15 +1,16 @@
|
|||
#include "warning_page.hpp"
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include "main_frame.hpp"
|
||||
#include "constants.hpp"
|
||||
#include "utils.hpp"
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include "fs.hpp"
|
||||
|
||||
namespace i18n = brls::i18n;
|
||||
using namespace i18n::literals;
|
||||
WarningPage::WarningPage(std::string text)
|
||||
{
|
||||
util::createTree(CONFIG_PATH);
|
||||
fs::createTree(CONFIG_PATH);
|
||||
std::ofstream(HIDDEN_AIO_FILE);
|
||||
this->button = (new brls::Button(brls::ButtonStyle::PRIMARY))->setLabel("menus/common/continue"_i18n);
|
||||
this->button->setParent(this);
|
||||
|
|
Loading…
Reference in a new issue