mirror of
				https://github.com/PabloMK7/citra.git
				synced 2025-10-31 05:40:04 +00:00 
			
		
		
		
	citra_qt, core: game list "Open XXX Location" improvements
This commit is contained in:
		
							parent
							
								
									41688b2f2a
								
							
						
					
					
						commit
						bbf391abb9
					
				
					 11 changed files with 120 additions and 19 deletions
				
			
		|  | @ -27,6 +27,8 @@ | ||||||
| #include "citra_qt/ui_settings.h" | #include "citra_qt/ui_settings.h" | ||||||
| #include "common/common_paths.h" | #include "common/common_paths.h" | ||||||
| #include "common/logging/log.h" | #include "common/logging/log.h" | ||||||
|  | #include "core/file_sys/archive_extsavedata.h" | ||||||
|  | #include "core/file_sys/archive_source_sd_savedata.h" | ||||||
| #include "core/hle/service/fs/archive.h" | #include "core/hle/service/fs/archive.h" | ||||||
| #include "core/loader/loader.h" | #include "core/loader/loader.h" | ||||||
| 
 | 
 | ||||||
|  | @ -409,7 +411,9 @@ void GameList::PopupContextMenu(const QPoint& menu_location) { | ||||||
|     QMenu context_menu; |     QMenu context_menu; | ||||||
|     switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) { |     switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) { | ||||||
|     case GameListItemType::Game: |     case GameListItemType::Game: | ||||||
|         AddGamePopup(context_menu, selected.data(GameListItemPath::ProgramIdRole).toULongLong()); |         AddGamePopup(context_menu, selected.data(GameListItemPath::FullPathRole).toString(), | ||||||
|  |                      selected.data(GameListItemPath::ProgramIdRole).toULongLong(), | ||||||
|  |                      selected.data(GameListItemPath::ExtdataIdRole).toULongLong()); | ||||||
|         break; |         break; | ||||||
|     case GameListItemType::CustomDir: |     case GameListItemType::CustomDir: | ||||||
|         AddPermDirPopup(context_menu, selected); |         AddPermDirPopup(context_menu, selected); | ||||||
|  | @ -423,23 +427,46 @@ void GameList::PopupContextMenu(const QPoint& menu_location) { | ||||||
|     context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location)); |     context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void GameList::AddGamePopup(QMenu& context_menu, u64 program_id) { | void GameList::AddGamePopup(QMenu& context_menu, const QString& path, u64 program_id, | ||||||
|  |                             u64 extdata_id) { | ||||||
|     QAction* open_save_location = context_menu.addAction(tr("Open Save Data Location")); |     QAction* open_save_location = context_menu.addAction(tr("Open Save Data Location")); | ||||||
|  |     QAction* open_extdata_location = context_menu.addAction(tr("Open Extra Data Location")); | ||||||
|     QAction* open_application_location = context_menu.addAction(tr("Open Application Location")); |     QAction* open_application_location = context_menu.addAction(tr("Open Application Location")); | ||||||
|     QAction* open_update_location = context_menu.addAction(tr("Open Update Data Location")); |     QAction* open_update_location = context_menu.addAction(tr("Open Update Data Location")); | ||||||
|     QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry")); |     QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry")); | ||||||
| 
 | 
 | ||||||
|     open_save_location->setEnabled(program_id != 0); |     const bool is_application = | ||||||
|     open_application_location->setVisible(FileUtil::Exists( |         0x0004000000000000 <= program_id && program_id <= 0x00040000FFFFFFFF; | ||||||
|         Service::AM::GetTitleContentPath(Service::FS::MediaType::SDMC, program_id))); | 
 | ||||||
|     open_update_location->setEnabled(0x0004000000000000 <= program_id && |     std::string sdmc_dir = FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir); | ||||||
|                                      program_id <= 0x00040000FFFFFFFF); |     open_save_location->setVisible( | ||||||
|  |         is_application && FileUtil::Exists(FileSys::ArchiveSource_SDSaveData::GetSaveDataPathFor( | ||||||
|  |                               sdmc_dir, program_id))); | ||||||
|  | 
 | ||||||
