Added context menu for game list

This commit is contained in:
SSimco 2024-10-13 11:30:07 +03:00
parent 6c0dc45f86
commit 1d17abd9d9
28 changed files with 1065 additions and 152 deletions

View File

@ -27,14 +27,18 @@ public:
[[nodiscard]] const std::optional<std::string>& GetGameName() const { return m_gameName; }
[[nodiscard]] const std::optional<bool>& ShouldLoadSharedLibraries() const { return m_loadSharedLibraries; }
void SetShouldLoadSharedLibraries(bool shouldLoadSharedLibraries) { m_loadSharedLibraries = shouldLoadSharedLibraries; }
[[nodiscard]] bool StartWithGamepadView() const { return m_startWithPadView; }
[[nodiscard]] const std::optional<GraphicAPI>& GetGraphicsAPI() const { return m_graphics_api; }
[[nodiscard]] const AccurateShaderMulOption& GetAccurateShaderMul() const { return m_accurateShaderMul; }
void SetAccurateShaderMul(AccurateShaderMulOption accurateShaderMulOption) { m_accurateShaderMul = accurateShaderMulOption; }
[[nodiscard]] const std::optional<PrecompiledShaderOption>& GetPrecompiledShadersState() const { return m_precompiledShaders; }
[[nodiscard]] uint32 GetThreadQuantum() const { return m_threadQuantum; }
void SetThreadQuantum(uint32 threadQuantum){ m_threadQuantum = threadQuantum; }
[[nodiscard]] const std::optional<CPUMode>& GetCPUMode() const { return m_cpuMode; }
void SetCPUMode(CPUMode cpuMode) { m_cpuMode = cpuMode; }
[[nodiscard]] bool IsAudioDisabled() const { return m_disableAudio; }

View File

@ -2,34 +2,68 @@
#include "GameTitleLoader.h"
#include "JNIUtils.h"
#include <android/bitmap.h>
class AndroidGameTitleLoadedCallback : public GameTitleLoadedCallback
{
jmethodID m_onGameTitleLoadedMID;
JNIUtils::Scopedjobject m_gameTitleLoadedCallbackObj;
jmethodID m_gameConstructorMID;
JNIUtils::Scopedjclass m_gamejclass{"info/cemu/Cemu/nativeinterface/NativeGameTitles$Game"};
jmethodID m_createBitmapMID;
JNIUtils::Scopedjclass m_bitmapClass{"android/graphics/Bitmap"};
JNIUtils::Scopedjobject m_bitmapFormat;
public:
AndroidGameTitleLoadedCallback(jmethodID onGameTitleLoadedMID, jobject gameTitleLoadedCallbackObj)
: m_onGameTitleLoadedMID(onGameTitleLoadedMID),
m_gameTitleLoadedCallbackObj(gameTitleLoadedCallbackObj) {}
m_gameTitleLoadedCallbackObj(gameTitleLoadedCallbackObj)
{
JNIUtils::ScopedJNIENV env;
m_bitmapFormat = JNIUtils::getEnumValue(*env, "android/graphics/Bitmap$Config", "ARGB_8888");
m_gameConstructorMID = env->GetMethodID(*m_gamejclass, "<init>", "(JLjava/lang/String;Ljava/lang/String;SSISSSIZLandroid/graphics/Bitmap;)V");
m_createBitmapMID = env->GetStaticMethodID(*m_bitmapClass, "createBitmap", "([IIILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");
}
void onTitleLoaded(const Game& game, const std::shared_ptr<Image>& icon) override
{
JNIUtils::ScopedJNIENV env;
static JNIUtils::ScopedJNIENV env;
jstring name = env->NewStringUTF(game.name.c_str());
jstring path = game.path.has_value() ? env->NewStringUTF(game.path->c_str()) : nullptr;
int width = -1, height = -1;
jintArray jIconData = nullptr;
jobject bitmap = nullptr;
sint32 lastPlayedYear = 0, lastPlayedMonth = 0, lastPlayedDay = 0;
if (game.lastPlayed.has_value())
{
lastPlayedYear = static_cast<int>(game.lastPlayed->year());
lastPlayedMonth = static_cast<unsigned int>(game.lastPlayed->month());
lastPlayedDay = static_cast<unsigned int>(game.lastPlayed->day());
}
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, path, name, jIconData, width, height);
if (jIconData != nullptr)
jintArray jIconData = env->NewIntArray(icon->m_width * icon->m_height);
env->SetIntArrayRegion(jIconData, 0, icon->m_width * icon->m_height, icon->m_colors);
bitmap = env->CallStaticObjectMethod(*m_bitmapClass, m_createBitmapMID, jIconData, icon->m_width, icon->m_height, *m_bitmapFormat);
env->DeleteLocalRef(jIconData);
}
jobject gamejobject = env->NewObject(
*m_gamejclass,
m_gameConstructorMID,
game.titleId,
path,
name,
game.version,
game.dlc,
static_cast<sint32>(game.region),
lastPlayedYear,
lastPlayedMonth,
lastPlayedDay,
game.minutesPlayed,
game.isFavorite,
bitmap);
env->CallVoidMethod(*m_gameTitleLoadedCallbackObj, m_onGameTitleLoadedMID, gamejobject);
env->DeleteLocalRef(gamejobject);
if (bitmap != nullptr)
env->DeleteLocalRef(bitmap);
if (path != nullptr)
env->DeleteLocalRef(path);
env->DeleteLocalRef(name);

View File

