mirror of
https://github.com/SSimco/Cemu.git
synced 2024-11-23 13:29:38 +00:00
Refactored game list code & added option to remove & add multiple game paths
This commit is contained in:
parent
1485d0e315
commit
0037544a00
@ -1,25 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "GameIconLoader.h"
|
||||
#include "JNIUtils.h"
|
||||
|
||||
class AndroidGameIconLoadedCallback : public GameIconLoadedCallback
|
||||
{
|
||||
jmethodID m_onGameIconLoadedMID;
|
||||
JNIUtils::Scopedjobject m_gameTitleLoadedCallbackObj;
|
||||
|
||||
public:
|
||||
AndroidGameIconLoadedCallback(jmethodID onGameIconLoadedMID,
|
||||
jobject gameTitleLoadedCallbackObj)
|
||||
: m_onGameIconLoadedMID(onGameIconLoadedMID),
|
||||
m_gameTitleLoadedCallbackObj(gameTitleLoadedCallbackObj) {}
|
||||
|
||||
void onIconLoaded(TitleId titleId, int* colors, int width, int height) override
|
||||
{
|
||||
JNIUtils::ScopedJNIENV env;
|
||||
jintArray jIconData = env->NewIntArray(width * height);
|
||||
env->SetIntArrayRegion(jIconData, 0, width * height, reinterpret_cast<const jint*>(colors));
|
||||
env->CallVoidMethod(*m_gameTitleLoadedCallbackObj, m_onGameIconLoadedMID, static_cast<jlong>(titleId), jIconData, width, height);
|
||||
env->DeleteLocalRef(jIconData);
|
||||
}
|
||||
};
|
@ -1,8 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "GameTitleLoader.h"
|
||||
#include "JNIUtils.h"
|
||||
|
||||
class AndroidGameTitleLoadedCallback : public GameTitleLoadedCallback {
|
||||
class AndroidGameTitleLoadedCallback : public GameTitleLoadedCallback
|
||||
{
|
||||
jmethodID m_onGameTitleLoadedMID;
|
||||
JNIUtils::Scopedjobject m_gameTitleLoadedCallbackObj;
|
||||
|
||||
@ -11,12 +13,23 @@ class AndroidGameTitleLoadedCallback : public GameTitleLoadedCallback {
|
||||
: m_onGameTitleLoadedMID(onGameTitleLoadedMID),
|
||||
m_gameTitleLoadedCallbackObj(gameTitleLoadedCallbackObj) {}
|
||||
|
||||
void onTitleLoaded(const Game& game) override
|
||||
void onTitleLoaded(const Game& game, const std::shared_ptr<Image>& icon) override
|
||||
{
|
||||
JNIUtils::ScopedJNIENV env;
|
||||
jstring name = env->NewStringUTF(game.name.c_str());
|
||||
jlong titleId = static_cast<const jlong>(game.titleId);
|
||||
env->CallVoidMethod(*m_gameTitleLoadedCallbackObj, m_onGameTitleLoadedMID, titleId, name);
|
||||
int width = -1, height = -1;
|
||||
jintArray jIconData = nullptr;
|
||||
if (icon)
|
||||
{
|
||||
width = icon->m_width;
|
||||
height = icon->m_height;
|
||||
jIconData = env->NewIntArray(width * height);
|
||||
env->SetIntArrayRegion(jIconData, 0, width * height, reinterpret_cast<const jint*>(icon->intColors()));
|
||||
}
|
||||
env->CallVoidMethod(*m_gameTitleLoadedCallbackObj, m_onGameTitleLoadedMID, static_cast<jlong>(titleId), name, jIconData, width, height);
|
||||
if (jIconData != nullptr)
|
||||
env->DeleteLocalRef(jIconData);
|
||||
env->DeleteLocalRef(name);
|
||||
}
|
||||
};
|
||||
|
@ -4,14 +4,11 @@ add_library(CemuAndroid SHARED
|
||||
AndroidEmulatedController.cpp
|
||||
AndroidEmulatedController.h
|
||||
AndroidFilesystemCallbacks.h
|
||||
AndroidGameIconLoadedCallback.h
|
||||
AndroidGameTitleLoadedCallback.h
|
||||
CMakeLists.txt
|
||||
CafeSystemUtils.cpp
|
||||
CafeSystemUtils.h
|
||||
EmulationState.h
|
||||
GameIconLoader.cpp
|
||||
GameIconLoader.h
|
||||
GameTitleLoader.cpp
|
||||
GameTitleLoader.h
|
||||
Image.cpp
|
||||
|
@ -11,7 +11,6 @@
|
||||
#include "CafeSystemUtils.h"
|
||||
#include "Cafe/CafeSystem.h"
|
||||
#include "Cemu/GuiSystem/GuiSystem.h"
|
||||
#include "GameIconLoader.h"
|
||||
#include "GameTitleLoader.h"
|
||||
#include "Utils.h"
|
||||
#include "input/ControllerFactory.h"
|
||||
@ -23,7 +22,6 @@ void CemuCommonInit();
|
||||
|
||||
class EmulationState
|
||||
{
|
||||
GameIconLoader m_gameIconLoader;
|
||||
GameTitleLoader m_gameTitleLoader;
|
||||
std::unordered_map<int64_t, GraphicPackPtr> m_graphicPacks;
|
||||
void fillGraphicPacks()
|
||||
@ -241,24 +239,28 @@ class EmulationState
|
||||
m_gameTitleLoader.setOnTitleLoaded(onGameTitleLoaded);
|
||||
}
|
||||
|
||||
const Image& getGameIcon(TitleId titleId)
|
||||
void addGamesPath(const std::string& gamePath)
|
||||
{
|
||||
return m_gameIconLoader.getGameIcon(titleId);
|
||||
auto& gamePaths = g_config.data().game_paths;
|
||||
if (std::any_of(gamePaths.begin(), gamePaths.end(), [&](auto path) { return path == gamePath; }))
|
||||
return;
|
||||
gamePaths.push_back(gamePath);
|
||||
g_config.Save();
|
||||
CafeTitleList::ClearScanPaths();
|
||||
for (auto& it : gamePaths)
|
||||
CafeTitleList::AddScanPath(it);
|
||||
CafeTitleList::Refresh();
|
||||
}
|
||||
|
||||
void setOnGameIconLoaded(const std::shared_ptr<class GameIconLoadedCallback>& onGameIconLoaded)
|
||||
void removeGamesPath(const std::string& gamePath)
|
||||
{
|
||||
m_gameIconLoader.setOnIconLoaded(onGameIconLoaded);
|
||||
}
|
||||
|
||||
void addGamePath(const fs::path& gamePath)
|
||||
{
|
||||
m_gameTitleLoader.addGamePath(gamePath);
|
||||
}
|
||||
|
||||
void requestGameIcon(TitleId titleId)
|
||||
{
|
||||
m_gameIconLoader.requestIcon(titleId);
|
||||
auto& gamePaths = g_config.data().game_paths;
|
||||
std::erase_if(gamePaths, [&](auto path) { return path == gamePath; });
|
||||
g_config.Save();
|
||||
CafeTitleList::ClearScanPaths();
|
||||
for (auto& it : gamePaths)
|
||||
CafeTitleList::AddScanPath(it);
|
||||
CafeTitleList::Refresh();
|
||||
}
|
||||
|
||||
void reloadGameTitles()
|
||||
|
@ -1,86 +0,0 @@
|
||||
#include "GameIconLoader.h"
|
||||
|
||||
#include "Cafe/TitleList/TitleInfo.h"
|
||||
#include "Cafe/TitleList/TitleList.h"
|
||||
|
||||
GameIconLoader::GameIconLoader()
|
||||
{
|
||||
m_loaderThread = std::thread(&GameIconLoader::loadGameIcons, this);
|
||||
}
|
||||
|
||||
GameIconLoader::~GameIconLoader()
|
||||
{
|
||||
m_continueLoading = false;
|
||||
m_condVar.notify_one();
|
||||
m_loaderThread.join();
|
||||
}
|
||||
|
||||
const Image& GameIconLoader::getGameIcon(TitleId titleId)
|
||||
{
|
||||
return m_iconCache.at(titleId);
|
||||
}
|
||||
|
||||
void GameIconLoader::requestIcon(TitleId titleId)
|
||||
{
|
||||
{
|
||||
std::lock_guard lock(m_threadMutex);
|
||||
m_iconsToLoad.emplace_front(titleId);
|
||||
}
|
||||
m_condVar.notify_one();
|
||||
}
|
||||
|
||||
void GameIconLoader::loadGameIcons()
|
||||
{
|
||||
while (m_continueLoading)
|
||||
{
|
||||
TitleId titleId = 0;
|
||||
{
|
||||
std::unique_lock lock(m_threadMutex);
|
||||
m_condVar.wait(lock, [this] { return !m_iconsToLoad.empty() || !m_continueLoading; });
|
||||
if (!m_continueLoading)
|
||||
return;
|
||||
titleId = m_iconsToLoad.front();
|
||||
m_iconsToLoad.pop_front();
|
||||
}
|
||||
TitleInfo titleInfo;
|
||||
if (!CafeTitleList::GetFirstByTitleId(titleId, titleInfo))
|
||||
continue;
|
||||
|
||||
if (auto iconIt = m_iconCache.find(titleId); iconIt != m_iconCache.end())
|
||||
{
|
||||
auto& icon = iconIt->second;
|
||||
if (m_onIconLoaded)
|
||||
m_onIconLoaded->onIconLoaded(titleId, icon.intColors(), icon.m_width, icon.m_height);
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string tempMountPath = TitleInfo::GetUniqueTempMountingPath();
|
||||
if (!titleInfo.Mount(tempMountPath, "", FSC_PRIORITY_BASE))
|
||||
continue;
|
||||
auto tgaData = fsc_extractFile((tempMountPath + "/meta/iconTex.tga").c_str());
|
||||
if (tgaData && tgaData->size() > 16)
|
||||
{
|
||||
Image image(tgaData.value());
|
||||
if (image.isOk())
|
||||
{
|
||||
if (m_onIconLoaded)
|
||||
m_onIconLoaded->onIconLoaded(titleId, image.intColors(), image.m_width, image.m_height);
|
||||
m_iconCache.emplace(titleId, std::move(image));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
cemuLog_log(LogType::Force, "Failed to load icon for title {:016x}", titleId);
|
||||
}
|
||||
titleInfo.Unmount(tempMountPath);
|
||||
}
|
||||
}
|
||||
|
||||
void GameIconLoader::setOnIconLoaded(const std::shared_ptr<GameIconLoadedCallback>& onIconLoaded)
|
||||
{
|
||||
{
|
||||
std::lock_guard lock(m_threadMutex);
|
||||
m_onIconLoaded = onIconLoaded;
|
||||
}
|
||||
m_condVar.notify_one();
|
||||
};
|
@ -1,33 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "Cafe/TitleList/TitleId.h"
|
||||
#include "Image.h"
|
||||
|
||||
class GameIconLoadedCallback {
|
||||
public:
|
||||
virtual void onIconLoaded(TitleId titleId, int* colors, int width, int height) = 0;
|
||||
};
|
||||
|
||||
class GameIconLoader {
|
||||
std::condition_variable m_condVar;
|
||||
std::atomic_bool m_continueLoading = true;
|
||||
std::thread m_loaderThread;
|
||||
std::mutex m_threadMutex;
|
||||
std::deque<TitleId> m_iconsToLoad;
|
||||
std::map<TitleId, Image> m_iconCache;
|
||||
std::shared_ptr<GameIconLoadedCallback> m_onIconLoaded = nullptr;
|
||||
|
||||
public:
|
||||
GameIconLoader();
|
||||
|
||||
~GameIconLoader();
|
||||
|
||||
const Image& getGameIcon(TitleId titleId);
|
||||
|
||||
void setOnIconLoaded(const std::shared_ptr<GameIconLoadedCallback>& onIconLoaded);
|
||||
|
||||
void requestIcon(TitleId titleId);
|
||||
|
||||
private:
|
||||
void loadGameIcons();
|
||||
};
|
@ -1,5 +1,13 @@
|
||||
#include "GameTitleLoader.h"
|
||||
|
||||
std::optional<TitleInfo> getFirstTitleInfoByTitleId(TitleId titleId)
|
||||
{
|
||||
TitleInfo titleInfo;
|
||||
if (CafeTitleList::GetFirstByTitleId(titleId, titleInfo))
|
||||
return titleInfo;
|
||||
return {};
|
||||
}
|
||||
|
||||
GameTitleLoader::GameTitleLoader()
|
||||
{
|
||||
m_loaderThread = std::thread(&GameTitleLoader::loadGameTitles, this);
|
||||
@ -27,11 +35,14 @@ void GameTitleLoader::reloadGameTitles()
|
||||
{
|
||||
if (m_callbackIdTitleList.has_value())
|
||||
{
|
||||
CafeTitleList::Refresh();
|
||||
CafeTitleList::UnregisterCallback(m_callbackIdTitleList.value());
|
||||
}
|
||||
m_gameInfos.clear();
|
||||
registerCallback();
|
||||
CafeTitleList::ClearScanPaths();
|
||||
for (auto&& gamePath : g_config.data().game_paths)
|
||||
CafeTitleList::AddScanPath(gamePath);
|
||||
CafeTitleList::Refresh();
|
||||
m_callbackIdTitleList = CafeTitleList::RegisterCallback([](CafeTitleListCallbackEvent* evt, void* ctx) { static_cast<GameTitleLoader*>(ctx)->HandleTitleListCallback(evt); }, this);
|
||||
}
|
||||
|
||||
GameTitleLoader::~GameTitleLoader()
|
||||
@ -57,11 +68,14 @@ void GameTitleLoader::titleRefresh(TitleId titleId)
|
||||
isNewEntry = true;
|
||||
m_gameInfos[baseTitleId] = Game();
|
||||
}
|
||||
|
||||
Game& game = m_gameInfos[baseTitleId];
|
||||
std::optional<TitleInfo> titleInfo = getFirstTitleInfoByTitleId(titleId);
|
||||
game.titleId = baseTitleId;
|
||||
game.name = GetNameByTitleId(baseTitleId);
|
||||
game.name = getNameByTitleId(baseTitleId, titleInfo);
|
||||
game.version = gameInfo.GetVersion();
|
||||
game.region = gameInfo.GetRegion();
|
||||
std::shared_ptr<Image> icon = loadIcon(baseTitleId, titleInfo);
|
||||
if (gameInfo.HasAOC())
|
||||
{
|
||||
game.dlc = gameInfo.GetAOCVersion();
|
||||
@ -72,7 +86,7 @@ void GameTitleLoader::titleRefresh(TitleId titleId)
|
||||
if (iosu::pdm::GetStatForGamelist(baseTitleId, playTimeStat))
|
||||
game.secondsPlayed = playTimeStat.numMinutesPlayed * 60;
|
||||
if (m_gameTitleLoadedCallback)
|
||||
m_gameTitleLoadedCallback->onTitleLoaded(game);
|
||||
m_gameTitleLoadedCallback->onTitleLoaded(game, icon);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -84,11 +98,10 @@ void GameTitleLoader::loadGameTitles()
|
||||
{
|
||||
while (m_continueLoading)
|
||||
{
|
||||
TitleId titleId = 0;
|
||||
TitleId titleId;
|
||||
{
|
||||
std::unique_lock lock(m_threadMutex);
|
||||
m_condVar.wait(lock, [this] { return (!m_titlesToLoad.empty()) ||
|
||||
!m_continueLoading; });
|
||||
m_condVar.wait(lock, [this] { return (!m_titlesToLoad.empty()) || !m_continueLoading; });
|
||||
if (!m_continueLoading)
|
||||
return;
|
||||
titleId = m_titlesToLoad.front();
|
||||
@ -97,29 +110,43 @@ void GameTitleLoader::loadGameTitles()
|
||||
titleRefresh(titleId);
|
||||
}
|
||||
}
|
||||
|
||||
std::string GameTitleLoader::GetNameByTitleId(uint64 titleId)
|
||||
std::string GameTitleLoader::getNameByTitleId(TitleId titleId, const std::optional<TitleInfo>& titleInfo)
|
||||
{
|
||||
auto it = m_name_cache.find(titleId);
|
||||
if (it != m_name_cache.end())
|
||||
return it->second;
|
||||
TitleInfo titleInfo;
|
||||
if (!CafeTitleList::GetFirstByTitleId(titleId, titleInfo))
|
||||
if (!titleInfo.has_value())
|
||||
return "Unknown title";
|
||||
std::string name;
|
||||
if (!GetConfig().GetGameListCustomName(titleId, name))
|
||||
name = titleInfo.GetMetaTitleName();
|
||||
name = titleInfo.value().GetMetaTitleName();
|
||||
m_name_cache.emplace(titleId, name);
|
||||
return name;
|
||||
}
|
||||
|
||||
void GameTitleLoader::registerCallback()
|
||||
std::shared_ptr<Image> GameTitleLoader::loadIcon(TitleId titleId, const std::optional<TitleInfo>& titleInfo)
|
||||
{
|
||||
m_callbackIdTitleList = CafeTitleList::RegisterCallback(
|
||||
[](CafeTitleListCallbackEvent* evt, void* ctx) {
|
||||
static_cast<GameTitleLoader*>(ctx)->HandleTitleListCallback(evt);
|
||||
},
|
||||
this);
|
||||
if (auto iconIt = m_iconCache.find(titleId); iconIt != m_iconCache.end())
|
||||
return iconIt->second;
|
||||
std::string tempMountPath = TitleInfo::GetUniqueTempMountingPath();
|
||||
if (!titleInfo.has_value())
|
||||
return {};
|
||||
auto titleInfoValue = titleInfo.value();
|
||||
if (!titleInfoValue.Mount(tempMountPath, "", FSC_PRIORITY_BASE))
|
||||
return {};
|
||||
auto tgaData = fsc_extractFile((tempMountPath + "/meta/iconTex.tga").c_str());
|
||||
if (!tgaData || tgaData->size() <= 16)
|
||||
{
|
||||
cemuLog_log(LogType::Force, "Failed to load icon for title {:016x}", titleId);
|
||||
titleInfoValue.Unmount(tempMountPath);
|
||||
return {};
|
||||
}
|
||||
auto image = std::make_shared<Image>(tgaData.value());
|
||||
titleInfoValue.Unmount(tempMountPath);
|
||||
if (!image->isOk())
|
||||
return {};
|
||||
m_iconCache.emplace(titleId, image);
|
||||
return image;
|
||||
}
|
||||
|
||||
void GameTitleLoader::HandleTitleListCallback(CafeTitleListCallbackEvent* evt)
|
||||
@ -129,10 +156,3 @@ void GameTitleLoader::HandleTitleListCallback(CafeTitleListCallbackEvent* evt)
|
||||
queueTitle(evt->titleInfo->GetAppTitleId());
|
||||
}
|
||||
}
|
||||
|
||||
void GameTitleLoader::addGamePath(const fs::path& path)
|
||||
{
|
||||
CafeTitleList::ClearScanPaths();
|
||||
CafeTitleList::AddScanPath(path);
|
||||
CafeTitleList::Refresh();
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
#include "Cafe/IOSU/PDM/iosu_pdm.h"
|
||||
#include "Cafe/TitleList/TitleId.h"
|
||||
#include "Cafe/TitleList/TitleList.h"
|
||||
#include "Image.h"
|
||||
|
||||
struct Game
|
||||
{
|
||||
@ -14,12 +15,14 @@ struct Game
|
||||
CafeConsoleRegion region;
|
||||
};
|
||||
|
||||
class GameTitleLoadedCallback {
|
||||
class GameTitleLoadedCallback
|
||||
{
|
||||
public:
|
||||
virtual void onTitleLoaded(const Game& game) = 0;
|
||||
virtual void onTitleLoaded(const Game& game, const std::shared_ptr<Image>& icon) = 0;
|
||||
};
|
||||
|
||||
class GameTitleLoader {
|
||||
class GameTitleLoader
|
||||
{
|
||||
std::mutex m_threadMutex;
|
||||
std::condition_variable m_condVar;
|
||||
std::thread m_loaderThread;
|
||||
@ -27,30 +30,22 @@ class GameTitleLoader {
|
||||
std::deque<TitleId> m_titlesToLoad;
|
||||
std::optional<uint64> m_callbackIdTitleList;
|
||||
std::map<TitleId, Game> m_gameInfos;
|
||||
std::map<TitleId, std::shared_ptr<Image>> m_iconCache;
|
||||
std::map<TitleId, std::string> m_name_cache;
|
||||
std::shared_ptr<GameTitleLoadedCallback> m_gameTitleLoadedCallback = nullptr;
|
||||
|
||||
public:
|
||||
GameTitleLoader();
|
||||
|
||||
void queueTitle(TitleId titleId);
|
||||
|
||||
void setOnTitleLoaded(const std::shared_ptr<GameTitleLoadedCallback>& gameTitleLoadedCallback);
|
||||
|
||||
void reloadGameTitles();
|
||||
|
||||
~GameTitleLoader();
|
||||
|
||||
void titleRefresh(TitleId titleId);
|
||||
|
||||
void addGamePath(const fs::path& path);
|
||||
|
||||
private:
|
||||
void loadGameTitles();
|
||||
|
||||
std::string GetNameByTitleId(uint64 titleId);
|
||||
|
||||
void registerCallback();
|
||||
|
||||
std::string getNameByTitleId(TitleId titleId, const std::optional<TitleInfo>& titleInfo);
|
||||
std::shared_ptr<Image> loadIcon(TitleId titleId, const std::optional<TitleInfo>& titleInfo);
|
||||
void HandleTitleListCallback(CafeTitleListCallbackEvent* evt);
|
||||
};
|
||||
|
@ -1,5 +1,4 @@
|
||||
#include "Cafe/GraphicPack/GraphicPack2.h"
|
||||
#include "AndroidGameIconLoadedCallback.h"
|
||||
#include "AndroidGameTitleLoadedCallback.h"
|
||||
#include "EmulationState.h"
|
||||
#include "JNIUtils.h"
|
||||
@ -35,26 +34,17 @@ Java_info_cemu_Cemu_NativeLibrary_startGame([[maybe_unused]] JNIEnv* env, [[mayb
|
||||
extern "C" [[maybe_unused]] JNIEXPORT void JNICALL
|
||||
Java_info_cemu_Cemu_NativeLibrary_setGameTitleLoadedCallback(JNIEnv* env, [[maybe_unused]] jclass clazz, jobject game_title_loaded_callback)
|
||||
{
|
||||
if (game_title_loaded_callback == nullptr)
|
||||
{
|
||||
s_emulationState.setOnGameTitleLoaded(nullptr);
|
||||
return;
|
||||
}
|
||||
jclass gameTitleLoadedCallbackClass = env->GetObjectClass(game_title_loaded_callback);
|
||||
jmethodID onGameTitleLoadedMID = env->GetMethodID(gameTitleLoadedCallbackClass, "onGameTitleLoaded", "(JLjava/lang/String;)V");
|
||||
jmethodID onGameTitleLoadedMID = env->GetMethodID(gameTitleLoadedCallbackClass, "onGameTitleLoaded", "(JLjava/lang/String;[III)V");
|
||||
env->DeleteLocalRef(gameTitleLoadedCallbackClass);
|
||||
s_emulationState.setOnGameTitleLoaded(std::make_shared<AndroidGameTitleLoadedCallback>(onGameTitleLoadedMID, game_title_loaded_callback));
|
||||
}
|
||||
|
||||
extern "C" [[maybe_unused]] JNIEXPORT void JNICALL
|
||||
Java_info_cemu_Cemu_NativeLibrary_setGameIconLoadedCallback(JNIEnv* env, [[maybe_unused]] jclass clazz, jobject game_icon_loaded_callback)
|
||||
{
|
||||
jclass gameIconLoadedCallbackClass = env->GetObjectClass(game_icon_loaded_callback);
|
||||
jmethodID gameIconLoadedMID = env->GetMethodID(gameIconLoadedCallbackClass, "onGameIconLoaded", "(J[III)V");
|
||||
s_emulationState.setOnGameIconLoaded(std::make_shared<AndroidGameIconLoadedCallback>(gameIconLoadedMID, game_icon_loaded_callback));
|
||||
}
|
||||
|
||||
extern "C" [[maybe_unused]] JNIEXPORT void JNICALL
|
||||
Java_info_cemu_Cemu_NativeLibrary_requestGameIcon([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jlong title_id)
|
||||
{
|
||||
s_emulationState.requestGameIcon(static_cast<TitleId>(title_id));
|
||||
}
|
||||
|
||||
extern "C" [[maybe_unused]] JNIEXPORT void JNICALL
|
||||
Java_info_cemu_Cemu_NativeLibrary_reloadGameTitles([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz)
|
||||
{
|
||||
@ -97,9 +87,21 @@ Java_info_cemu_Cemu_NativeLibrary_recreateRenderSurface([[maybe_unused]] JNIEnv*
|
||||
}
|
||||
|
||||
extern "C" [[maybe_unused]] JNIEXPORT void JNICALL
|
||||
Java_info_cemu_Cemu_NativeLibrary_addGamePath(JNIEnv* env, [[maybe_unused]] jclass clazz, jstring uri)
|
||||
Java_info_cemu_Cemu_NativeLibrary_addGamesPath(JNIEnv* env, [[maybe_unused]] jclass clazz, jstring uri)
|
||||
{
|
||||
s_emulationState.addGamePath(JNIUtils::JStringToString(env, uri));
|
||||
s_emulationState.addGamesPath(JNIUtils::JStringToString(env, uri));
|
||||
}
|
||||
|
||||
extern "C" [[maybe_unused]] JNIEXPORT void JNICALL
|
||||
Java_info_cemu_Cemu_NativeLibrary_removeGamesPath(JNIEnv* env, [[maybe_unused]] jclass clazz, jstring uri)
|
||||
{
|
||||
s_emulationState.removeGamesPath(JNIUtils::JStringToString(env, uri));
|
||||
}
|
||||
|
||||
extern "C" [[maybe_unused]] JNIEXPORT jobject JNICALL
|
||||
Java_info_cemu_Cemu_NativeLibrary_getGamesPaths(JNIEnv* env, [[maybe_unused]] jclass clazz)
|
||||
{
|
||||
return JNIUtils::createJavaStringArrayList(env, g_config.data().game_paths);
|
||||
}
|
||||
|
||||
extern "C" [[maybe_unused]] JNIEXPORT void JNICALL
|
||||
|
@ -28,28 +28,23 @@ public class NativeLibrary {
|
||||
|
||||
public static native void recreateRenderSurface(boolean isMainCanvas);
|
||||
|
||||
|
||||
public interface GameTitleLoadedCallback {
|
||||
void onGameTitleLoaded(long titleId, String title);
|
||||
void onGameTitleLoaded(long titleId, String title, int[] colors, int width, int height);
|
||||
}
|
||||
|
||||
public static native void setGameTitleLoadedCallback(GameTitleLoadedCallback gameTitleLoadedCallback);
|
||||
|
||||
public interface GameIconLoadedCallback {
|
||||
void onGameIconLoaded(long titleId, int[] colors, int width, int height);
|
||||
}
|
||||
|
||||
public static native void setGameIconLoadedCallback(GameIconLoadedCallback gameIconLoadedCallback);
|
||||
|
||||
public static native void requestGameIcon(long titleId);
|
||||
|
||||
public static native void reloadGameTitles();
|
||||
|
||||
public static native void initializeActiveSettings(String dataPath, String cachePath);
|
||||
|
||||
public static native void initializeEmulation();
|
||||
|
||||
public static native void addGamePath(String uri);
|
||||
public static native void addGamesPath(String uri);
|
||||
|
||||
public static native void removeGamesPath(String uri);
|
||||
|
||||
public static native ArrayList<String> getGamesPaths();
|
||||
|
||||
public static native void onNativeKey(String deviceDescriptor, String deviceName, int key, boolean isPressed);
|
||||
|
||||
|
@ -7,12 +7,9 @@ public class Game {
|
||||
String title;
|
||||
Bitmap icon;
|
||||
|
||||
public Game(Long titleId, String title) {
|
||||
public Game(Long titleId, String title, Bitmap icon) {
|
||||
this.titleId = titleId;
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public void setIconData(Bitmap icon) {
|
||||
this.icon = icon;
|
||||
}
|
||||
|
||||
|
@ -1,67 +1,56 @@
|
||||
package info.cemu.Cemu.gameview;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Filter;
|
||||
import android.widget.Filterable;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import info.cemu.Cemu.R;
|
||||
|
||||
public class GameAdapter extends RecyclerView.Adapter<GameAdapter.ViewHolder> implements Filterable {
|
||||
private final Map<Long, Game> gameInfoMap;
|
||||
private final List<Long> gameTitleIds;
|
||||
private List<Long> filteredGameTitleIds;
|
||||
public class GameAdapter extends ListAdapter<Game, GameAdapter.ViewHolder> {
|
||||
private final GameTitleClickAction gameTitleClickAction;
|
||||
private List<Game> orignalGameList;
|
||||
private String filterText;
|
||||
public static final DiffUtil.ItemCallback<Game> DIFF_CALLBACK = new DiffUtil.ItemCallback<>() {
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull Game oldItem, @NonNull Game newItem) {
|
||||
return oldItem.titleId.equals(newItem.titleId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Filter getFilter() {
|
||||
return new Filter() {
|
||||
@Override
|
||||
protected FilterResults performFiltering(CharSequence charSequence) {
|
||||
String gameTitle = charSequence.toString();
|
||||
if (gameTitle.isEmpty())
|
||||
filteredGameTitleIds = gameTitleIds;
|
||||
else
|
||||
filteredGameTitleIds = gameTitleIds.stream()
|
||||
.filter(titleId -> gameInfoMap.get(titleId).title.toLowerCase().contains(gameTitle.toLowerCase()))
|
||||
.collect(Collectors.toList());
|
||||
FilterResults filterResults = new FilterResults();
|
||||
filterResults.values = filteredGameTitleIds;
|
||||
return filterResults;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void publishResults(CharSequence charSequence, FilterResults filterResults) {
|
||||
filteredGameTitleIds = (List<Long>) filterResults.values;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull Game oldItem, @NonNull Game newItem) {
|
||||
return oldItem.titleId.equals(newItem.titleId);
|
||||
}
|
||||
};
|
||||
|
||||
public interface GameTitleClickAction {
|
||||
void action(long titleId);
|
||||
}
|
||||
|
||||
public GameAdapter(GameTitleClickAction gameTitleClickAction) {
|
||||
super();
|
||||
super(DIFF_CALLBACK);
|
||||
this.gameTitleClickAction = gameTitleClickAction;
|
||||
gameTitleIds = new ArrayList<>();
|
||||
filteredGameTitleIds = new ArrayList<>();
|
||||
gameInfoMap = new HashMap<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void submitList(@Nullable List<Game> list) {
|
||||
orignalGameList = list;
|
||||
if (filterText == null || filterText.isBlank() || orignalGameList == null) {
|
||||
super.submitList(orignalGameList);
|
||||
return;
|
||||
}
|
||||
super.submitList(orignalGameList.stream().filter(g -> g.title.toLowerCase(Locale.US).contains(this.filterText)).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@ -73,7 +62,7 @@ public class GameAdapter extends RecyclerView.Adapter<GameAdapter.ViewHolder> im
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull GameAdapter.ViewHolder holder, int position) {
|
||||
Game game = gameInfoMap.get(filteredGameTitleIds.get(position));
|
||||
Game game = getItem(position);
|
||||
if (game != null) {
|
||||
holder.icon.setImageBitmap(game.getIcon());
|
||||
holder.text.setText(game.getTitle());
|
||||
@ -84,31 +73,16 @@ public class GameAdapter extends RecyclerView.Adapter<GameAdapter.ViewHolder> im
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return filteredGameTitleIds.size();
|
||||
}
|
||||
|
||||
void clear() {
|
||||
notifyDataSetChanged();
|
||||
gameInfoMap.clear();
|
||||
gameTitleIds.clear();
|
||||
filteredGameTitleIds.clear();
|
||||
}
|
||||
|
||||
void addGameInfo(long titleId, String title) {
|
||||
gameTitleIds.add(titleId);
|
||||
gameInfoMap.put(titleId, new Game(titleId, title));
|
||||
filteredGameTitleIds = gameTitleIds;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
void setGameIcon(long titleId, Bitmap icon) {
|
||||
Game game = gameInfoMap.get(titleId);
|
||||
if (game != null) {
|
||||
game.setIconData(icon);
|
||||
notifyDataSetChanged();
|
||||
public void setFilterText(String filterText) {
|
||||
if (filterText != null) {
|
||||
filterText = filterText.toLowerCase(Locale.US);
|
||||
}
|
||||
this.filterText = filterText;
|
||||
if (filterText == null || filterText.isBlank() || orignalGameList == null) {
|
||||
super.submitList(orignalGameList);
|
||||
return;
|
||||
}
|
||||
super.submitList(orignalGameList.stream().filter(g -> g.title.toLowerCase(Locale.US).contains(this.filterText)).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
@ -0,0 +1,50 @@
|
||||
package info.cemu.Cemu.gameview;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import info.cemu.Cemu.NativeLibrary;
|
||||
|
||||
public class GameViewModel extends ViewModel {
|
||||
private final MutableLiveData<List<Game>> gamesData;
|
||||
|
||||
private final ArrayList<Game> games = new ArrayList<>();
|
||||
|
||||
public LiveData<List<Game>> getGames() {
|
||||
return gamesData;
|
||||
}
|
||||
|
||||
public GameViewModel() {
|
||||
this.gamesData = new MutableLiveData<>();
|
||||
NativeLibrary.setGameTitleLoadedCallback((titleId, title, colors, width, height) -> {
|
||||
Bitmap icon = null;
|
||||
if (colors != null)
|
||||
icon = Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888);
|
||||
Game game = new Game(titleId, title, icon);
|
||||
synchronized (GameViewModel.this) {
|
||||
games.add(game);
|
||||
gamesData.postValue(new ArrayList<>(games));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
NativeLibrary.setGameTitleLoadedCallback(null);
|
||||
}
|
||||
|
||||
public void refreshGames() {
|
||||
games.clear();
|
||||
gamesData.setValue(null);
|
||||
NativeLibrary.reloadGameTitles();
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
package info.cemu.Cemu.gameview;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
@ -12,88 +11,62 @@ import android.view.ViewGroup;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import info.cemu.Cemu.R;
|
||||
import info.cemu.Cemu.databinding.FragmentGamesBinding;
|
||||
import info.cemu.Cemu.emulation.EmulationActivity;
|
||||
import info.cemu.Cemu.NativeLibrary;
|
||||
import info.cemu.Cemu.settings.SettingsActivity;
|
||||
import info.cemu.Cemu.settings.SettingsManager;
|
||||
|
||||
public class GamesFragment extends Fragment {
|
||||
private FragmentGamesBinding binding;
|
||||
private GameAdapter gameAdapter;
|
||||
private SettingsManager settingsManager;
|
||||
private GameViewModel gameViewModel;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
settingsManager = new SettingsManager(requireContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
binding = FragmentGamesBinding.inflate(inflater, container, false);
|
||||
String gamesPath = settingsManager.getGamesPath();
|
||||
if (gamesPath != null)
|
||||
NativeLibrary.addGamePath(gamesPath);
|
||||
RecyclerView recyclerView = binding.gamesRecyclerView;
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
binding.settingsButton.setOnClickListener(v -> {
|
||||
Intent intent = new Intent(requireActivity(), SettingsActivity.class);
|
||||
startActivity(intent);
|
||||
});
|
||||
View rootView = binding.getRoot();
|
||||
gameAdapter = new GameAdapter(titleId -> {
|
||||
Intent intent = new Intent(getContext(), EmulationActivity.class);
|
||||
intent.putExtra(EmulationActivity.GAME_TITLE_ID, titleId);
|
||||
startActivity(intent);
|
||||
});
|
||||
gameAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
|
||||
@Override
|
||||
public void onChanged() {
|
||||
if (gameAdapter.getItemCount() == 0) {
|
||||
if (!binding.searchText.getText().toString().isEmpty())
|
||||
binding.gamesListMessage.setText(R.string.no_games_found);
|
||||
binding.gamesListMessage.setVisibility(View.VISIBLE);
|
||||
gameViewModel = new ViewModelProvider(this).get(GameViewModel.class);
|
||||
gameViewModel.getGames().observe(this, gameList -> gameAdapter.submitList(gameList));
|
||||
}
|
||||
|
||||
} else {
|
||||
binding.gamesListMessage.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
gameViewModel.refreshGames();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
FragmentGamesBinding binding = FragmentGamesBinding.inflate(inflater, container, false);
|
||||
RecyclerView recyclerView = binding.gamesRecyclerView;
|
||||
binding.settingsButton.setOnClickListener(v -> {
|
||||
Intent intent = new Intent(requireActivity(), SettingsActivity.class);
|
||||
startActivity(intent);
|
||||
});
|
||||
View rootView = binding.getRoot();
|
||||
|
||||
binding.gamesSwipeRefresh.setOnRefreshListener(() -> {
|
||||
gameAdapter.clear();
|
||||
NativeLibrary.reloadGameTitles();
|
||||
gameViewModel.refreshGames();
|
||||
binding.gamesSwipeRefresh.setRefreshing(false);
|
||||
});
|
||||
recyclerView.setAdapter(gameAdapter);
|
||||
NativeLibrary.setGameTitleLoadedCallback((titleId, title) -> requireActivity().runOnUiThread(() -> {
|
||||
gameAdapter.addGameInfo(titleId, title);
|
||||
NativeLibrary.requestGameIcon(titleId);
|
||||
}));
|
||||
NativeLibrary.setGameIconLoadedCallback((titleId, colors, width, height) -> {
|
||||
Bitmap icon = Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888);
|
||||
requireActivity().runOnUiThread(() -> gameAdapter.setGameIcon(titleId, icon));
|
||||
});
|
||||
NativeLibrary.reloadGameTitles();
|
||||
|
||||
binding.searchText.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
|
||||
gameAdapter.getFilter().filter(charSequence.toString());
|
||||
gameAdapter.setFilterText(charSequence.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable editable) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,69 +0,0 @@
|
||||
package info.cemu.Cemu.settings;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import info.cemu.Cemu.R;
|
||||
import info.cemu.Cemu.databinding.FragmentOptionsBinding;
|
||||
import info.cemu.Cemu.guibasecomponents.ButtonRecyclerViewItem;
|
||||
import info.cemu.Cemu.guibasecomponents.GenericRecyclerViewAdapter;
|
||||
import info.cemu.Cemu.guibasecomponents.SimpleButtonRecyclerViewItem;
|
||||
|
||||
public class OptionsFragment extends Fragment {
|
||||
private ActivityResultLauncher<Intent> folderSelectionLauncher;
|
||||
private SettingsManager settingsManager;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
settingsManager = new SettingsManager(requireContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
FragmentOptionsBinding binding = FragmentOptionsBinding.inflate(inflater, container, false);
|
||||
GenericRecyclerViewAdapter genericRecyclerViewAdapter = new GenericRecyclerViewAdapter();
|
||||
folderSelectionLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
|
||||
if (result.getResultCode() == RESULT_OK) {
|
||||
Intent data = result.getData();
|
||||
if (data != null) {
|
||||
Uri uri = Objects.requireNonNull(data.getData());
|
||||
requireActivity().getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
DocumentFile documentFile = DocumentFile.fromTreeUri(requireContext(), uri);
|
||||
if (documentFile == null) return;
|
||||
String gamesPath = documentFile.getUri().toString();
|
||||
settingsManager.setGamesPath(gamesPath);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
genericRecyclerViewAdapter.addRecyclerViewItem(new ButtonRecyclerViewItem(getString(R.string.games_folder_label), getString(R.string.games_folder_description), () -> {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
folderSelectionLauncher.launch(intent);
|
||||
}));
|
||||
genericRecyclerViewAdapter.addRecyclerViewItem(new SimpleButtonRecyclerViewItem(getString(R.string.input_settings), () -> NavHostFragment.findNavController(OptionsFragment.this).navigate(R.id.action_optionsFragment_to_inputSettingsFragment)));
|
||||
genericRecyclerViewAdapter.addRecyclerViewItem(new SimpleButtonRecyclerViewItem(getString(R.string.graphics_settings), () -> NavHostFragment.findNavController(OptionsFragment.this).navigate(R.id.action_optionsFragment_to_graphicSettingsFragment)));
|
||||
genericRecyclerViewAdapter.addRecyclerViewItem(new SimpleButtonRecyclerViewItem(getString(R.string.audio_settings), () -> NavHostFragment.findNavController(OptionsFragment.this).navigate(R.id.action_optionsFragment_to_audioSettingsFragment)));
|
||||
genericRecyclerViewAdapter.addRecyclerViewItem(new SimpleButtonRecyclerViewItem(getString(R.string.graphic_packs), () -> NavHostFragment.findNavController(OptionsFragment.this).navigate(R.id.action_optionsFragment_to_graphicPacksRootFragment)));
|
||||
genericRecyclerViewAdapter.addRecyclerViewItem(new SimpleButtonRecyclerViewItem(getString(R.string.overlay), () -> NavHostFragment.findNavController(OptionsFragment.this).navigate(R.id.action_optionsFragment_to_overlaySettingsFragment)));
|
||||
binding.optionsRecyclerView.setAdapter(genericRecyclerViewAdapter);
|
||||
|
||||
return binding.getRoot();
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package info.cemu.Cemu.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import info.cemu.Cemu.R;
|
||||
import info.cemu.Cemu.databinding.GenericRecyclerViewLayoutBinding;
|
||||
import info.cemu.Cemu.guibasecomponents.ButtonRecyclerViewItem;
|
||||
import info.cemu.Cemu.guibasecomponents.GenericRecyclerViewAdapter;
|
||||
import info.cemu.Cemu.guibasecomponents.SimpleButtonRecyclerViewItem;
|
||||
|
||||
public class SettingsFragment extends Fragment {
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
GenericRecyclerViewLayoutBinding binding = GenericRecyclerViewLayoutBinding.inflate(inflater, container, false);
|
||||
GenericRecyclerViewAdapter genericRecyclerViewAdapter = new GenericRecyclerViewAdapter();
|
||||
|
||||
genericRecyclerViewAdapter.addRecyclerViewItem(new ButtonRecyclerViewItem(getString(R.string.add_game_path), getString(R.string.games_folder_description), () -> NavHostFragment.findNavController(SettingsFragment.this).navigate(R.id.action_settingsFragment_to_gamePathsFragment)));
|
||||
genericRecyclerViewAdapter.addRecyclerViewItem(new SimpleButtonRecyclerViewItem(getString(R.string.input_settings), () -> NavHostFragment.findNavController(SettingsFragment.this).navigate(R.id.action_settingsFragment_to_inputSettingsFragment)));
|
||||
genericRecyclerViewAdapter.addRecyclerViewItem(new SimpleButtonRecyclerViewItem(getString(R.string.graphics_settings), () -> NavHostFragment.findNavController(SettingsFragment.this).navigate(R.id.action_settingsFragment_to_graphicSettingsFragment)));
|
||||
genericRecyclerViewAdapter.addRecyclerViewItem(new SimpleButtonRecyclerViewItem(getString(R.string.audio_settings), () -> NavHostFragment.findNavController(SettingsFragment.this).navigate(R.id.action_settingsFragment_to_audioSettingsFragment)));
|
||||
genericRecyclerViewAdapter.addRecyclerViewItem(new SimpleButtonRecyclerViewItem(getString(R.string.graphic_packs), () -> NavHostFragment.findNavController(SettingsFragment.this).navigate(R.id.action_settingsFragment_to_graphicPacksRootFragment)));
|
||||
genericRecyclerViewAdapter.addRecyclerViewItem(new SimpleButtonRecyclerViewItem(getString(R.string.overlay), () -> NavHostFragment.findNavController(SettingsFragment.this).navigate(R.id.action_settingsFragment_to_overlaySettingsFragment)));
|
||||
binding.recyclerView.setAdapter(genericRecyclerViewAdapter);
|
||||
|
||||
return binding.getRoot();
|
||||
}
|
||||
}
|
@ -5,18 +5,9 @@ import android.content.SharedPreferences;
|
||||
|
||||
public class SettingsManager {
|
||||
public static final String PREFERENCES_NAME = "CEMU_PREFERENCES";
|
||||
public static final String GAMES_PATH_KEY = "GAME_PATHS_KEY";
|
||||
private final SharedPreferences sharedPreferences;
|
||||
|
||||
public SettingsManager(Context context) {
|
||||
sharedPreferences = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
public String getGamesPath() {
|
||||
return sharedPreferences.getString(GAMES_PATH_KEY, null);
|
||||
}
|
||||
|
||||
public void setGamesPath(String gamesPath) {
|
||||
sharedPreferences.edit().putString(GAMES_PATH_KEY, gamesPath).apply();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,65 @@
|
||||
package info.cemu.Cemu.settings.gamespath;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import info.cemu.Cemu.R;
|
||||
|
||||
public class GamePathAdapter extends ListAdapter<String, GamePathAdapter.ViewHolder> {
|
||||
public interface OnRemoveGamePath {
|
||||
void onRemoveGamePath(String path);
|
||||
}
|
||||
|
||||
private final static DiffUtil.ItemCallback<String> DIFF_CALLBACK = new DiffUtil.ItemCallback<>() {
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull String oldItem, @NonNull String newItem) {
|
||||
return oldItem.equals(newItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull String oldItem, @NonNull String newItem) {
|
||||
return oldItem.equals(newItem);
|
||||
}
|
||||
};
|
||||
private final OnRemoveGamePath onRemoveGamePath;
|
||||
|
||||
public GamePathAdapter(OnRemoveGamePath onRemoveGamePath) {
|
||||
super(DIFF_CALLBACK);
|
||||
this.onRemoveGamePath = onRemoveGamePath;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_game_path, parent, false);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
String gamePath = getItem(position);
|
||||
holder.gamePath.setText(gamePath);
|
||||
holder.gamePath.setSelected(true);
|
||||
holder.deleteButton.setOnClickListener(v -> onRemoveGamePath.onRemoveGamePath(gamePath));
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView gamePath;
|
||||
Button deleteButton;
|
||||
|
||||
public ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
gamePath = itemView.findViewById(R.id.game_path_text);
|
||||
deleteButton = itemView.findViewById(R.id.remove_game_path_button);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
package info.cemu.Cemu.settings.gamespath;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.MenuProvider;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import info.cemu.Cemu.NativeLibrary;
|
||||
import info.cemu.Cemu.R;
|
||||
import info.cemu.Cemu.databinding.GenericRecyclerViewLayoutBinding;
|
||||
|
||||
public class GamePathsFragment extends Fragment {
|
||||
private ActivityResultLauncher<Intent> folderSelectionLauncher;
|
||||
private GamePathAdapter gamePathAdapter;
|
||||
private List<String> gamesPaths;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
folderSelectionLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
|
||||
if (result.getResultCode() == RESULT_OK) {
|
||||
Intent data = result.getData();
|
||||
if (data != null) {
|
||||
Uri uri = Objects.requireNonNull(data.getData());
|
||||
requireActivity().getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
DocumentFile documentFile = DocumentFile.fromTreeUri(requireContext(), uri);
|
||||
if (documentFile == null) return;
|
||||
String gamesPath = documentFile.getUri().toString();
|
||||
if (gamesPaths.stream().anyMatch(p -> p.equals(gamesPath))) {
|
||||
Toast.makeText(requireContext(), R.string.game_path_already_added, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
NativeLibrary.addGamesPath(gamesPath);
|
||||
gamesPaths = Stream.concat(Stream.of(gamesPath), gamesPaths.stream()).collect(Collectors.toList());
|
||||
gamePathAdapter.submitList(gamesPaths);
|
||||
}
|
||||
}
|
||||
});
|
||||
gamePathAdapter = new GamePathAdapter(new GamePathAdapter.OnRemoveGamePath() {
|
||||
@Override
|
||||
public void onRemoveGamePath(String path) {
|
||||
NativeLibrary.removeGamesPath(path);
|
||||
gamesPaths = gamesPaths.stream().filter(p -> !p.equals(path)).collect(Collectors.toList());
|
||||
gamePathAdapter.submitList(gamesPaths);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
GenericRecyclerViewLayoutBinding binding = GenericRecyclerViewLayoutBinding.inflate(inflater, container, false);
|
||||
binding.recyclerView.setAdapter(gamePathAdapter);
|
||||
gamesPaths = NativeLibrary.getGamesPaths();
|
||||
gamePathAdapter.submitList(gamesPaths);
|
||||
requireActivity().addMenuProvider(new MenuProvider() {
|
||||
@Override
|
||||
public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) {
|
||||
menuInflater.inflate(R.menu.menu_game_paths, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemSelected(@NonNull MenuItem menuItem) {
|
||||
if (menuItem.getItemId() == R.id.action_add_game_path) {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
folderSelectionLauncher.launch(intent);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}, getViewLifecycleOwner(), Lifecycle.State.RESUMED);
|
||||
return binding.getRoot();
|
||||
}
|
||||
}
|
@ -23,7 +23,6 @@ public class InputSettingsFragment extends Fragment {
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
|
||||
var binding = GenericRecyclerViewLayoutBinding.inflate(inflater, container, false);
|
||||
GenericRecyclerViewAdapter genericRecyclerViewAdapter = new GenericRecyclerViewAdapter();
|
||||
genericRecyclerViewAdapter.addRecyclerViewItem(new SimpleButtonRecyclerViewItem(getString(R.string.input_overlay_settings), () -> NavHostFragment.findNavController(InputSettingsFragment.this).navigate(R.id.action_inputSettingsFragment_to_inputOverlaySettingsFragment)));
|
||||
|
10
src/android/app/src/main/res/drawable/ic_add.xml
Normal file
10
src/android/app/src/main/res/drawable/ic_add.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M440,520L200,520L200,440L440,440L440,200L520,200L520,440L760,440L760,520L520,520L520,760L440,760L440,520Z" />
|
||||
</vector>
|
10
src/android/app/src/main/res/drawable/ic_delete.xml
Normal file
10
src/android/app/src/main/res/drawable/ic_delete.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M280,840Q247,840 223.5,816.5Q200,793 200,760L200,240L160,240L160,160L360,160L360,120L600,120L600,160L800,160L800,240L760,240L760,760Q760,793 736.5,816.5Q713,840 680,840L280,840ZM680,240L280,240L280,760Q280,760 280,760Q280,760 280,760L680,760Q680,760 680,760Q680,760 680,760L680,240ZM360,680L440,680L440,320L360,320L360,680ZM520,680L600,680L600,320L520,320L520,680ZM280,240L280,240L280,760Q280,760 280,760Q280,760 280,760L280,760Q280,760 280,760Q280,760 280,760L280,240Z" />
|
||||
</vector>
|
@ -1,40 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/games_swipe_refresh"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="8dp">
|
||||
android:padding="8dp"
|
||||
tools:context=".gameview.GamesFragment">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/constraintLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/materialCardView"
|
||||
style="?attr/materialCardViewFilledStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_marginEnd="328dp"
|
||||
android:layout_toStartOf="@+id/settings_button"
|
||||
app:cardCornerRadius="30dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/settings_button"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/game_search_icon"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_gravity="center"
|
||||
@ -44,49 +47,42 @@
|
||||
|
||||
<EditText
|
||||
android:id="@+id/search_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/transparent"
|
||||
android:hint="@string/search_games"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="text"
|
||||
android:maxWidth="300dp"
|
||||
android:maxLines="1" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/settings_button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="8dp"
|
||||
style="?attr/materialIconButtonFilledStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_gravity="end"
|
||||
android:contentDescription="@string/settings"
|
||||
android:insetLeft="0dp"
|
||||
android:insetTop="0dp"
|
||||
android:insetRight="0dp"
|
||||
android:insetBottom="0dp"
|
||||
android:gravity="center_vertical"
|
||||
app:icon="@drawable/ic_settings"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="0dp"
|
||||
app:iconSize="24dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/games_list_message"
|
||||
style="@style/TextAppearance.Material3.TitleLarge"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/games_recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@+id/constraintLayout" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
android:layout_margin="8dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/constraintLayout"
|
||||
app:spanCount="@integer/games_view_span_count" />
|
||||
</LinearLayout>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
@ -3,13 +3,14 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:orientation="horizontal">
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/game_icon"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="60dp"
|
||||
android:layout_margin="8dp"
|
||||
android:contentDescription="@string/game_icon"
|
||||
android:scaleType="fitXY" />
|
||||
|
||||
|
42
src/android/app/src/main/res/layout/layout_game_path.xml
Normal file
42
src/android/app/src/main/res/layout/layout_game_path.xml
Normal file
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
style="?attr/materialCardViewFilledStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/game_path_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="marquee"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:padding="12dp"
|
||||
android:paddingVertical="12dp"
|
||||
android:singleLine="true"
|
||||
android:text="Game path"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/remove_game_path_button"
|
||||
style="?attr/materialIconButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:contentDescription="@string/remove_game_path"
|
||||
android:tooltipText="@string/remove_game_path"
|
||||
app:icon="@drawable/ic_delete"
|
||||
app:iconSize="32dp" />
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
9
src/android/app/src/main/res/menu/menu_game_paths.xml
Normal file
9
src/android/app/src/main/res/menu/menu_game_paths.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/action_add_game_path"
|
||||
android:icon="@drawable/ic_add"
|
||||
android:title="@string/add_game_path"
|
||||
app:showAsAction="collapseActionView|ifRoom" />
|
||||
</menu>
|
@ -3,11 +3,12 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/nav_settings_graph"
|
||||
app:startDestination="@id/optionsFragment">
|
||||
app:startDestination="@id/settingsFragment">
|
||||
<fragment
|
||||
android:id="@+id/inputSettingsFragment"
|
||||
android:name="info.cemu.Cemu.settings.input.InputSettingsFragment"
|
||||
android:label="@string/input_settings">
|
||||
android:label="@string/input_settings"
|
||||
tools:layout="@layout/generic_recycler_view_layout">
|
||||
<action
|
||||
android:id="@+id/action_inputSettingsFragment_to_controllerInputsFragment"
|
||||
app:destination="@id/controllerInputsFragment" />
|
||||
@ -18,7 +19,8 @@
|
||||
<fragment
|
||||
android:id="@+id/graphicPacksRootFragment"
|
||||
android:name="info.cemu.Cemu.settings.graphicpacks.GraphicPacksRootFragment"
|
||||
android:label="@string/graphic_packs">
|
||||
android:label="@string/graphic_packs"
|
||||
tools:layout="@layout/generic_recycler_view_layout">
|
||||
<action
|
||||
android:id="@+id/action_graphicPacksRootFragment_to_graphicPacksFragment"
|
||||
app:destination="@id/graphicPacksFragment" />
|
||||
@ -26,7 +28,8 @@
|
||||
<fragment
|
||||
android:id="@+id/graphicPacksFragment"
|
||||
android:name="info.cemu.Cemu.settings.graphicpacks.GraphicPacksFragment"
|
||||
android:label="{title}">
|
||||
android:label="{title}"
|
||||
tools:layout="@layout/generic_recycler_view_layout">
|
||||
<action
|
||||
android:id="@+id/action_graphicPacksFragment_self"
|
||||
app:destination="@id/graphicPacksFragment" />
|
||||
@ -36,43 +39,56 @@
|
||||
app:nullable="true" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/optionsFragment"
|
||||
android:name="info.cemu.Cemu.settings.OptionsFragment"
|
||||
android:label="@string/options"
|
||||
tools:layout="@layout/fragment_options">
|
||||
android:id="@+id/settingsFragment"
|
||||
android:name="info.cemu.Cemu.settings.SettingsFragment"
|
||||
android:label="@string/settings"
|
||||
tools:layout="@layout/generic_recycler_view_layout">
|
||||
<action
|
||||
android:id="@+id/action_optionsFragment_to_inputSettingsFragment"
|
||||
android:id="@+id/action_settingsFragment_to_inputSettingsFragment"
|
||||
app:destination="@id/inputSettingsFragment" />
|
||||
<action
|
||||
android:id="@+id/action_optionsFragment_to_graphicSettingsFragment"
|
||||
android:id="@+id/action_settingsFragment_to_graphicSettingsFragment"
|
||||
app:destination="@id/graphicsSettingsFragment" />
|
||||
<action
|
||||
android:id="@+id/action_optionsFragment_to_audioSettingsFragment"
|
||||
android:id="@+id/action_settingsFragment_to_audioSettingsFragment"
|
||||
app:destination="@id/audioSettingsFragment" />
|
||||
<action
|
||||
android:id="@+id/action_optionsFragment_to_graphicPacksRootFragment"
|
||||
android:id="@+id/action_settingsFragment_to_graphicPacksRootFragment"
|
||||
app:destination="@id/graphicPacksRootFragment" />
|
||||
<action
|
||||
android:id="@+id/action_optionsFragment_to_overlaySettingsFragment"
|
||||
android:id="@+id/action_settingsFragment_to_overlaySettingsFragment"
|
||||
app:destination="@id/overlaySettingsFragment" />
|
||||
<action
|
||||
android:id="@+id/action_settingsFragment_to_gamePathsFragment"
|
||||
app:destination="@id/gamePathsFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/controllerInputsFragment"
|
||||
android:name="info.cemu.Cemu.settings.input.ControllerInputsFragment" />
|
||||
android:name="info.cemu.Cemu.settings.input.ControllerInputsFragment"
|
||||
tools:layout="@layout/generic_recycler_view_layout" />
|
||||
<fragment
|
||||
android:id="@+id/graphicsSettingsFragment"
|
||||
android:name="info.cemu.Cemu.settings.graphics.GraphicsSettingsFragment"
|
||||
android:label="@string/graphics_settings" />
|
||||
android:label="@string/graphics_settings"
|
||||
tools:layout="@layout/generic_recycler_view_layout" />
|
||||
<fragment
|
||||
android:id="@+id/audioSettingsFragment"
|
||||
android:name="info.cemu.Cemu.settings.audio.AudioSettingsFragment"
|
||||
android:label="@string/audio_settings" />
|
||||
android:label="@string/audio_settings"
|
||||
tools:layout="@layout/generic_recycler_view_layout" />
|
||||
<fragment
|
||||
android:id="@+id/overlaySettingsFragment"
|
||||
android:name="info.cemu.Cemu.settings.overlay.OverlaySettingsFragment"
|
||||
android:label="@string/overlay_settings" />
|
||||
android:label="@string/overlay_settings"
|
||||
tools:layout="@layout/generic_recycler_view_layout" />
|
||||
<fragment
|
||||
android:id="@+id/inputOverlaySettingsFragment"
|
||||
android:name="info.cemu.Cemu.inputoverlay.InputOverlaySettingsFragment"
|
||||
android:label="@string/input_overlay_settings" />
|
||||
android:label="@string/input_overlay_settings"
|
||||
tools:layout="@layout/generic_recycler_view_layout" />
|
||||
<fragment
|
||||
android:id="@+id/gamePathsFragment"
|
||||
android:name="info.cemu.Cemu.settings.gamespath.GamePathsFragment"
|
||||
android:label="@string/game_paths_settings"
|
||||
tools:layout="@layout/generic_recycler_view_layout" />
|
||||
</navigation>
|
3
src/android/app/src/main/res/values-w1240dp/integers.xml
Normal file
3
src/android/app/src/main/res/values-w1240dp/integers.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<integer name="games_view_span_count">2</integer>
|
||||
</resources>
|
3
src/android/app/src/main/res/values/integers.xml
Normal file
3
src/android/app/src/main/res/values/integers.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<integer name="games_view_span_count">1</integer>
|
||||
</resources>
|
@ -65,9 +65,8 @@
|
||||
<string name="right_axis">Right Axis</string>
|
||||
<string name="extra">Extra</string>
|
||||
<string name="nunchuck">Nunchuck</string>
|
||||
<string name="games_folder_description">Add the games path to scan for games displayed in the game list</string>
|
||||
<string name="games_folder_description">Add a game path to scan for games displayed in the game list</string>
|
||||
<string name="games_folder_empty">No games folder selected</string>
|
||||
<string name="games_folder_label">Set games folder</string>
|
||||
<string name="options">Options</string>
|
||||
<string name="emulated_controller_selection_description">Selected controller: %1s</string>
|
||||
<string name="async_shader_compile">Async shader compile</string>
|
||||
@ -165,4 +164,8 @@
|
||||
<string name="replace_tv_with_pad">Replace TV with PAD</string>
|
||||
<string name="installed_games_title">Installed games</string>
|
||||
<string name="enable_motion">Enable motion</string>
|
||||
<string name="add_game_path">Add game path</string>
|
||||
<string name="remove_game_path">Remove game path</string>
|
||||
<string name="game_paths_settings">Game paths</string>
|
||||
<string name="game_path_already_added">Game path already added</string>
|
||||
</resources>
|
Loading…
Reference in New Issue
Block a user