|  |     if (extdata_id) { | ||||||
|  |         open_extdata_location->setVisible( | ||||||
|  |             is_application && | ||||||
|  |             FileUtil::Exists(FileSys::GetExtDataPathFromId(sdmc_dir, extdata_id))); | ||||||
|  |     } else { | ||||||
|  |         open_extdata_location->setVisible(false); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     auto media_type = Service::AM::GetTitleMediaType(program_id); | ||||||
|  |     open_application_location->setVisible(path.toStdString() == | ||||||
|  |                                           Service::AM::GetTitleContentPath(media_type, program_id)); | ||||||
|  |     open_update_location->setVisible( | ||||||
|  |         is_application && FileUtil::Exists(Service::AM::GetTitlePath(Service::FS::MediaType::SDMC, | ||||||
|  |                                                                      program_id + 0xe00000000) + | ||||||
|  |                                            "content/")); | ||||||
|     auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); |     auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); | ||||||
|     navigate_to_gamedb_entry->setVisible(it != compatibility_list.end()); |     navigate_to_gamedb_entry->setVisible(it != compatibility_list.end()); | ||||||
| 
 | 
 | ||||||
|     connect(open_save_location, &QAction::triggered, [this, program_id] { |     connect(open_save_location, &QAction::triggered, [this, program_id] { | ||||||
|         emit OpenFolderRequested(program_id, GameListOpenTarget::SAVE_DATA); |         emit OpenFolderRequested(program_id, GameListOpenTarget::SAVE_DATA); | ||||||
|     }); |     }); | ||||||
|  |     connect(open_extdata_location, &QAction::triggered, [this, extdata_id] { | ||||||
|  |         emit OpenFolderRequested(extdata_id, GameListOpenTarget::EXT_DATA); | ||||||
|  |     }); | ||||||
|     connect(open_application_location, &QAction::triggered, [this, program_id] { |     connect(open_application_location, &QAction::triggered, [this, program_id] { | ||||||
|         emit OpenFolderRequested(program_id, GameListOpenTarget::APPLICATION); |         emit OpenFolderRequested(program_id, GameListOpenTarget::APPLICATION); | ||||||
|     }); |     }); | ||||||
|  | @ -651,6 +678,9 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign | ||||||
|             u64 program_id = 0; |             u64 program_id = 0; | ||||||
|             loader->ReadProgramId(program_id); |             loader->ReadProgramId(program_id); | ||||||
| 
 | 
 | ||||||
|  |             u64 extdata_id = 0; | ||||||
|  |             loader->ReadExtdataId(extdata_id); | ||||||
|  | 
 | ||||||