@ -56,6 +56,7 @@ GameTitleLoader::~GameTitleLoader()
void GameTitleLoader::titleRefresh(TitleId titleId)
{
using namespace std::chrono;
GameInfo2 gameInfo = CafeTitleList::GetGameInfo(titleId);
if (!gameInfo.IsValid())
{
@ -74,26 +75,28 @@ void GameTitleLoader::titleRefresh(TitleId titleId)
game.titleId = baseTitleId;
if (titleInfo.has_value())
game.path = titleInfo->GetPath();
game.isFavorite = GetConfig().IsGameListFavorite(baseTitleId);
game.name = getNameByTitleId(baseTitleId, titleInfo);
game.version = gameInfo.GetVersion();
game.region = gameInfo.GetRegion();
game.dlc = gameInfo.GetAOCVersion();
std::shared_ptr<Image> icon = loadIcon(baseTitleId, titleInfo);
if (gameInfo.HasAOC())
if (!isNewEntry)
{
game.dlc = gameInfo.GetAOCVersion();
// TOOD: update?
return;
}
if (isNewEntry)
iosu::pdm::GameListStat playTimeStat{};
if (iosu::pdm::GetStatForGamelist(baseTitleId, playTimeStat))
{
iosu::pdm::GameListStat playTimeStat{};
if (iosu::pdm::GetStatForGamelist(baseTitleId, playTimeStat))
game.secondsPlayed = playTimeStat.numMinutesPlayed * 60;
if (m_gameTitleLoadedCallback)
m_gameTitleLoadedCallback->onTitleLoaded(game, icon);
}
else
{
// TODO: Update
game.minutesPlayed = playTimeStat.numMinutesPlayed;
if (playTimeStat.last_played.year != 0)
{
game.lastPlayed = year_month_day(year(playTimeStat.last_played.year), month(playTimeStat.last_played.month), day(playTimeStat.last_played.day));
}
}
if (m_gameTitleLoadedCallback)
m_gameTitleLoadedCallback->onTitleLoaded(game, icon);
}
void GameTitleLoader::loadGameTitles()

View File

@ -9,10 +9,12 @@ struct Game
{
std::string name;
std::optional<fs::path> path;
uint32 secondsPlayed;
uint16 dlc;
bool isFavorite;
uint16 version;
uint16 dlc;
TitleId titleId;
std::optional<std::chrono::year_month_day> lastPlayed;
uint32 minutesPlayed;
CafeConsoleRegion region;
};

View File

@ -7,44 +7,39 @@
Image::Image(Image&& image)
{
this->m_image = image.m_image;
this->m_colors = image.m_colors;
this->m_width = image.m_width;
this->m_height = image.m_height;
this->m_channels = image.m_channels;
image.m_image = nullptr;
image.m_colors = nullptr;
}
Image::Image(const std::vector<uint8>& imageBytes)
{
m_image = stbi_load_from_memory(imageBytes.data(), imageBytes.size(), &m_width, &m_height, &m_channels, STBI_rgb_alpha);
if (m_image)
stbi_uc* stbImage = stbi_load_from_memory(imageBytes.data(), imageBytes.size(), &m_width, &m_height, &m_channels, STBI_rgb_alpha);
if (!stbImage)
return;
for (size_t i = 0; i < m_width * m_height * 4; i += 4)
{
for (int i = 0; i < m_width * m_height * 4; i += 4)
{
uint8 r = m_image[i];
uint8 g = m_image[i + 1];
uint8 b = m_image[i + 2];
uint8 a = m_image[i + 3];
m_image[i] = b;
m_image[i + 1] = g;
m_image[i + 2] = r;
m_image[i + 3] = a;
}
uint8 r = stbImage[i];
uint8 g = stbImage[i + 1];
uint8 b = stbImage[i + 2];
uint8 a = stbImage[i + 3];
stbImage[i] = b;
stbImage[i + 1] = g;
stbImage[i + 2] = r;
stbImage[i + 3] = a;
}
m_colors = reinterpret_cast<sint32*>(stbImage);
}
bool Image::isOk() const
{
return m_image != nullptr;
}
int* Image::intColors() const
{
return reinterpret_cast<int*>(m_image);
return m_colors != nullptr;
}
Image::~Image()
{
if (m_image)
stbi_image_free(m_image);
if (m_colors)
stbi_image_free(m_colors);
}

View File

@ -2,7 +2,7 @@
struct Image
{
uint8_t* m_image = nullptr;
sint32* m_colors = nullptr;
int m_width = 0;
int m_height = 0;
int m_channels = 0;
@ -13,7 +13,5 @@ struct Image
bool isOk() const;
int* intColors() const;
~Image();
};

View File

@ -1,10 +1,120 @@
#include "JNIUtils.h"
#include "Cafe/GameProfile/GameProfile.h"
#include "GameTitleLoader.h"
#include "AndroidGameTitleLoadedCallback.h"
namespace NativeGameTitles
{
GameTitleLoader s_gameTitleLoader;
std::list<fs::path> getCachesPaths(const TitleId& titleId)
{
std::list<fs::path> cachePaths{
ActiveSettings::GetCachePath("shaderCache/driver/vk/{:016x}.bin", titleId),
ActiveSettings::GetCachePath("shaderCache/precompiled/{:016x}_spirv.bin", titleId),
ActiveSettings::GetCachePath("shaderCache/precompiled/{:016x}_gl.bin", titleId),
ActiveSettings::GetCachePath("shaderCache/transferable/{:016x}_shaders.bin", titleId),
ActiveSettings::GetCachePath("shaderCache/transferable/{:016x}_vkpipeline.bin", titleId),
};
cachePaths.remove_if([](const fs::path& cachePath) {
std::error_code ec;
return !fs::exists(cachePath, ec);
});
return cachePaths;
}
TitleId s_currentTitleId = 0;
GameProfile s_currentGameProfile{};
void LoadGameProfile(TitleId titleId)
{
if (s_currentTitleId == titleId)
return;
s_currentTitleId = titleId;
s_currentGameProfile.Reset();
s_currentGameProfile.Load(titleId);
}
} // namespace NativeGameTitles
extern "C" [[maybe_unused]] JNIEXPORT jboolean JNICALL
Java_info_cemu_Cemu_nativeinterface_NativeGameTitles_isLoadingSharedLibrariesForTitleEnabled([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jlong game_title_id)
{
NativeGameTitles::LoadGameProfile(game_title_id);
return NativeGameTitles::s_currentGameProfile.ShouldLoadSharedLibraries().value_or(false);
}
extern "C" [[maybe_unused]] JNIEXPORT void JNICALL
Java_info_cemu_Cemu_nativeinterface_NativeGameTitles_setLoadingSharedLibrariesForTitleEnabled([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jlong game_title_id, jboolean enabled)
{
NativeGameTitles::LoadGameProfile(game_title_id);
NativeGameTitles::s_currentGameProfile.SetShouldLoadSharedLibraries(enabled);
NativeGameTitles::s_currentGameProfile.Save(game_title_id);
}
extern "C" [[maybe_unused]] JNIEXPORT jint JNICALL
Java_info_cemu_Cemu_nativeinterface_NativeGameTitles_getCpuModeForTitle([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jlong game_title_id)
{
NativeGameTitles::LoadGameProfile(game_title_id);
return static_cast<jint>(NativeGameTitles::s_currentGameProfile.GetCPUMode().value_or(CPUMode::Auto));
}
extern "C" [[maybe_unused]] JNIEXPORT void JNICALL
Java_info_cemu_Cemu_nativeinterface_NativeGameTitles_setCpuModeForTitle([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jlong game_title_id, jint cpu_mode)
{
NativeGameTitles::LoadGameProfile(game_title_id);
NativeGameTitles::s_currentGameProfile.SetCPUMode(static_cast<CPUMode>(cpu_mode));
NativeGameTitles::s_currentGameProfile.Save(game_title_id);
}
extern "C" [[maybe_unused]] JNIEXPORT jint JNICALL
Java_info_cemu_Cemu_nativeinterface_NativeGameTitles_getThreadQuantumForTitle([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jlong game_title_id)
{
NativeGameTitles::LoadGameProfile(game_title_id);
return NativeGameTitles::s_currentGameProfile.GetThreadQuantum();
}
extern "C" [[maybe_unused]] JNIEXPORT void JNICALL
Java_info_cemu_Cemu_nativeinterface_NativeGameTitles_setThreadQuantumForTitle([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jlong game_title_id, jint thread_quantum)
{
NativeGameTitles::LoadGameProfile(game_title_id);
NativeGameTitles::s_currentGameProfile.SetThreadQuantum(std::clamp(thread_quantum, 5000, 536870912));
NativeGameTitles::s_currentGameProfile.Save(game_title_id);
}
extern "C" [[maybe_unused]] JNIEXPORT jboolean JNICALL
Java_info_cemu_Cemu_nativeinterface_NativeGameTitles_isShaderMultiplicationAccuracyForTitleEnabled([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jlong game_title_id)
{
NativeGameTitles::LoadGameProfile(game_title_id);
return NativeGameTitles::s_currentGameProfile.GetAccurateShaderMul() == AccurateShaderMulOption::True;
}
extern "C" [[maybe_unused]] JNIEXPORT void JNICALL
Java_info_cemu_Cemu_nativeinterface_NativeGameTitles_setShaderMultiplicationAccuracyForTitleEnabled([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jlong game_title_id, jboolean enabled)
{
NativeGameTitles::LoadGameProfile(game_title_id);
NativeGameTitles::s_currentGameProfile.SetAccurateShaderMul(enabled ? AccurateShaderMulOption::True : AccurateShaderMulOption::False);
NativeGameTitles::s_currentGameProfile.Save(game_title_id);
}
extern "C" [[maybe_unused]] JNIEXPORT jboolean JNICALL
Java_info_cemu_Cemu_nativeinterface_NativeGameTitles_titleHasShaderCacheFiles([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jlong game_title_id)
{
return !NativeGameTitles::getCachesPaths(game_title_id).empty();
}
extern "C" [[maybe_unused]] JNIEXPORT void JNICALL
Java_info_cemu_Cemu_nativeinterface_NativeGameTitles_removeShaderCacheFilesForTitle([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jlong game_title_id)
{
std::error_code ec;
for (auto&& cacheFilePath : NativeGameTitles::getCachesPaths(game_title_id))
fs::remove(cacheFilePath, ec);
}
extern "C" [[maybe_unused]] JNIEXPORT void JNICALL
Java_info_cemu_Cemu_nativeinterface_NativeGameTitles_setGameTitleFavorite([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jlong game_title_id, jboolean isFavorite)
{
GetConfig().SetGameListFavorite(game_title_id, isFavorite);
g_config.Save();
}
extern "C" [[maybe_unused]] JNIEXPORT void JNICALL
@ -16,7 +126,7 @@ Java_info_cemu_Cemu_nativeinterface_NativeGameTitles_setGameTitleLoadedCallback(
return;
}
jclass gameTitleLoadedCallbackClass = env->GetObjectClass(game_title_loaded_callback);
jmethodID onGameTitleLoadedMID = env->GetMethodID(gameTitleLoadedCallbackClass, "onGameTitleLoaded", "(Ljava/lang/String;Ljava/lang/String;[III)V");
jmethodID onGameTitleLoadedMID = env->GetMethodID(gameTitleLoadedCallbackClass, "onGameTitleLoaded", "(Linfo/cemu/Cemu/nativeinterface/NativeGameTitles$Game;)V");
env->DeleteLocalRef(gameTitleLoadedCallbackClass);
NativeGameTitles::s_gameTitleLoader.setOnTitleLoaded(std::make_shared<AndroidGameTitleLoadedCallback>(onGameTitleLoadedMID, game_title_loaded_callback));
}
@ -27,7 +137,7 @@ Java_info_cemu_Cemu_nativeinterface_NativeGameTitles_reloadGameTitles([[maybe_un
NativeGameTitles::s_gameTitleLoader.reloadGameTitles();
}
extern "C" JNIEXPORT jobject JNICALL
extern "C" [[maybe_unused]] JNIEXPORT jobject JNICALL
Java_info_cemu_Cemu_nativeinterface_NativeGameTitles_getInstalledGamesTitleIds(JNIEnv* env, [[maybe_unused]] jclass clazz)
{
return JNIUtils::createJavaLongArrayList(env, CafeTitleList::GetAllTitleIds());

View File

@ -1,6 +0,0 @@
package info.cemu.Cemu.gameview;
import android.graphics.Bitmap;
public record Game(String path, String title, Bitmap icon) {
}

View File

@ -1,10 +1,12 @@
package info.cemu.Cemu.gameview;
import android.opengl.Visibility;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -18,24 +20,34 @@ import java.util.stream.Collectors;
import info.cemu.Cemu.R;
import info.cemu.Cemu.nativeinterface.NativeGameTitles.Game;
public class GameAdapter extends ListAdapter<Game, GameAdapter.ViewHolder> {
private final GameTitleClickAction gameTitleClickAction;
private List<Game> orignalGameList;
private String filterText;
private Game selectedGame;
public Game getSelectedGame() {
return selectedGame;
}
public static final DiffUtil.ItemCallback<Game> DIFF_CALLBACK = new DiffUtil.ItemCallback<>() {
@Override
public boolean areItemsTheSame(@NonNull Game oldItem, @NonNull Game newItem) {
return oldItem.path().equals(newItem.path());
return oldItem.titleId() == newItem.titleId();
}
@Override
public boolean areContentsTheSame(@NonNull Game oldItem, @NonNull Game newItem) {
return oldItem.path().equals(newItem.path());
return oldItem.path().equals(newItem.path()) &&
oldItem.titleId() == newItem.titleId() &&
oldItem.isFavorite() == newItem.isFavorite();
}
};
public interface GameTitleClickAction {
void action(String gamePath);
void action(Game game);
}
public GameAdapter(GameTitleClickAction gameTitleClickAction) {
@ -50,7 +62,7 @@ public class GameAdapter extends ListAdapter<Game, GameAdapter.ViewHolder> {
super.submitList(orignalGameList);
return;
}
super.submitList(orignalGameList.stream().filter(g -> g.title().toLowerCase(Locale.US).contains(this.filterText)).collect(Collectors.toList()));
super.submitList(orignalGameList.stream().filter(g -> g.name().toLowerCase(Locale.US).contains(this.filterText)).collect(Collectors.toList()));
}
@NonNull
@ -63,14 +75,15 @@ public class GameAdapter extends ListAdapter<Game, GameAdapter.ViewHolder> {
@Override
public void onBindViewHolder(@NonNull GameAdapter.ViewHolder holder, int position) {
Game game = getItem(position);
if (game != null) {
holder.icon.setImageBitmap(game.icon());
holder.text.setText(game.title());
holder.itemView.setOnClickListener(v -> {
String gamePath = game.path();
gameTitleClickAction.action(gamePath);
});
}
if (game == null) return;
holder.icon.setImageBitmap(game.icon());
holder.favoriteIcon.setVisibility(game.isFavorite() ? View.VISIBLE : View.GONE);
holder.text.setText(game.name());
holder.itemView.setOnClickListener(v -> gameTitleClickAction.action(game));
holder.itemView.setOnLongClickListener(v -> {
selectedGame = game;
return false;
});
}
public void setFilterText(String filterText) {
@ -82,17 +95,19 @@ public class GameAdapter extends ListAdapter<Game, GameAdapter.ViewHolder> {
super.submitList(orignalGameList);
return;
}
super.submitList(orignalGameList.stream().filter(g -> g.title().toLowerCase(Locale.US).contains(this.filterText)).collect(Collectors.toList()));
super.submitList(orignalGameList.stream().filter(g -> g.name().toLowerCase(Locale.US).contains(this.filterText)).collect(Collectors.toList()));
}
public static class ViewHolder extends RecyclerView.ViewHolder {
ImageView icon;
TextView text;
ImageView favoriteIcon;
public ViewHolder(View itemView) {
super(itemView);
icon = itemView.findViewById(R.id.game_icon);
text = itemView.findViewById(R.id.game_title);
favoriteIcon = itemView.findViewById(R.id.game_favorite_icon);
}
}
}

View File

@ -0,0 +1,72 @@
package info.cemu.Cemu.gameview;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import info.cemu.Cemu.R;
import info.cemu.Cemu.databinding.FragmentGameDetailsBinding;
import info.cemu.Cemu.nativeinterface.NativeGameTitles;
import info.cemu.Cemu.nativeinterface.NativeGameTitles.Game;
public class GameDetailsFragment extends Fragment {
private static final DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT);
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
FragmentGameDetailsBinding binding = FragmentGameDetailsBinding.inflate(inflater, container, false);
var game = new ViewModelProvider(requireActivity()).get(GameViewModel.class).getGame();
binding.gameTitleName.setText(game.name());
binding.titleVersion.setText(String.valueOf(game.version()));
if (game.icon() != null)
binding.titleIcon.setImageBitmap(game.icon());
if (game.dlc() != 0)
binding.titleDlc.setText(String.valueOf(game.dlc()));
binding.titleTimePlayed.setText(getTimePlayed(game));
binding.titleLastPlayed.setText(getLastPlayedDate(game));
binding.titleId.setText(String.valueOf(game.titleId()));
binding.titleRegion.setText(getRegionName(game));
NavigationUI.setupWithNavController(binding.gameDetailsToolbar, NavHostFragment.findNavController(this), new AppBarConfiguration.Builder().build());
return binding.getRoot();
}
private String getLastPlayedDate(Game game) {
if (game.lastPlayedYear() == 0) return getString(R.string.never_played);
LocalDate lastPlayedDate = LocalDate.of(game.lastPlayedYear(), game.lastPlayedMonth(), game.lastPlayedDay());
return dateFormatter.format(lastPlayedDate);
}
private String getTimePlayed(Game game) {
if (game.minutesPlayed() == 0) return getString(R.string.never_played);
if (game.minutesPlayed() < 60)
return getString(R.string.minutes_played, game.minutesPlayed());
return getString(R.string.hours_minutes_played, game.minutesPlayed() / 60, game.minutesPlayed() % 60);
}
private @StringRes() int getRegionName(Game game) {
return switch (game.region()) {
case NativeGameTitles.CONSOLE_REGION_JPN -> R.string.console_region_japan;
case NativeGameTitles.CONSOLE_REGION_USA -> R.string.console_region_usa;
case NativeGameTitles.CONSOLE_REGION_EUR -> R.string.console_region_europe;
case NativeGameTitles.CONSOLE_REGION_AUS_DEPR -> R.string.console_region_australia;
case NativeGameTitles.CONSOLE_REGION_CHN -> R.string.console_region_china;
case NativeGameTitles.CONSOLE_REGION_KOR -> R.string.console_region_korea;
case NativeGameTitles.CONSOLE_REGION_TWN -> R.string.console_region_taiwan;
case NativeGameTitles.CONSOLE_REGION_AUTO -> R.string.console_region_auto;
default -> R.string.console_region_many;
};
}
}

View File

@ -0,0 +1,67 @@
package info.cemu.Cemu.gameview;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeSet;
import info.cemu.Cemu.nativeinterface.NativeGameTitles;
import info.cemu.Cemu.nativeinterface.NativeGameTitles.Game;
public class GameListViewModel extends ViewModel {
private final MutableLiveData<List<Game>> gamesData;
private final TreeSet<Game> games = new TreeSet<>();
public LiveData<List<Game>> getGames() {
return gamesData;
}
public GameListViewModel() {
this.gamesData = new MutableLiveData<>();
NativeGameTitles.setGameTitleLoadedCallback(game -> {
synchronized (GameListViewModel.this) {
games.add(game);
gamesData.postValue(new ArrayList<>(games));
}
});
}
public void setGameTitleFavorite(Game game, boolean isFavorite) {
synchronized (this) {
if (!games.contains(game)) return;
NativeGameTitles.setGameTitleFavorite(game.titleId(), isFavorite);
games.remove(game);
Game newGame = new Game(
game.titleId(),
game.path(),
game.name(),
game.version(),
game.dlc(),
game.region(),
game.lastPlayedYear(),
game.lastPlayedMonth(),
game.lastPlayedDay(),
game.minutesPlayed(),
isFavorite,
game.icon()
);
games.add(newGame);
gamesData.postValue(new ArrayList<>(games));
}
}
@Override
protected void onCleared() {
NativeGameTitles.setGameTitleLoadedCallback(null);
}
public void refreshGames() {
games.clear();
gamesData.setValue(null);
NativeGameTitles.reloadGameTitles();
}
}

View File

@ -0,0 +1,96 @@
package info.cemu.Cemu.gameview;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI;
import java.util.Arrays;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import info.cemu.Cemu.R;
import info.cemu.Cemu.databinding.FragmentGameProfileEditBinding;
import info.cemu.Cemu.guibasecomponents.GenericRecyclerViewAdapter;
import info.cemu.Cemu.guibasecomponents.HeaderRecyclerViewItem;
import info.cemu.Cemu.guibasecomponents.SelectionAdapter;
import info.cemu.Cemu.guibasecomponents.SingleSelectionRecyclerViewItem;
import info.cemu.Cemu.guibasecomponents.ToggleRecyclerViewItem;
import info.cemu.Cemu.nativeinterface.NativeGameTitles;
public class GameProfileEditFragment extends Fragment {
private static @StringRes() int cpuModeToResourceNameId(int cpuMode) {
return switch (cpuMode) {
case NativeGameTitles.CPU_MODE_SINGLECOREINTERPRETER ->
R.string.cpu_mode_single_core_interpreter;
case NativeGameTitles.CPU_MODE_SINGLECORERECOMPILER ->
R.string.cpu_mode_single_core_recompiler;
case NativeGameTitles.CPU_MODE_MULTICORERECOMPILER ->
R.string.cpu_mode_multi_core_recompiler;
default -> R.string.cpu_mode_auto;
};
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
FragmentGameProfileEditBinding binding = FragmentGameProfileEditBinding.inflate(inflater, container, false);
var game = new ViewModelProvider(requireActivity()).get(GameViewModel.class).getGame();
long titleId = game.titleId();
GenericRecyclerViewAdapter genericRecyclerViewAdapter = new GenericRecyclerViewAdapter();
genericRecyclerViewAdapter.addRecyclerViewItem(new HeaderRecyclerViewItem(game.name()));
ToggleRecyclerViewItem loadSharedLibrariesToggle = new ToggleRecyclerViewItem("Load shared libraries",
"Load libraries from the cafeLibs directory", NativeGameTitles.isLoadingSharedLibrariesForTitleEnabled(titleId),
checked -> NativeGameTitles.setLoadingSharedLibrariesForTitleEnabled(titleId, checked));
genericRecyclerViewAdapter.addRecyclerViewItem(loadSharedLibrariesToggle);
ToggleRecyclerViewItem shaderMultiplicationAccuracyToggle = new ToggleRecyclerViewItem("Shader multiplication accuracy",
"Controls the accuracy of floating point multiplication in shaders", NativeGameTitles.isShaderMultiplicationAccuracyForTitleEnabled(titleId),
checked -> NativeGameTitles.setShaderMultiplicationAccuracyForTitleEnabled(titleId, checked));
genericRecyclerViewAdapter.addRecyclerViewItem(shaderMultiplicationAccuracyToggle);
int currentCpuMode = NativeGameTitles.getCpuModeForTitle(titleId);
var cpuModeChoices = Stream.of(NativeGameTitles.CPU_MODE_SINGLECOREINTERPRETER,
NativeGameTitles.CPU_MODE_SINGLECORERECOMPILER,
NativeGameTitles.CPU_MODE_MULTICORERECOMPILER,
NativeGameTitles.CPU_MODE_AUTO)
.map(cpuMode -> new SelectionAdapter.ChoiceItem<>(t -> t.setText(cpuModeToResourceNameId(cpuMode)), cpuMode))
.collect(Collectors.toList());
SelectionAdapter<Integer> cpuSelectionAdapter = new SelectionAdapter<>(cpuModeChoices, currentCpuMode);
SingleSelectionRecyclerViewItem<Integer> cpuModeSelection = new SingleSelectionRecyclerViewItem<>(getString(R.string.cpu_mode),
getString(cpuModeToResourceNameId(currentCpuMode)), cpuSelectionAdapter,
(cpuMode, selectionRecyclerViewItem) -> {
NativeGameTitles.setCpuModeForTitle(titleId, cpuMode);
selectionRecyclerViewItem.setDescription(getString(cpuModeToResourceNameId(cpuMode)));
});
genericRecyclerViewAdapter.addRecyclerViewItem(cpuModeSelection);
int currentThreadQuantum = NativeGameTitles.getThreadQuantumForTitle(titleId);
var threadQuantumChoices = Arrays.stream(NativeGameTitles.THREAD_QUANTUM_VALUES)
.mapToObj(threadQuantum -> new SelectionAdapter.ChoiceItem<>(t -> t.setText(String.valueOf(threadQuantum)), threadQuantum))
.collect(Collectors.toList());
SelectionAdapter<Integer> threadQuantumSelectionAdapter = new SelectionAdapter<>(threadQuantumChoices, currentThreadQuantum);
SingleSelectionRecyclerViewItem<Integer> threadQuantumSelection = new SingleSelectionRecyclerViewItem<>(getString(R.string.thread_quantum),
String.valueOf(currentThreadQuantum), threadQuantumSelectionAdapter,
(threadQuantum, selectionRecyclerViewItem) -> {
NativeGameTitles.setThreadQuantumForTitle(titleId, threadQuantum);
selectionRecyclerViewItem.setDescription(String.valueOf(threadQuantum));
});
genericRecyclerViewAdapter.addRecyclerViewItem(threadQuantumSelection);
binding.recyclerView.setAdapter(genericRecyclerViewAdapter);
NavigationUI.setupWithNavController(binding.gameEditProfileToolbar, NavHostFragment.findNavController(this), new AppBarConfiguration.Builder().build());
return binding.getRoot();
}
}

View File

@ -1,47 +1,19 @@
package info.cemu.Cemu.gameview;
import android.graphics.Bitmap;
import static info.cemu.Cemu.nativeinterface.NativeGameTitles.*;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import java.util.ArrayList;
import java.util.List;
import info.cemu.Cemu.nativeinterface.NativeGameTitles;
public class GameViewModel extends ViewModel {
private final MutableLiveData<List<Game>> gamesData;
private Game game = null;
private final ArrayList<Game> games = new ArrayList<>();
public LiveData<List<Game>> getGames() {
return gamesData;
public Game getGame() {
return game;
}
public GameViewModel() {
this.gamesData = new MutableLiveData<>();
NativeGameTitles.setGameTitleLoadedCallback((path, title, colors, width, height) -> {
Bitmap icon = null;
if (colors != null)
icon = Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888);
Game game = new Game(path, title, icon);
synchronized (GameViewModel.this) {
games.add(game);
gamesData.postValue(new ArrayList<>(games));
}
});
}
@Override
protected void onCleared() {
NativeGameTitles.setGameTitleLoadedCallback(null);
}
public void refreshGames() {
games.clear();
gamesData.setValue(null);
NativeGameTitles.reloadGameTitles();
public void setGame(Game game) {
this.game = game;
}
}

View File

@ -2,48 +2,117 @@ package info.cemu.Cemu.gameview;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import info.cemu.Cemu.R;
import info.cemu.Cemu.databinding.FragmentGamesBinding;
import info.cemu.Cemu.emulation.EmulationActivity;
import info.cemu.Cemu.nativeinterface.NativeGameTitles;
import info.cemu.Cemu.nativeinterface.NativeGameTitles.Game;
import info.cemu.Cemu.settings.SettingsActivity;
import info.cemu.Cemu.settings.SettingsFragment;
public class GamesFragment extends Fragment {
private GameAdapter gameAdapter;
private GameListViewModel gameListViewModel;
private GameViewModel gameViewModel;
private boolean refreshing = false;
private final Handler handler = new Handler(Looper.getMainLooper());
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
gameAdapter = new GameAdapter(titleId -> {
gameAdapter = new GameAdapter(game -> {
Intent intent = new Intent(getContext(), EmulationActivity.class);
intent.putExtra(EmulationActivity.LAUNCH_PATH, titleId);
intent.putExtra(EmulationActivity.LAUNCH_PATH, game.path());
startActivity(intent);
});
gameViewModel = new ViewModelProvider(this).get(GameViewModel.class);
gameViewModel.getGames().observe(this, gameList -> gameAdapter.submitList(gameList));
gameListViewModel = new ViewModelProvider(this).get(GameListViewModel.class);
gameViewModel = new ViewModelProvider(requireActivity()).get(GameViewModel.class);
gameListViewModel.getGames().observe(this, gameList -> gameAdapter.submitList(gameList));
NativeGameTitles.reloadGameTitles();
}
@Override
public void onResume() {
super.onResume();
gameViewModel.refreshGames();
}
@Override
public void onCreateContextMenu(@NonNull ContextMenu menu, @NonNull View v, @Nullable ContextMenu.ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
MenuInflater inflater = requireActivity().getMenuInflater();
inflater.inflate(R.menu.menu_game, menu);
Game selectedGame = gameAdapter.getSelectedGame();
menu.findItem(R.id.favorite).setChecked(selectedGame.isFavorite());
menu.findItem(R.id.remove_shader_caches).setEnabled(NativeGameTitles.titleHasShaderCacheFiles(selectedGame.titleId()));
}
@Override
public boolean onContextItemSelected(@NonNull MenuItem item) {
Game game = gameAdapter.getSelectedGame();
if (game == null) {
return super.onContextItemSelected(item);
}
int itemId = item.getItemId();
if (itemId == R.id.favorite) {
gameListViewModel.setGameTitleFavorite(game, !game.isFavorite());
return true;
}
if (itemId == R.id.game_profile) {
gameViewModel.setGame(game);
NavHostFragment.findNavController(this).navigate(R.id.action_games_fragment_to_game_edit_profile);
return true;
}
if (itemId == R.id.remove_shader_caches) {
removeShaderCachesForGame(game);
return true;
}
if (itemId == R.id.about_title) {
gameViewModel.setGame(game);
NavHostFragment.findNavController(this).navigate(R.id.action_games_fragment_to_game_details_fragment);
return true;
}
return super.onContextItemSelected(item);
}
private void removeShaderCachesForGame(Game game) {
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext());
builder.setTitle(R.string.remove_shader_caches)
.setMessage(getString(R.string.remove_shader_caches_message, game.name()))
.setPositiveButton(R.string.yes, (dialog, which) -> {
NativeGameTitles.removeShaderCacheFilesForTitle(game.titleId());
Toast.makeText(requireContext(), R.string.shader_caches_removed_notification, Toast.LENGTH_SHORT).show();
})
.setNegativeButton(R.string.no, (dialog, which) -> dialog.dismiss())
.show();
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
FragmentGamesBinding binding = FragmentGamesBinding.inflate(inflater, container, false);
RecyclerView recyclerView = binding.gamesRecyclerView;
registerForContextMenu(recyclerView);
binding.settingsButton.setOnClickListener(v -> {
Intent intent = new Intent(requireActivity(), SettingsActivity.class);
startActivity(intent);
@ -51,8 +120,13 @@ public class GamesFragment extends Fragment {
View rootView = binding.getRoot();
binding.gamesSwipeRefresh.setOnRefreshListener(() -> {
gameViewModel.refreshGames();
binding.gamesSwipeRefresh.setRefreshing(false);
if (refreshing) return;
refreshing = true;
handler.postDelayed(() -> {
binding.gamesSwipeRefresh.setRefreshing(false);
refreshing = false;
}, 1000);
gameListViewModel.refreshGames();
});
recyclerView.setAdapter(gameAdapter);
binding.searchText.addTextChangedListener(new TextWatcher() {

View File

@ -7,6 +7,8 @@ import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import java.util.Optional;
import info.cemu.Cemu.R;
public class HeaderRecyclerViewItem implements RecyclerViewItem {
@ -20,10 +22,17 @@ public class HeaderRecyclerViewItem implements RecyclerViewItem {
}
}
private final int headerResourceIdText;
private final Optional<Integer> headerResourceIdText;
private final String headerText;
public HeaderRecyclerViewItem(int headerResourceIdText) {
this.headerResourceIdText = headerResourceIdText;
this.headerResourceIdText = Optional.of(headerResourceIdText);
this.headerText = null;
}
public HeaderRecyclerViewItem(String headerText) {
this.headerResourceIdText = Optional.empty();
this.headerText = headerText;
}
@Override
@ -34,6 +43,10 @@ public class HeaderRecyclerViewItem implements RecyclerViewItem {
@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, RecyclerView.Adapter<RecyclerView.ViewHolder> adapter) {
HeaderViewHolder headerViewHolder = (HeaderViewHolder) viewHolder;
headerViewHolder.header.setText(headerResourceIdText);
if (headerResourceIdText.isEmpty()) {
headerViewHolder.header.setText(headerText);
return;
}
headerViewHolder.header.setText(headerResourceIdText.get());
}
}

View File

@ -0,0 +1,61 @@
package info.cemu.Cemu.guibasecomponents;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.materialswitch.MaterialSwitch;
import info.cemu.Cemu.R;
public class ToggleRecyclerViewItem implements RecyclerViewItem {
public interface OnCheckedChangeListener {
void onCheckChanged(boolean checked);
}
private static class ToggleViewHolder extends RecyclerView.ViewHolder {
TextView label;
TextView description;
MaterialSwitch toggle;
public ToggleViewHolder(View itemView) {
super(itemView);
label = itemView.findViewById(R.id.toggle_label);
description = itemView.findViewById(R.id.toggle_description);
toggle = itemView.findViewById(R.id.toggle);
}
}
private final String label;
private final String description;
private boolean checked;
private final OnCheckedChangeListener onCheckedChangeListener;
public ToggleRecyclerViewItem(String label, String description, boolean checked, OnCheckedChangeListener onCheckedChangeListener) {
this.label = label;
this.description = description;
this.checked = checked;
this.onCheckedChangeListener = onCheckedChangeListener;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) {
return new ToggleViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_toggle, parent, false));
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, RecyclerView.Adapter<RecyclerView.ViewHolder> adapter) {
ToggleViewHolder toggleViewHolder = (ToggleViewHolder) viewHolder;
toggleViewHolder.label.setText(label);
toggleViewHolder.description.setText(description);
toggleViewHolder.toggle.setChecked(checked);
toggleViewHolder.itemView.setOnClickListener(view -> {
checked = !toggleViewHolder.toggle.isChecked();
toggleViewHolder.toggle.setChecked(checked);
if (onCheckedChangeListener != null) onCheckedChangeListener.onCheckChanged(checked);
});
}
}

View File

@ -1,6 +1,5 @@
package info.cemu.Cemu.inputoverlay;
import android.content.Context;
import android.graphics.Canvas;
import android.os.VibrationEffect;
@ -41,18 +40,19 @@ public class InputOverlaySurfaceView extends SurfaceView implements View.OnTouch
}
public void setInputMode(InputMode inputMode) {
if (inputs != null) {
if (this.inputMode != InputMode.DEFAULT) {
for (var input : inputs) {
input.saveConfiguration();
}
} else {
for (var input : inputs) {
input.reset();
}
}
}
this.inputMode = inputMode;
if (inputs == null) {
return;
}
if (this.inputMode != InputMode.DEFAULT) {
for (var input : inputs) {
input.saveConfiguration();
}
return;
}
for (var input : inputs) {
input.reset();
}
}
public InputMode getInputMode() {
@ -267,10 +267,21 @@ public class InputOverlaySurfaceView extends SurfaceView implements View.OnTouch
}
void onJoystickStateChange(OverlayJoystick joystick, float x, float y) {
float up = y < 0 ? -y : 0;
float down = y > 0 ? y : 0;
float left = x < 0 ? -x : 0;
float right = x > 0 ? x : 0;
float up, down, left, right;
if (y < 0) {
up = -y;
down = 0;
} else {
up = 0;
down = y;
}
if (x < 0) {
left = -x;
right = 0;
} else {
left = 0;
right = x;
}
switch (nativeControllerType) {
case NativeInput.EMULATED_CONTROLLER_TYPE_VPAD ->
onVPADJoystickStateChange(joystick, up, down, left, right);

View File

@ -1,12 +1,87 @@
package info.cemu.Cemu.nativeinterface;
import android.graphics.Bitmap;
import java.util.ArrayList;
public class NativeGameTitles {
public interface GameTitleLoadedCallback {
void onGameTitleLoaded(String path, String title, int[] colors, int width, int height);
public static final int CONSOLE_REGION_JPN = 0x1;
public static final int CONSOLE_REGION_USA = 0x2;
public static final int CONSOLE_REGION_EUR = 0x4;
public static final int CONSOLE_REGION_AUS_DEPR = 0x8;
public static final int CONSOLE_REGION_CHN = 0x10;
public static final int CONSOLE_REGION_KOR = 0x20;
public static final int CONSOLE_REGION_TWN = 0x40;
public static final int CONSOLE_REGION_AUTO = 0xFF;
public record Game(
long titleId,
String path,
String name,
short version,
short dlc,
int region,
short lastPlayedYear,
short lastPlayedMonth,
short lastPlayedDay,
int minutesPlayed,
boolean isFavorite,
Bitmap icon
) implements Comparable<Game> {
@Override
public int compareTo(Game other) {
if (titleId == other.titleId) {
return 0;
}
if (isFavorite && !other.isFavorite) {
return -1;
}
if (!isFavorite && other.isFavorite) {
return 1;
}
return name.compareTo(other.name);
}
}
public interface GameTitleLoadedCallback {
void onGameTitleLoaded(Game game);
}
public static native boolean isLoadingSharedLibrariesForTitleEnabled(long gameTitleId);
public static native void setLoadingSharedLibrariesForTitleEnabled(long gameTitleId, boolean enabled);
public final static int CPU_MODE_SINGLECOREINTERPRETER = 0;
public final static int CPU_MODE_SINGLECORERECOMPILER = 1;
public final static int CPU_MODE_MULTICORERECOMPILER = 3;
public final static int CPU_MODE_AUTO = 4;
public static native int getCpuModeForTitle(long gameTitleId);
public static native void setCpuModeForTitle(long gameTitleId, int cpuMode);
public static final int[] THREAD_QUANTUM_VALUES = new int[]{
20000,
45000,
60000,
80000,
100000,
};
public static native int getThreadQuantumForTitle(long gameTitleId);
public static native void setThreadQuantumForTitle(long gameTitleId, int threadQuantum);
public static native boolean isShaderMultiplicationAccuracyForTitleEnabled(long gameTitleId);
public static native void setShaderMultiplicationAccuracyForTitleEnabled(long gameTitleId, boolean enabled);
public static native boolean titleHasShaderCacheFiles(long gameTitleId);
public static native void removeShaderCacheFilesForTitle(long gameTitleId);
public static native void setGameTitleFavorite(long gameTitleId, boolean isFavorite);
public static native void setGameTitleLoadedCallback(GameTitleLoadedCallback gameTitleLoadedCallback);
public static native void reloadGameTitles();

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M320,720L480,598L640,720L580,522L740,408L544,408L480,200L416,408L220,408L380,522L320,720ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>

View File

@ -13,5 +13,5 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />
app:navGraph="@navigation/nav_main_graph" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,148 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".gameview.GameDetailsFragment">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/game_details_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="none">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<com.google.android.material.card.MaterialCardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="8dp"
app:cardCornerRadius="8dp">
<ImageView
android:id="@+id/title_icon"
android:layout_width="128dp"
android:layout_height="128dp"
android:contentDescription="@string/game_icon"
android:scaleType="fitXY"
tools:ignore="ImageContrastCheck" />
</com.google.android.material.card.MaterialCardView>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp"
android:text="@string/title_name"
android:textSize="24sp"
android:textStyle="bold" />
<TextView
android:id="@+id/game_title_name"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="8dp"
android:textSize="16sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp"
android:text="@string/title_id"
android:textSize="24sp"
android:textStyle="bold" />
<TextView
android:id="@+id/title_id"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="8dp"
android:textSize="16sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp"
android:text="@string/version"
android:textSize="24sp"
android:textStyle="bold" />
<TextView
android:id="@+id/title_version"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="8dp"
android:textSize="16sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp"
android:text="@string/dlc"
android:textSize="24sp"
android:textStyle="bold" />
<TextView
android:id="@+id/title_dlc"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="8dp"
android:textSize="16sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp"
android:text="@string/title_time_played"
android:textSize="24sp"
android:textStyle="bold" />
<TextView
android:id="@+id/title_time_played"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="8dp"
android:textSize="16sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp"
android:text="@string/title_last_played"
android:textSize="24sp"
android:textStyle="bold" />
<TextView
android:id="@+id/title_last_played"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="8dp"
android:textSize="16sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp"
android:text="@string/title_region"
android:textSize="24sp"
android:textStyle="bold" />
<TextView
android:id="@+id/title_region"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="8dp"
android:textSize="16sp" />
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".gameview.GameProfileEditFragment">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/game_edit_profile_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="8dp"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</LinearLayout>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
@ -7,12 +8,37 @@
android:orientation="horizontal"
android:padding="8dp">
<ImageView
android:id="@+id/game_icon"
android:layout_width="60dp"
android:layout_height="60dp"
android:contentDescription="@string/game_icon"
android:scaleType="fitXY" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.google.android.material.card.MaterialCardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardCornerRadius="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/game_icon"
android:layout_width="60dp"
android:layout_height="60dp"
android:contentDescription="@string/game_icon"
android:scaleType="fitXY" />
</com.google.android.material.card.MaterialCardView>
<ImageView
android:id="@+id/game_favorite_icon"
android:layout_width="32dp"
android:layout_height="32dp"
android:contentDescription="@string/game_favorite_description"
android:src="@drawable/ic_favorite"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?android:attr/colorPrimary" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.textview.MaterialTextView
android:id="@+id/game_title"

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true">
<TextView
android:id="@+id/toggle_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginHorizontal="8dp"
android:layout_marginTop="8dp"
android:layout_toStartOf="@+id/toggle"
android:text="Toggle label"
android:textAppearance="?attr/textAppearanceHeadline6"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/toggle_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/toggle_label"
android:layout_alignStart="@+id/toggle_label"
android:layout_alignParentStart="true"
android:layout_margin="8dp"
android:layout_toStartOf="@+id/toggle"
android:text="Toggle description"
android:textAlignment="textStart"
android:textAppearance="?attr/textAppearanceSubtitle1"
tools:ignore="HardcodedText" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginVertical="8dp"
android:layout_marginEnd="8dp"
android:clickable="false"
android:focusable="false" />
</RelativeLayout>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/favorite"
android:checkable="true"
android:title="Favorite" />
<item
android:id="@+id/game_profile"
android:title="Edit game profile" />
<item
android:id="@+id/remove_shader_caches"
android:title="Remove shader caches" />
<item
android:id="@+id/about_title"
android:title="About title" />
</menu>

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation 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/nav_graph"
app:startDestination="@id/gamesFragment">
<fragment
android:id="@+id/gamesFragment"
android:name="info.cemu.Cemu.gameview.GamesFragment"
android:label="GamesFragment" />
</navigation>

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation 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/nav_graph"
app:startDestination="@id/games_fragment">
<fragment
android:id="@+id/games_fragment"
android:name="info.cemu.Cemu.gameview.GamesFragment"
tools:layout="@layout/fragment_games">
<action
android:id="@+id/action_games_fragment_to_game_details_fragment"
app:destination="@id/game_details_fragment" />
<action
android:id="@+id/action_games_fragment_to_game_edit_profile"
app:destination="@id/game_edit_profile" />
</fragment>
<fragment
android:id="@+id/game_details_fragment"
android:name="info.cemu.Cemu.gameview.GameDetailsFragment"
android:label="@string/about_title"
tools:layout="@layout/fragment_game_details" />
<fragment
android:id="@+id/game_edit_profile"
android:name="info.cemu.Cemu.gameview.GameProfileEditFragment"
android:label="@string/edit_game_profile"
tools:layout="@layout/fragment_game_profile_edit" />
</navigation>

View File

@ -191,4 +191,35 @@
<string name="notifications_text_scale">Notifications text scale</string>
<string name="exit">Exit</string>
<string name="show_input_overlay">Show input overlay</string>
<string name="game_favorite_description">Game is marked as favorite</string>
<string name="remove_shader_caches">Remove shader caches</string>
<string name="remove_shader_caches_message">Remove the shader caches for %1$s?</string>
<string name="minutes_played">Minutes: %d</string>
<string name="hours_minutes_played">Hours: %1$d Minutes: %2$d</string>
<string name="never_played">Never played</string>
<string name="console_region_japan">Japan</string>
<string name="console_region_usa">USA</string>
<string name="console_region_europe">Europe</string>
<string name="console_region_australia">Australia</string>
<string name="console_region_china">China</string>
<string name="console_region_korea">Korea</string>
<string name="console_region_taiwan">Taiwan</string>
<string name="console_region_auto">Auto</string>
<string name="console_region_many">Many</string>
<string name="shader_caches_removed_notification">Shader caches removed</string>
<string name="about_title">About title</string>
<string name="title_name">Title name</string>
<string name="title_id">Title ID</string>
<string name="version">Version</string>
<string name="dlc">DLC</string>
<string name="title_time_played">You\'ve played</string>
<string name="title_last_played">Last played</string>
<string name="title_region">Region</string>
<string name="edit_game_profile">Edit game profile</string>
<string name="cpu_mode_auto">Auto (recommended)</string>
<string name="cpu_mode_single_core_interpreter">Single-core interpreter</string>
<string name="cpu_mode_single_core_recompiler">Single-core recompiler</string>
<string name="cpu_mode_multi_core_recompiler">Multi-core recompiler</string>
<string name="cpu_mode">CPU mode</string>
<string name="thread_quantum">Thread quantum</string>
</resources>