|             std::vector<u8> smdh = [program_id, &loader]() -> std::vector<u8> { |             std::vector<u8> smdh = [program_id, &loader]() -> std::vector<u8> { | ||||||
|                 std::vector<u8> original_smdh; |                 std::vector<u8> original_smdh; | ||||||
|                 loader->ReadIcon(original_smdh); |                 loader->ReadIcon(original_smdh); | ||||||
|  | @ -683,7 +713,8 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign | ||||||
| 
 | 
 | ||||||
|             emit EntryReady( |             emit EntryReady( | ||||||
|                 { |                 { | ||||||
|                     new GameListItemPath(QString::fromStdString(physical_name), smdh, program_id), |                     new GameListItemPath(QString::fromStdString(physical_name), smdh, program_id, | ||||||
|  |                                          extdata_id), | ||||||
|                     new GameListItemCompat(compatibility), |                     new GameListItemCompat(compatibility), | ||||||
|                     new GameListItemRegion(smdh), |                     new GameListItemRegion(smdh), | ||||||
|                     new GameListItem( |                     new GameListItem( | ||||||
|  |  | ||||||
|  | @ -27,7 +27,7 @@ class QTreeView; | ||||||
| class QToolButton; | class QToolButton; | ||||||
| class QVBoxLayout; | class QVBoxLayout; | ||||||
| 
 | 
 | ||||||
| enum class GameListOpenTarget { SAVE_DATA = 0, APPLICATION = 1, UPDATE_DATA = 2 }; | enum class GameListOpenTarget { SAVE_DATA = 0, EXT_DATA = 1, APPLICATION = 2, UPDATE_DATA = 3 }; | ||||||
| 
 | 
 | ||||||
| class GameList : public QWidget { | class GameList : public QWidget { | ||||||
|     Q_OBJECT |     Q_OBJECT | ||||||
|  | @ -89,7 +89,7 @@ private: | ||||||
|     void RefreshGameDirectory(); |     void RefreshGameDirectory(); | ||||||
| 
 | 
 | ||||||
|     void PopupContextMenu(const QPoint& menu_location); |     void PopupContextMenu(const QPoint& menu_location); | ||||||
|     void AddGamePopup(QMenu& context_menu, u64 program_id); |     void AddGamePopup(QMenu& context_menu, const QString& path, u64 program_id, u64 extdata_id); | ||||||
|     void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected); |     void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected); | ||||||
|     void AddPermDirPopup(QMenu& context_menu, QModelIndex selected); |     void AddPermDirPopup(QMenu& context_menu, QModelIndex selected); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -135,12 +135,15 @@ public: | ||||||
|     static const int TitleRole = SortRole; |     static const int TitleRole = SortRole; | ||||||
|     static const int FullPathRole = SortRole + 1; |     static const int FullPathRole = SortRole + 1; | ||||||
|     static const int ProgramIdRole = SortRole + 2; |     static const int ProgramIdRole = SortRole + 2; | ||||||
|  |     static const int ExtdataIdRole = SortRole + 3; | ||||||
| 
 | 
 | ||||||
|     GameListItemPath() = default; |     GameListItemPath() = default; | ||||||
|     GameListItemPath(const QString& game_path, const std::vector<u8>& smdh_data, u64 program_id) { |     GameListItemPath(const QString& game_path, const std::vector<u8>& smdh_data, u64 program_id, | ||||||
|  |                      u64 extdata_id) { | ||||||
|         setData(type(), TypeRole); |         setData(type(), TypeRole); | ||||||
|         setData(game_path, FullPathRole); |         setData(game_path, FullPathRole); | ||||||
|         setData(qulonglong(program_id), ProgramIdRole); |         setData(qulonglong(program_id), ProgramIdRole); | ||||||
|  |         setData(qulonglong(extdata_id), ExtdataIdRole); | ||||||
| 
 | 
 | ||||||
|         if (!Loader::IsValidSMDH(smdh_data)) { |         if (!Loader::IsValidSMDH(smdh_data)) { | ||||||
|             // SMDH is not valid, set a default icon
 |             // SMDH is not valid, set a default icon
 | ||||||
|  |  | ||||||
|  | @ -52,6 +52,7 @@ | ||||||
| #include "common/scm_rev.h" | #include "common/scm_rev.h" | ||||||
| #include "common/scope_exit.h" | #include "common/scope_exit.h" | ||||||
| #include "core/core.h" | #include "core/core.h" | ||||||
|  | #include "core/file_sys/archive_extsavedata.h" | ||||||
| #include "core/file_sys/archive_source_sd_savedata.h" | #include "core/file_sys/archive_source_sd_savedata.h" | ||||||
| #include "core/frontend/applets/default_applets.h" | #include "core/frontend/applets/default_applets.h" | ||||||
| #include "core/gdbstub/gdbstub.h" | #include "core/gdbstub/gdbstub.h" | ||||||
|  | @ -879,7 +880,7 @@ void GMainWindow::OnGameListLoadFile(QString game_path) { | ||||||
|     BootGame(game_path); |     BootGame(game_path); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void GMainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target) { | void GMainWindow::OnGameListOpenFolder(u64 data_id, GameListOpenTarget target) { | ||||||
|     std::string path; |     std::string path; | ||||||
|     std::string open_target; |     std::string open_target; | ||||||
| 
 | 
 | ||||||
|  | @ -887,16 +888,24 @@ void GMainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target | ||||||
|     case GameListOpenTarget::SAVE_DATA: { |     case GameListOpenTarget::SAVE_DATA: { | ||||||
|         open_target = "Save Data"; |         open_target = "Save Data"; | ||||||
|         std::string sdmc_dir = FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir); |         std::string sdmc_dir = FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir); | ||||||
|         path = FileSys::ArchiveSource_SDSaveData::GetSaveDataPathFor(sdmc_dir, program_id); |         path = FileSys::ArchiveSource_SDSaveData::GetSaveDataPathFor(sdmc_dir, data_id); | ||||||
|         break; |         break; | ||||||
|     } |     } | ||||||
|     case GameListOpenTarget::APPLICATION: |     case GameListOpenTarget::EXT_DATA: { | ||||||
|         open_target = "Application"; |         open_target = "Extra Data"; | ||||||
|         path = Service::AM::GetTitlePath(Service::FS::MediaType::SDMC, program_id) + "content/"; |         std::string sdmc_dir = FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir); | ||||||
|  |         path = FileSys::GetExtDataPathFromId(sdmc_dir, data_id); | ||||||
|         break; |         break; | ||||||
|  |     } | ||||||
|  |     case GameListOpenTarget::APPLICATION: { | ||||||
|  |         open_target = "Application"; | ||||||
|  |         auto media_type = Service::AM::GetTitleMediaType(data_id); | ||||||
|  |         path = Service::AM::GetTitlePath(media_type, data_id) + "content/"; | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|     case GameListOpenTarget::UPDATE_DATA: |     case GameListOpenTarget::UPDATE_DATA: | ||||||
|         open_target = "Update Data"; |         open_target = "Update Data"; | ||||||
|         path = Service::AM::GetTitlePath(Service::FS::MediaType::SDMC, program_id + 0xe00000000) + |         path = Service::AM::GetTitlePath(Service::FS::MediaType::SDMC, data_id + 0xe00000000) + | ||||||
|                "content/"; |                "content/"; | ||||||
|         break; |         break; | ||||||
|     default: |     default: | ||||||
|  | @ -914,7 +923,7 @@ void GMainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target | ||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     LOG_INFO(Frontend, "Opening {} path for program_id={:016x}", open_target, program_id); |     LOG_INFO(Frontend, "Opening {} path for data_id={:016x}", open_target, data_id); | ||||||
| 
 | 
 | ||||||
|     QDesktopServices::openUrl(QUrl::fromLocalFile(qpath)); |     QDesktopServices::openUrl(QUrl::fromLocalFile(qpath)); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -176,6 +176,13 @@ std::string GetExtDataContainerPath(const std::string& mount_point, bool shared) | ||||||
|     return fmt::format("{}Nintendo 3DS/{}/{}/extdata/", mount_point, SYSTEM_ID, SDCARD_ID); |     return fmt::format("{}Nintendo 3DS/{}/{}/extdata/", mount_point, SYSTEM_ID, SDCARD_ID); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | std::string GetExtDataPathFromId(const std::string& mount_point, u64 extdata_id) { | ||||||
|  |     u32 high = static_cast<u32>(extdata_id >> 32); | ||||||
|  |     u32 low = static_cast<u32>(extdata_id & 0xFFFFFFFF); | ||||||
|  | 
 | ||||||
|  |     return fmt::format("{}{:08x}/{:08x}/", GetExtDataContainerPath(mount_point, false), high, low); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| Path ConstructExtDataBinaryPath(u32 media_type, u32 high, u32 low) { | Path ConstructExtDataBinaryPath(u32 media_type, u32 high, u32 low) { | ||||||
|     ExtSaveDataArchivePath path; |     ExtSaveDataArchivePath path; | ||||||
|     path.media_type = media_type; |     path.media_type = media_type; | ||||||
|  |  | ||||||
|  | @ -70,6 +70,15 @@ private: | ||||||
|  */ |  */ | ||||||
| std::string GetExtSaveDataPath(const std::string& mount_point, const Path& path); | std::string GetExtSaveDataPath(const std::string& mount_point, const Path& path); | ||||||
| 
 | 
 | ||||||
|  | /**
 | ||||||
|  |  * Constructs a path to the concrete ExtData archive in the host filesystem based on the | ||||||
|  |  * extdata ID and base mount point. | ||||||
|  |  * @param mount_point The base mount point of the ExtSaveData archives. | ||||||
|  |  * @param extdata_id The id of the ExtSaveData | ||||||
|  |  * @returns The complete path to the specified extdata archive in the host filesystem | ||||||
|  |  */ | ||||||
|  | std::string GetExtDataPathFromId(const std::string& mount_point, u64 extdata_id); | ||||||
|  | 
 | ||||||
| /**
 | /**
 | ||||||
|  * Constructs a path to the base folder to hold concrete ExtSaveData archives in the host file |  * Constructs a path to the base folder to hold concrete ExtSaveData archives in the host file | ||||||
|  * system. |  * system. | ||||||
|  |  | ||||||
|  | @ -576,6 +576,23 @@ Loader::ResultStatus NCCHContainer::ReadProgramId(u64_le& program_id) { | ||||||
|     return Loader::ResultStatus::Success; |     return Loader::ResultStatus::Success; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | Loader::ResultStatus NCCHContainer::ReadExtdataId(u64& extdata_id) { | ||||||
|  |     Loader::ResultStatus result = Load(); | ||||||
|  |     if (result != Loader::ResultStatus::Success) | ||||||
|  |         return result; | ||||||
|  | 
 | ||||||
|  |     if (!has_exheader) | ||||||
|  |         return Loader::ResultStatus::ErrorNotUsed; | ||||||
|  | 
 | ||||||
|  |     if (exheader_header.arm11_system_local_caps.storage_info.other_attributes >> 1) { | ||||||
|  |         // Extdata id is not present when using extended savedata access
 | ||||||
|  |         return Loader::ResultStatus::ErrorNotUsed; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     extdata_id = exheader_header.arm11_system_local_caps.storage_info.ext_save_data_id; | ||||||
|  |     return Loader::ResultStatus::Success; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| bool NCCHContainer::HasExeFS() { | bool NCCHContainer::HasExeFS() { | ||||||
|     Loader::ResultStatus result = Load(); |     Loader::ResultStatus result = Load(); | ||||||
|     if (result != Loader::ResultStatus::Success) |     if (result != Loader::ResultStatus::Success) | ||||||
|  |  | ||||||
|  | @ -125,7 +125,7 @@ struct ExHeader_SystemInfo { | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| struct ExHeader_StorageInfo { | struct ExHeader_StorageInfo { | ||||||
|     u8 ext_save_data_id[8]; |     u64_le ext_save_data_id; | ||||||
|     u8 system_save_data_id[8]; |     u8 system_save_data_id[8]; | ||||||
|     u8 reserved[8]; |     u8 reserved[8]; | ||||||
|     u8 access_info[7]; |     u8 access_info[7]; | ||||||
|  | @ -251,6 +251,12 @@ public: | ||||||
|      */ |      */ | ||||||
|     Loader::ResultStatus ReadProgramId(u64_le& program_id); |     Loader::ResultStatus ReadProgramId(u64_le& program_id); | ||||||
| 
 | 
 | ||||||
|  |     /**
 | ||||||
|  |      * Get the Extdata ID of the NCCH container | ||||||
|  |      * @return ResultStatus result of function | ||||||
|  |      */ | ||||||
|  |     Loader::ResultStatus ReadExtdataId(u64& extdata_id); | ||||||
|  | 
 | ||||||
|     /**
 |     /**
 | ||||||
|      * Checks whether the NCCH container contains an ExeFS |      * Checks whether the NCCH container contains an ExeFS | ||||||
|      * @return bool check result |      * @return bool check result | ||||||
|  |  | ||||||
|  | @ -157,6 +157,15 @@ public: | ||||||
|         return ResultStatus::ErrorNotImplemented; |         return ResultStatus::ErrorNotImplemented; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /**
 | ||||||
|  |      * Get the extdata id for the application | ||||||
|  |      * @param out_extdata_id Reference to store extdata id into | ||||||
|  |      * @return ResultStatus result of function | ||||||
|  |      */ | ||||||
|  |     virtual ResultStatus ReadExtdataId(u64& out_extdata_id) { | ||||||
|  |         return ResultStatus::ErrorNotImplemented; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /**
 |     /**
 | ||||||
|      * Get the RomFS of the application |      * Get the RomFS of the application | ||||||
|      * Since the RomFS can be huge, we return a file reference instead of copying to a buffer |      * Since the RomFS can be huge, we return a file reference instead of copying to a buffer | ||||||
|  |  | ||||||
|  | @ -216,6 +216,14 @@ ResultStatus AppLoader_NCCH::ReadProgramId(u64& out_program_id) { | ||||||
|     return ResultStatus::Success; |     return ResultStatus::Success; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | ResultStatus AppLoader_NCCH::ReadExtdataId(u64& out_extdata_id) { | ||||||
|  |     ResultStatus result = base_ncch.ReadExtdataId(out_extdata_id); | ||||||
|  |     if (result != ResultStatus::Success) | ||||||
|  |         return result; | ||||||
|  | 
 | ||||||
|  |     return ResultStatus::Success; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| ResultStatus AppLoader_NCCH::ReadRomFS(std::shared_ptr<FileSys::RomFSReader>& romfs_file) { | ResultStatus AppLoader_NCCH::ReadRomFS(std::shared_ptr<FileSys::RomFSReader>& romfs_file) { | ||||||
|     return base_ncch.ReadRomFS(romfs_file); |     return base_ncch.ReadRomFS(romfs_file); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -51,6 +51,8 @@ public: | ||||||
| 
 | 
 | ||||||
|     ResultStatus ReadProgramId(u64& out_program_id) override; |     ResultStatus ReadProgramId(u64& out_program_id) override; | ||||||
| 
 | 
 | ||||||
|  |     ResultStatus ReadExtdataId(u64& out_extdata_id) override; | ||||||
|  | 
 | ||||||
|     ResultStatus ReadRomFS(std::shared_ptr<FileSys::RomFSReader>& romfs_file) override; |     ResultStatus ReadRomFS(std::shared_ptr<FileSys::RomFSReader>& romfs_file) override; | ||||||
| 
 | 
 | ||||||
|     ResultStatus ReadUpdateRomFS(std::shared_ptr<FileSys::RomFSReader>& romfs_file) override; |     ResultStatus ReadUpdateRomFS(std::shared_ptr<FileSys::RomFSReader>& romfs_file) override; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue