Added graphic packs settings

This commit is contained in:
SSimco
2024-04-07 23:33:49 +03:00
parent 275a46f97f
commit 3c916747af
32 changed files with 1509 additions and 64 deletions
+5 -2
View File
@@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name="info.cemu.Cemu.CemuApplication"
android:allowBackup="true"
@@ -17,11 +19,13 @@
<activity
android:name=".EmulationActivity"
android:launchMode="singleTop"
android:parentActivityName=".MainActivity"
android:screenOrientation="userLandscape" />
<activity
android:name=".SettingsActivity"
android:exported="false" />
android:exported="false"
android:parentActivityName=".MainActivity" />
<activity
android:name=".MainActivity"
android:exported="true"
@@ -33,5 +37,4 @@
</intent-filter>
</activity>
</application>
</manifest>
+59 -23
View File
@@ -8,6 +8,7 @@
#include "Cafe/HW/Latte/Renderer/Vulkan/VulkanAPI.h"
#include "Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.h"
#include "CafeSystemUtils.h"
#include "Cafe/CafeSystem.h"
#include "Cemu/GuiSystem/GuiSystem.h"
#include "GameIconLoader.h"
#include "GameTitleLoader.h"
@@ -19,24 +20,34 @@
int mainEmulatorHLE();
class EmulationState
{
class EmulationState {
GameIconLoader m_gameIconLoader;
GameTitleLoader m_gameTitleLoader;
std::unordered_map<int64_t, GraphicPackPtr> m_graphicPacks;
void fillGraphicPacks()
{
m_graphicPacks.clear();
auto graphicPacks = GraphicPack2::GetGraphicPacks();
for (auto&& graphicPack : graphicPacks)
{
m_graphicPacks[reinterpret_cast<int64_t>(graphicPack.get())] = graphicPack;
}
}
public:
public:
void initializeEmulation()
{
g_config.Load();
FilesystemAndroid::setFilesystemCallbacks(std::make_shared<AndroidFilesystemCallbacks>());
NetworkConfig::LoadOnce();
mainEmulatorHLE();
InputManager::instance().load();
InitializeGlobalVulkan();
createCemuDirectories();
g_config.Load();
mainEmulatorHLE();
fillGraphicPacks();
}
void initializeActiveSettings(const fs::path &dataPath, const fs::path &cachePath)
void initializeActiveSettings(const fs::path& dataPath, const fs::path& cachePath)
{
ActiveSettings::LoadOnce({}, dataPath, dataPath, cachePath, dataPath);
}
@@ -51,12 +62,12 @@ class EmulationState
VulkanRenderer::GetInstance()->NotifySurfaceChanged(isMainCanvas);
}
void setSurface(JNIEnv *env, jobject surface, bool isMainCanvas)
void setSurface(JNIEnv* env, jobject surface, bool isMainCanvas)
{
auto &windowHandleInfo = isMainCanvas ? GuiSystem::getWindowInfo().canvas_main : GuiSystem::getWindowInfo().canvas_pad;
auto& windowHandleInfo = isMainCanvas ? GuiSystem::getWindowInfo().canvas_main : GuiSystem::getWindowInfo().canvas_pad;
if (windowHandleInfo.surface)
{
ANativeWindow_release(static_cast<ANativeWindow *>(windowHandleInfo.surface));
ANativeWindow_release(static_cast<ANativeWindow*>(windowHandleInfo.surface));
windowHandleInfo.surface = nullptr;
}
if (surface)
@@ -67,7 +78,7 @@ class EmulationState
void setSurfaceSize(int width, int height, bool isMainCanvas)
{
auto &windowInfo = GuiSystem::getWindowInfo();
auto& windowInfo = GuiSystem::getWindowInfo();
if (isMainCanvas)
{
windowInfo.width = windowInfo.phys_width = width;
@@ -80,17 +91,17 @@ class EmulationState
}
}
void onKeyEvent(const std::string &deviceDescriptor, const std::string &deviceName, int keyCode, bool isPressed)
void onKeyEvent(const std::string& deviceDescriptor, const std::string& deviceName, int keyCode, bool isPressed)
{
auto apiProvider = InputManager::instance().get_api_provider(InputAPI::Android);
auto androidControllerProvider = dynamic_cast<AndroidControllerProvider *>(apiProvider.get());
auto androidControllerProvider = dynamic_cast<AndroidControllerProvider*>(apiProvider.get());
androidControllerProvider->on_key_event(deviceDescriptor, deviceName, keyCode, isPressed);
}
void onAxisEvent(const std::string &deviceDescriptor, const std::string &deviceName, int axisCode, float value)
void onAxisEvent(const std::string& deviceDescriptor, const std::string& deviceName, int axisCode, float value)
{
auto apiProvider = InputManager::instance().get_api_provider(InputAPI::Android);
auto androidControllerProvider = dynamic_cast<AndroidControllerProvider *>(apiProvider.get());
auto androidControllerProvider = dynamic_cast<AndroidControllerProvider*>(apiProvider.get());
androidControllerProvider->on_axis_event(deviceDescriptor, deviceName, axisCode, value);
}
@@ -120,7 +131,7 @@ class EmulationState
{
auto emulatedController = AndroidEmulatedController::getAndroidEmulatedController(
i)
.getEmulatedController();
.getEmulatedController();
if (!emulatedController)
continue;
if (emulatedController->type() != EmulatedController::Type::VPAD)
@@ -144,7 +155,7 @@ class EmulationState
void setEmulatedControllerType(size_t index, EmulatedController::Type type)
{
auto &androidEmulatedController = AndroidEmulatedController::getAndroidEmulatedController(index);
auto& androidEmulatedController = AndroidEmulatedController::getAndroidEmulatedController(index);
if (EmulatedController::Type::VPAD <= type && type < EmulatedController::Type::MAX)
androidEmulatedController.setType(type);
else
@@ -166,7 +177,7 @@ class EmulationState
void setDPI(float dpi)
{
auto &windowInfo = GuiSystem::getWindowInfo();
auto& windowInfo = GuiSystem::getWindowInfo();
windowInfo.dpi_scale = windowInfo.pad_dpi_scale = dpi;
}
@@ -180,7 +191,7 @@ class EmulationState
return AndroidEmulatedController::getAndroidEmulatedController(index).getMappings();
}
void setControllerMapping(const std::string &deviceDescriptor, const std::string &deviceName, size_t index, uint64 mappingId, uint64 buttonId)
void setControllerMapping(const std::string& deviceDescriptor, const std::string& deviceName, size_t index, uint64 mappingId, uint64 buttonId)
{
auto apiProvider = InputManager::instance().get_api_provider(InputAPI::Android);
auto controller = ControllerFactory::CreateController(InputAPI::Android, deviceDescriptor, deviceName);
@@ -188,7 +199,7 @@ class EmulationState
}
void initializeAudioDevices()
{
auto &config = g_config.data();
auto& config = g_config.data();
if (!config.tv_device.empty())
AndroidAudio::createAudioDevice(IAudioAPI::AudioAPI::Cubeb, config.tv_channels, config.tv_volume, true);
@@ -196,22 +207,22 @@ class EmulationState
AndroidAudio::createAudioDevice(IAudioAPI::AudioAPI::Cubeb, config.pad_channels, config.pad_volume, false);
}
void setOnGameTitleLoaded(const std::shared_ptr<GameTitleLoadedCallback> &onGameTitleLoaded)
void setOnGameTitleLoaded(const std::shared_ptr<GameTitleLoadedCallback>& onGameTitleLoaded)
{
m_gameTitleLoader.setOnTitleLoaded(onGameTitleLoaded);
}
const Image &getGameIcon(TitleId titleId)
const Image& getGameIcon(TitleId titleId)
{
return m_gameIconLoader.getGameIcon(titleId);
}
void setOnGameIconLoaded(const std::shared_ptr<class GameIconLoadedCallback> &onGameIconLoaded)
void setOnGameIconLoaded(const std::shared_ptr<class GameIconLoadedCallback>& onGameIconLoaded)
{
m_gameIconLoader.setOnIconLoaded(onGameIconLoaded);
}
void addGamePath(const fs::path &gamePath)
void addGamePath(const fs::path& gamePath)
{
m_gameTitleLoader.addGamePath(gamePath);
}
@@ -231,4 +242,29 @@ class EmulationState
initializeAudioDevices();
CafeSystemUtils::startGame(titleId);
}
void refreshGraphicPacks()
{
if (!CafeSystem::IsTitleRunning())
{
GraphicPack2::ClearGraphicPacks();
GraphicPack2::LoadAll();
fillGraphicPacks();
}
}
const std::unordered_map<int64_t, GraphicPackPtr>& getGraphicPacks() const
{
return m_graphicPacks;
}
void setEnabledStateForGraphicPack(int64_t id, bool state)
{
m_graphicPacks.at(id)->SetEnabled(state);
}
GraphicPackPtr getGraphicPack(int64_t id) const
{
return m_graphicPacks.at(id);
}
};
+109
View File
@@ -1,3 +1,4 @@
#include "Cafe/GraphicPack/GraphicPack2.h"
#include "AndroidGameIconLoadedCallback.h"
#include "AndroidGameTitleLoadedCallback.h"
#include "EmulationState.h"
@@ -292,3 +293,111 @@ Java_info_cemu_Cemu_NativeLibrary_setAudioDeviceVolume([[maybe_unused]] JNIEnv *
deviceVolume = volume;
g_config.Save();
}
extern "C" JNIEXPORT void JNICALL
Java_info_cemu_Cemu_NativeLibrary_refreshGraphicPacks([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz)
{
s_emulationState.refreshGraphicPacks();
}
extern "C" JNIEXPORT jobject JNICALL
Java_info_cemu_Cemu_NativeLibrary_getGraphicPackIdsAndVirtualPaths(JNIEnv* env, [[maybe_unused]] jclass clazz)
{
auto graphicPackInfoClass = env->FindClass("info/cemu/Cemu/NativeLibrary$GraphicPackIdAndVirtualPath");
auto graphicPackInfoCtorId = env->GetMethodID(graphicPackInfoClass, "<init>", "(JLjava/lang/String;)V");
auto graphicPacks = s_emulationState.getGraphicPacks();
std::vector<jobject> graphicPackInfoJObjects;
for (auto&& graphicPack : graphicPacks)
{
jstring virtualPath = env->NewStringUTF(graphicPack.second->GetVirtualPath().c_str());
jlong id = graphicPack.first;
jobject jGraphicPack = env->NewObject(graphicPackInfoClass, graphicPackInfoCtorId, id, virtualPath);
graphicPackInfoJObjects.push_back(jGraphicPack);
}
return JNIUtils::createArrayList(env, graphicPackInfoJObjects);
}
jobject getGraphicPresets(JNIEnv* env, GraphicPackPtr graphicPack, int64_t id)
{
auto graphicPackPresetClass = env->FindClass("info/cemu/Cemu/NativeLibrary$GraphicPackPreset");
auto graphicPackPresetCtorId = env->GetMethodID(graphicPackPresetClass, "<init>", "(JLjava/lang/String;Ljava/util/ArrayList;Ljava/lang/String;)V");
std::vector<std::string> order;
auto presets = graphicPack->GetCategorizedPresets(order);
std::vector<jobject> presetsJobjects;
for (const auto& category : order)
{
const auto& entry = presets[category];
// test if any preset is visible and update its status
if (std::none_of(entry.cbegin(), entry.cend(), [graphicPack](const auto& p) { return p->visible; }))
{
continue;
}
jstring categoryJStr = category.empty() ? nullptr : env->NewStringUTF(category.c_str());
std::vector<std::string> presetSelections;
std::optional<std::string> activePreset;
for (auto& pentry : entry)
{
if (!pentry->visible)
continue;
presetSelections.push_back(pentry->name);
if (pentry->active)
activePreset = pentry->name;
}
jstring activePresetJstr = nullptr;
if (activePreset)
activePresetJstr = env->NewStringUTF(activePreset->c_str());
else if (!presetSelections.empty())
activePresetJstr = env->NewStringUTF(presetSelections.front().c_str());
auto presetJObject = env->NewObject(graphicPackPresetClass,
graphicPackPresetCtorId,
id,
categoryJStr,
JNIUtils::createJavaStringArrayList(env, presetSelections),
activePresetJstr);
presetsJobjects.push_back(presetJObject);
}
return JNIUtils::createArrayList(env, presetsJobjects);
}
extern "C" JNIEXPORT jobject JNICALL
Java_info_cemu_Cemu_NativeLibrary_getGraphicPack(JNIEnv* env, [[maybe_unused]] jclass clazz, jlong id)
{
auto graphicPackClass = env->FindClass("info/cemu/Cemu/NativeLibrary$GraphicPack");
auto graphicPackCtorId = env->GetMethodID(graphicPackClass, "<init>", "(JZLjava/lang/String;Ljava/lang/String;Ljava/util/ArrayList;)V");
auto graphicPack = s_emulationState.getGraphicPack(id);
jstring graphicPackName = env->NewStringUTF(graphicPack->GetName().c_str());
jstring graphicPackDescription = env->NewStringUTF(graphicPack->GetDescription().c_str());
return env->NewObject(graphicPackClass,
graphicPackCtorId,
id,
graphicPack->IsEnabled(),
graphicPackName,
graphicPackDescription,
getGraphicPresets(env, graphicPack, id));
}
extern "C" JNIEXPORT void JNICALL
Java_info_cemu_Cemu_NativeLibrary_setGraphicPackActive([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jlong id, jboolean active)
{
s_emulationState.setEnabledStateForGraphicPack(id, active);
}
extern "C" JNIEXPORT void JNICALL
Java_info_cemu_Cemu_NativeLibrary_setGraphicPackActivePreset([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jlong id, jstring category, jstring preset)
{
std::string presetCategory = category == nullptr ? "" : JNIUtils::JStringToString(env, category);
s_emulationState.setGraphicPackActivePreset(id, presetCategory, JNIUtils::JStringToString(env, preset));
}
extern "C" JNIEXPORT jobject JNICALL
Java_info_cemu_Cemu_NativeLibrary_getGraphicPackPresets(JNIEnv* env, [[maybe_unused]] jclass clazz, jlong id)
{
return getGraphicPresets(env, s_emulationState.getGraphicPack(id), id);
}
@@ -0,0 +1,48 @@
package info.cemu.Cemu;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import com.google.android.material.radiobutton.MaterialRadioButton;
import java.util.ArrayList;
import java.util.List;
import java.util.OptionalInt;
import java.util.stream.IntStream;
public abstract class BaseSelectionAdapter<T> extends BaseAdapter {
protected int selectedPosition = 0;
public BaseSelectionAdapter() {
super();
}
public void setSelectedValue(T selectedValue) {
selectedPosition = getPositionOf(selectedValue);
}
public abstract int getPositionOf(T value);
abstract protected void setRadioButtonText(MaterialRadioButton radioButton, int position);
public abstract T getItem(int position);
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View view, ViewGroup viewGroup) {
if (view == null)
view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.layout_single_selection_item, viewGroup, false);
MaterialRadioButton radioButton = view.findViewById(R.id.single_selection_item_radio_button);
radioButton.setEnabled(isEnabled(position));
setRadioButtonText(radioButton, position);
radioButton.setChecked(position == selectedPosition);
return view;
}
}
@@ -0,0 +1,71 @@
package info.cemu.Cemu;
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.dialog.MaterialAlertDialogBuilder;
public abstract class BaseSingleSelectionRecyclerViewItem<T> implements RecyclerViewItem {
private static class SingleSelectionViewHolder extends RecyclerView.ViewHolder {
TextView label;
TextView description;
public SingleSelectionViewHolder(View itemView) {
super(itemView);
label = itemView.findViewById(R.id.selection_label);
description = itemView.findViewById(R.id.selection_description);
}
}
public interface OnItemSelectedListener<T> {
void onItemSelected(T selectedValue, BaseSingleSelectionRecyclerViewItem<T> selectionRecyclerViewItem);
}
private final SelectionAdapter<T> selectionAdapter;
private final String label;
private String description;
private final OnItemSelectedListener<T> onItemSelectedListener;
private SingleSelectionViewHolder singleSelectionViewHolder;
private RecyclerView.Adapter<RecyclerView.ViewHolder> recyclerViewAdapter;
public BaseSingleSelectionRecyclerViewItem(String label, String description, SelectionAdapter<T> selectionAdapter, OnItemSelectedListener<T> onItemSelectedListener) {
this.label = label;
this.description = description;
this.selectionAdapter = selectionAdapter;
this.onItemSelectedListener = onItemSelectedListener;
}
public void setDescription(String description) {
this.description = description;
recyclerViewAdapter.notifyItemChanged(singleSelectionViewHolder.getAdapterPosition());
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) {
return new SingleSelectionViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_single_selection, parent, false));
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, RecyclerView.Adapter<RecyclerView.ViewHolder> recyclerViewAdapter) {
this.recyclerViewAdapter = recyclerViewAdapter;
singleSelectionViewHolder = (SingleSelectionViewHolder) viewHolder;
singleSelectionViewHolder.label.setText(label);
singleSelectionViewHolder.description.setText(description);
singleSelectionViewHolder.itemView.setOnClickListener(view -> {
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(viewHolder.itemView.getContext());
builder.setTitle(label).setAdapter(selectionAdapter, (dialogInterface, position) -> {
if (!selectionAdapter.isEnabled(position))
return;
T selectedValue = selectionAdapter.getItem(position);
selectionAdapter.setSelectedValue(selectedValue);
if (onItemSelectedListener != null)
onItemSelectedListener.onItemSelected(selectedValue, BaseSingleSelectionRecyclerViewItem.this);
}).setNegativeButton(R.string.cancel, (dialogInterface, i) -> dialogInterface.dismiss()).show();
});
}
}
@@ -1,17 +1,15 @@
package info.cemu.Cemu;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract;
import android.util.Log;
import androidx.documentfile.provider.DocumentFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Objects;
public class FileUtil {
private static final String PATH_SEPARATOR_ENCODED = "%2F";
@@ -88,4 +86,29 @@ public class FileUtil {
return false;
}
}
public static void delete(Path fileToDelete) {
try {
if (!Files.exists(fileToDelete)) {
return;
}
if (Files.isRegularFile(fileToDelete)) {
Files.delete(fileToDelete);
return;
}
var files = Files.list(fileToDelete);
files.forEach(path -> {
if (Files.isDirectory(path)) {
delete(path);
}
try {
Files.delete(path);
} catch (IOException ignored) {
}
});
files.close();
Files.delete(fileToDelete);
} catch (IOException ignored) {
}
}
}
@@ -7,6 +7,7 @@ import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
public class GenericRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
@@ -17,6 +18,13 @@ public class GenericRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerVie
notifyItemInserted(recyclerViewItems.size() - 1);
}
public void removeRecyclerViewItem(RecyclerViewItem recyclerViewItem) {
int position = recyclerViewItems.indexOf(recyclerViewItem);
if (position == -1) return;
recyclerViewItems.remove(position);
notifyItemRemoved(position);
}
public void clearRecyclerViewItems() {
int itemsCount = recyclerViewItems.size();
recyclerViewItems.clear();
@@ -0,0 +1,58 @@
package info.cemu.Cemu;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
public class GraphicPackItemRecyclerViewItem implements RecyclerViewItem {
private static class HeaderViewHolder extends RecyclerView.ViewHolder {
TextView title;
ImageView icon;
public HeaderViewHolder(View itemView) {
super(itemView);
title = itemView.findViewById(R.id.graphic_pack_text);
icon = itemView.findViewById(R.id.graphic_pack_icon);
}
}
public interface OnClickCallback {
void onClick();
}
public enum GraphicPackItemType {
SECTION,
GRAPHIC_PACK,
}
private final String text;
private final GraphicPackItemType graphicPackItemType;
private final OnClickCallback onClickCallback;
public GraphicPackItemRecyclerViewItem(String text, GraphicPackItemType graphicPackItemType, OnClickCallback onClickCallback) {
this.text = text;
this.graphicPackItemType = graphicPackItemType;
this.onClickCallback = onClickCallback;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) {
return new HeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_graphic_pack_item, parent, false));
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, RecyclerView.Adapter<RecyclerView.ViewHolder> adapter) {
HeaderViewHolder headerViewHolder = (HeaderViewHolder) viewHolder;
headerViewHolder.itemView.setOnClickListener(v -> onClickCallback.onClick());
headerViewHolder.title.setText(text);
if (graphicPackItemType == GraphicPackItemType.SECTION)
headerViewHolder.icon.setImageResource(R.drawable.ic_lists);
else if (graphicPackItemType == GraphicPackItemType.GRAPHIC_PACK)
headerViewHolder.icon.setImageResource(R.drawable.ic_package_2);
}
}
@@ -0,0 +1,52 @@
package info.cemu.Cemu;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.checkbox.MaterialCheckBox;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
public class GraphicPackRecyclerViewItem implements RecyclerViewItem {
private static class GraphicPackViewHolder extends RecyclerView.ViewHolder {
TextView name;
MaterialCheckBox enableCheckBox;
TextView description;
public GraphicPackViewHolder(View itemView) {
super(itemView);
name = itemView.findViewById(R.id.graphic_pack_name);
enableCheckBox = itemView.findViewById(R.id.graphic_pack_enable_checkbox);
description = itemView.findViewById(R.id.graphic_pack_description);
}
}
private final NativeLibrary.GraphicPack graphicPack;
public GraphicPackRecyclerViewItem(NativeLibrary.GraphicPack graphicPack) {
this.graphicPack = graphicPack;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) {
return new GraphicPackViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_graphic_pack, parent, false));
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, RecyclerView.Adapter<RecyclerView.ViewHolder> recyclerViewAdapter) {
var graphicPackViewHolder = (GraphicPackViewHolder) viewHolder;
graphicPackViewHolder.name.setText(graphicPack.name);
if (graphicPack.description != null) {
graphicPackViewHolder.description.setText(graphicPack.description);
} else {
graphicPackViewHolder.description.setText(R.string.graphic_pack_no_description);
}
graphicPackViewHolder.enableCheckBox.setChecked(graphicPack.isActive());
graphicPackViewHolder.enableCheckBox.addOnCheckedStateChangedListener((materialCheckBox, i) -> graphicPack.setActive(materialCheckBox.isChecked()));
}
}
@@ -0,0 +1,109 @@
package info.cemu.Cemu;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavController;
import androidx.navigation.fragment.NavHostFragment;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import info.cemu.Cemu.databinding.FragmentGraphicPacksBinding;
public class GraphicPacksFragment extends Fragment {
private final GenericRecyclerViewAdapter genericRecyclerViewAdapter = new GenericRecyclerViewAdapter();
private GraphicPacksRootFragment.GraphicPackViewModel graphicPackPreviousViewModel;
private GraphicPacksRootFragment.GraphicPackViewModel graphicPackCurrentViewModel;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
NavController navController = NavHostFragment.findNavController(this);
graphicPackPreviousViewModel = new ViewModelProvider(Objects.requireNonNull(navController.getPreviousBackStackEntry()))
.get(GraphicPacksRootFragment.GraphicPackViewModel.class);
graphicPackCurrentViewModel = new ViewModelProvider(Objects.requireNonNull(navController.getCurrentBackStackEntry()))
.get(GraphicPacksRootFragment.GraphicPackViewModel.class);
fillData();
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
FragmentGraphicPacksBinding binding = FragmentGraphicPacksBinding.inflate(inflater, container, false);
binding.graphicPacksRecyclerView.setAdapter(genericRecyclerViewAdapter);
return binding.getRoot();
}
private void fillData() {
var graphicNode = graphicPackPreviousViewModel.getGraphicPackNode();
if (graphicNode instanceof GraphicPacksRootFragment.GraphicPackSectionNode sectionNode) {
fillData(sectionNode);
} else if (graphicNode instanceof GraphicPacksRootFragment.GraphicPackDataNode dataNode) {
fillData(dataNode);
}
}
private void fillData(GraphicPacksRootFragment.GraphicPackSectionNode graphicPackSectionNode) {
graphicPackSectionNode.children.forEach(node -> {
var graphicPackItemType = node instanceof GraphicPacksRootFragment.GraphicPackSectionNode
? GraphicPackItemRecyclerViewItem.GraphicPackItemType.SECTION
: GraphicPackItemRecyclerViewItem.GraphicPackItemType.GRAPHIC_PACK;
GraphicPackItemRecyclerViewItem graphicPackItemRecyclerViewItem = new GraphicPackItemRecyclerViewItem(node.name,
graphicPackItemType,
() -> {
graphicPackCurrentViewModel.setGraphicPackNode(node);
Bundle bundle = new Bundle();
bundle.putString("title", node.name);
NavHostFragment.findNavController(GraphicPacksFragment.this)
.navigate(R.id.action_graphicPacksFragment_self, bundle);
});
genericRecyclerViewAdapter.addRecyclerViewItem(graphicPackItemRecyclerViewItem);
});
}
private void fillData(GraphicPacksRootFragment.GraphicPackDataNode graphicPackDataNode) {
genericRecyclerViewAdapter.clearRecyclerViewItems();
var graphicPack = NativeLibrary.getGraphicPack(graphicPackDataNode.getId());
genericRecyclerViewAdapter.addRecyclerViewItem(new GraphicPackRecyclerViewItem(graphicPack));
fillPresets(graphicPack);
}
private void fillPresets(NativeLibrary.GraphicPack graphicPack) {
var recyclerViewItems = new ArrayList<SingleSelectionRecyclerViewItem<String>>();
graphicPack.presets.forEach(graphicPackPreset -> {
String category = graphicPackPreset.getCategory();
if (category == null) category = getString(R.string.active_preset_category);
var recyclerViewItem = new SingleSelectionRecyclerViewItem<>(
category,
graphicPackPreset.getActivePreset(),
new StringSelectionAdapter(
graphicPackPreset.getPresets(),
graphicPackPreset.getActivePreset()
),
(selectedValue, selectionRecyclerViewItem) -> {
graphicPackPreset.setActivePreset(selectedValue);
selectionRecyclerViewItem.setDescription(selectedValue);
var oldPresets = graphicPack.presets;
graphicPack.reloadPresets();
if (!new HashSet<>(oldPresets).containsAll(graphicPack.presets)) {
recyclerViewItems.forEach(genericRecyclerViewAdapter::removeRecyclerViewItem);
fillPresets(graphicPack);
}
}
);
recyclerViewItems.add(recyclerViewItem);
genericRecyclerViewAdapter.addRecyclerViewItem(recyclerViewItem);
});
}
}
@@ -0,0 +1,135 @@
package info.cemu.Cemu;
import static java.util.regex.Pattern.CASE_INSENSITIVE;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class GraphicPacksRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static class GraphicPackSearchItemRecyclerViewItem implements RecyclerViewItem {
private static class HeaderViewHolder extends RecyclerView.ViewHolder {
TextView name;
TextView path;
public HeaderViewHolder(View itemView) {
super(itemView);
name = itemView.findViewById(R.id.graphic_pack_search_name);
path = itemView.findViewById(R.id.graphic_pack_search_path);
}
}
public interface OnClickCallback {
void onClick();
}
private final String name;
private final String path;
private final OnClickCallback onClickCallback;
private GraphicPackSearchItemRecyclerViewItem(String name, String path, OnClickCallback onClickCallback) {
this.name = name;
this.path = path;
this.onClickCallback = onClickCallback;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) {
return new HeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_graphic_pack_search_item, parent, false));
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, RecyclerView.Adapter<RecyclerView.ViewHolder> adapter) {
HeaderViewHolder headerViewHolder = (HeaderViewHolder) viewHolder;
headerViewHolder.itemView.setOnClickListener(v -> onClickCallback.onClick());
headerViewHolder.name.setText(name);
headerViewHolder.path.setText(path);
}
}
public interface OnItemClickCallback {
void onClick(GraphicPacksRootFragment.GraphicPackDataNode graphicPackDataNode);
}
private final OnItemClickCallback onItemClickCallback;
public GraphicPacksRecyclerViewAdapter(OnItemClickCallback onItemClickCallback) {
this.onItemClickCallback = onItemClickCallback;
}
private List<GraphicPackSearchItemRecyclerViewItem> recyclerViewItems = new ArrayList<>();
private List<GraphicPackSearchItemRecyclerViewItem> filteredViewItems = new ArrayList<>();
public void setItems(List<GraphicPacksRootFragment.GraphicPackDataNode> graphicPackDataNodes) {
clearItems();
recyclerViewItems = graphicPackDataNodes.stream()
.sorted(Comparator.comparing(GraphicPacksRootFragment.GraphicPackDataNode::getPath))
.map(graphicPackDataNode ->
new GraphicPackSearchItemRecyclerViewItem(
graphicPackDataNode.getName(),
graphicPackDataNode.getPath(),
() -> GraphicPacksRecyclerViewAdapter.this.onItemClickCallback.onClick(graphicPackDataNode)
)).collect(Collectors.toList());
filteredViewItems = recyclerViewItems;
notifyItemRangeInserted(0, filteredViewItems.size());
}
public void filter(String text) {
if (text.isBlank()) {
filteredViewItems = recyclerViewItems;
notifyDataSetChanged();
return;
}
var stringBuilder = new StringBuilder();
String pattern = Arrays.stream(text.split(" "))
.map(s -> "(?=.*" + Pattern.quote(s) + ")")
.collect(() -> stringBuilder, StringBuilder::append, StringBuilder::append)
.append(".*")
.toString();
var regex = Pattern.compile(pattern, CASE_INSENSITIVE);
filteredViewItems = recyclerViewItems.stream()
.filter(g -> regex.matcher(g.path).matches())
.collect(Collectors.toList());
notifyDataSetChanged();
}
public void clearItems() {
if (filteredViewItems.isEmpty()) return;
int itemsCount = filteredViewItems.size();
filteredViewItems.clear();
notifyItemRangeRemoved(0, itemsCount);
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int position) {
return filteredViewItems.get(position).onCreateViewHolder(parent);
}
@Override
public int getItemViewType(int position) {
return position;
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
filteredViewItems.get(position).onBindViewHolder(holder, this);
}
@Override
public int getItemCount() {
return filteredViewItems.size();
}
}
@@ -0,0 +1,354 @@
package info.cemu.Cemu;
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.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.SearchView;
import androidx.core.view.MenuProvider;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.snackbar.Snackbar;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import info.cemu.Cemu.databinding.FragmentGraphicPacksRootBinding;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
public class GraphicPacksRootFragment extends Fragment {
public abstract static class GraphicPackNode {
protected String name;
public String getName() {
return name;
}
}
public static class GraphicPackDataNode extends GraphicPackNode {
private final long id;
private final String path;
public GraphicPackDataNode(long id, String name, String path) {
this.id = id;
this.name = name;
this.path = path;
}
public long getId() {
return id;
}
public String getPath() {
return path;
}
}
public static class GraphicPackSectionNode extends GraphicPackNode {
ArrayList<GraphicPackNode> children = new ArrayList<>();
public GraphicPackSectionNode(String name) {
this.name = name;
}
public GraphicPackSectionNode() {
}
void clear() {
children.clear();
}
private void addGraphicPackDataByTokens(List<String> tokens, GraphicPackDataNode dataNode) {
var node = this;
for (var token : tokens) {
node = getOrAddSectionByToken(token, node.children);
}
node.children.add(dataNode);
}
private GraphicPackSectionNode getOrAddSectionByToken(String token, ArrayList<GraphicPackNode> graphicPackNodes) {
var sectionNodeOptional = graphicPackNodes.stream()
.filter(g -> (g instanceof GraphicPackSectionNode) && g.name.equals(token))
.findFirst();
if (!sectionNodeOptional.isPresent()) {
var sectionNode = new GraphicPackSectionNode(token);
graphicPackNodes.add(sectionNode);
return sectionNode;
}
return (GraphicPackSectionNode) sectionNodeOptional.get();
}
private void sort() {
children.forEach(node -> {
if (node instanceof GraphicPackSectionNode sectionNode) sectionNode.sort();
});
children.sort(Comparator.comparing(o -> o.name));
}
}
public static class GraphicPackViewModel extends ViewModel {
private GraphicPackNode graphicPackNode;
public GraphicPackViewModel() {
}
public GraphicPackNode getGraphicPackNode() {
return graphicPackNode;
}
public void setGraphicPackNode(GraphicPackNode graphicPackNode) {
this.graphicPackNode = graphicPackNode;
}
}
private final GraphicPackSectionNode graphicPackSectionRootNode = new GraphicPackSectionNode();
private final GenericRecyclerViewAdapter genericRecyclerViewAdapter = new GenericRecyclerViewAdapter();
private GraphicPackViewModel graphicPackViewModel;
private final GraphicPacksRecyclerViewAdapter graphicPacksRecyclerViewAdapter = new GraphicPacksRecyclerViewAdapter(graphicPackDataNode -> {
graphicPackViewModel.setGraphicPackNode(graphicPackDataNode);
Bundle bundle = new Bundle();
bundle.putString("title", graphicPackDataNode.name);
NavHostFragment.findNavController(GraphicPacksRootFragment.this)
.navigate(R.id.action_graphicPacksRootFragment_to_graphicPacksFragment, bundle);
});
private final OkHttpClient client = new OkHttpClient();
private static final String RELEASES_API_URL = "https://api.github.com/repos/cemu-project/cemu_graphic_packs/releases/latest";
private boolean updateInProgress;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
fillGraphicPacks();
var navController = NavHostFragment.findNavController(GraphicPacksRootFragment.this);
graphicPackViewModel = new ViewModelProvider(Objects.requireNonNull(navController.getCurrentBackStackEntry()))
.get(GraphicPackViewModel.class);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
FragmentGraphicPacksRootBinding binding = FragmentGraphicPacksRootBinding.inflate(inflater, container, false);
binding.graphicPacksRootRecyclerView.setAdapter(this.genericRecyclerViewAdapter);
requireActivity().addMenuProvider(new MenuProvider() {
@Override
public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) {
menuInflater.inflate(R.menu.menu_graphic_packs, menu);
var searchMenuItem = menu.findItem(R.id.action_graphic_packs_search);
SearchView searchView = (SearchView) Objects.requireNonNull(searchMenuItem.getActionView());
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
graphicPacksRecyclerViewAdapter.filter(query);
return true;
}
@Override
public boolean onQueryTextChange(String newText) {
graphicPacksRecyclerViewAdapter.filter(newText);
return true;
}
});
searchMenuItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
@Override
public boolean onMenuItemActionExpand(@NonNull MenuItem item) {
binding.graphicPacksRootRecyclerView.setAdapter(graphicPacksRecyclerViewAdapter);
return true;
}
@Override
public boolean onMenuItemActionCollapse(@NonNull MenuItem item) {
binding.graphicPacksRootRecyclerView.setAdapter(genericRecyclerViewAdapter);
return true;
}
});
}
@Override
public boolean onMenuItemSelected(@NonNull MenuItem menuItem) {
if (menuItem.getItemId() == R.id.action_graphic_packs_download && !updateInProgress) {
checkForUpdates();
return true;
}
if (menuItem.getItemId() == R.id.action_graphic_packs_search) {
return true;
}
return false;
}
}, getViewLifecycleOwner(), Lifecycle.State.RESUMED);
return binding.getRoot();
}
private void fillGraphicPacks() {
var nativeGraphicPacks = NativeLibrary.getGraphicPackIdsAndVirtualPaths();
graphicPackSectionRootNode.clear();
List<GraphicPackDataNode> graphicPackDataNodes = new ArrayList<>();
nativeGraphicPacks.forEach(graphicPack -> {
var tokens = Arrays.asList(graphicPack.virtualPath().split("/"));
var dataNode = new GraphicPackDataNode(graphicPack.id(), tokens.get(tokens.size() - 1), graphicPack.virtualPath());
graphicPackDataNodes.add(dataNode);
graphicPackSectionRootNode.addGraphicPackDataByTokens(tokens.subList(0, tokens.size() - 1), dataNode);
});
graphicPacksRecyclerViewAdapter.setItems(graphicPackDataNodes);
genericRecyclerViewAdapter.clearRecyclerViewItems();
graphicPackSectionRootNode.sort();
graphicPackSectionRootNode.children.forEach(node -> {
var graphicPackItemType = node instanceof GraphicPackSectionNode
? GraphicPackItemRecyclerViewItem.GraphicPackItemType.SECTION
: GraphicPackItemRecyclerViewItem.GraphicPackItemType.GRAPHIC_PACK;
GraphicPackItemRecyclerViewItem graphicPackItemRecyclerViewItem = new GraphicPackItemRecyclerViewItem(node.name,
graphicPackItemType,
() -> {
graphicPackViewModel.setGraphicPackNode(node);
Bundle bundle = new Bundle();
bundle.putString("title", node.name);
NavHostFragment.findNavController(GraphicPacksRootFragment.this)
.navigate(R.id.action_graphicPacksRootFragment_to_graphicPacksFragment, bundle);
});
genericRecyclerViewAdapter.addRecyclerViewItem(graphicPackItemRecyclerViewItem);
});
}
private void checkForUpdates() {
AlertDialog dialog = createGraphicPacksDownloadDialog();
requireActivity().runOnUiThread(() -> Objects.requireNonNull((TextView) dialog.findViewById(R.id.textViewGraphicPacksDownload)).setText(R.string.checking_version_download_text));
Request request = new Request.Builder()
.url(RELEASES_API_URL)
.build();
requireActivity().runOnUiThread(() -> Objects.requireNonNull((TextView) dialog.findViewById(R.id.textViewGraphicPacksDownload)).setText(R.string.graphic_packs_download_text));
client.newCall(request).enqueue(new Callback() {
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
try (ResponseBody responseBody = response.body()) {
var json = new JSONObject(Objects.requireNonNull(responseBody).string());
var version = json.getString("name");
var currentVersion = getCurrentVersion();
if (currentVersion.isPresent() && currentVersion.get().equals(version)) {
onGraphicPacksLatestVersion(dialog);
return;
}
var downloadUrl = json.getJSONArray("assets")
.getJSONObject(0)
.getString("browser_download_url");
downloadNewUpdate(downloadUrl, version, dialog);
} catch (JSONException e) {
requireActivity().runOnUiThread(dialog::dismiss);
}
}
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
if (!call.isCanceled()) onDownloadError(dialog);
}
});
}
private Optional<String> getCurrentVersion() {
Path graphicPacksVersionFile = Paths.get(Objects.requireNonNull(requireActivity().getExternalFilesDir(null)).getAbsolutePath(),
"graphicPacks",
"downloadedGraphicPacks",
"version.txt");
try {
return Optional.of(new String(Files.readAllBytes(graphicPacksVersionFile)));
} catch (IOException ignored) {
return Optional.empty();
}
}
private void downloadNewUpdate(String graphicPacksZipDownloadUrl, String version, @NonNull AlertDialog dialog) {
Request request = new Request.Builder()
.url(graphicPacksZipDownloadUrl)
.build();
Path graphicPacksDirPath = Paths.get(Objects.requireNonNull(requireActivity().getExternalFilesDir(null)).getAbsolutePath(), "graphicPacks");
client.newCall(request).enqueue(new Callback() {
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
try (ResponseBody responseBody = response.body()) {
Path graphicPacksTempDirPath = graphicPacksDirPath.resolve("downloadedGraphicPacksTemp");
FileUtil.delete(graphicPacksTempDirPath);
ZipUtil.unzip(Objects.requireNonNull(responseBody).byteStream(), graphicPacksTempDirPath.toString());
Files.write(graphicPacksTempDirPath.resolve("version.txt"), version.getBytes(StandardCharsets.UTF_8));
Path downloadedGraphicPacksDirPath = graphicPacksDirPath.resolve("downloadedGraphicPacks");
FileUtil.delete(downloadedGraphicPacksDirPath);
Files.move(graphicPacksTempDirPath, downloadedGraphicPacksDirPath);
NativeLibrary.refreshGraphicPacks();
requireActivity().runOnUiThread(() -> fillGraphicPacks());
onDownloadFinish(dialog);
}
}
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
if (!call.isCanceled()) onDownloadError(dialog);
}
});
}
private void onDownloadError(AlertDialog dialog) {
requireActivity().runOnUiThread(() -> {
dialog.dismiss();
Snackbar.make(requireView(), R.string.download_graphic_packs_error_text, Snackbar.LENGTH_SHORT).show();
});
}
private void onDownloadFinish(AlertDialog dialog) {
requireActivity().runOnUiThread(() -> {
dialog.dismiss();
Snackbar.make(requireView(), R.string.download_graphic_packs_finish_text, Snackbar.LENGTH_SHORT).show();
});
}
private void onGraphicPacksLatestVersion(AlertDialog dialog) {
requireActivity().runOnUiThread(() -> {
dialog.dismiss();
Snackbar.make(requireView(), R.string.graphic_packs_no_updates_text, Snackbar.LENGTH_SHORT).show();
});
}
private AlertDialog createGraphicPacksDownloadDialog() {
updateInProgress = true;
return new MaterialAlertDialogBuilder(requireContext()).setView(R.layout.layout_graphic_packs_download)
.setTitle(R.string.graphic_packs_download_dialog_title)
.setCancelable(false)
.setOnDismissListener(d -> updateInProgress = false)
.setNegativeButton(R.string.cancel, (dialogInterface, i) -> client.dispatcher().cancelAll())
.show();
}
@Override
public void onDestroy() {
client.dispatcher().cancelAll();
super.onDestroy();
}
}
@@ -1,11 +1,16 @@
package info.cemu.Cemu;
import android.graphics.Bitmap;
import android.view.KeyEvent;
import android.view.Surface;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Predicate;
public class NativeLibrary {
static {
@@ -284,4 +289,94 @@ public class NativeLibrary {
public static native int getAudioDeviceVolume(boolean tv);
public record GraphicPackIdAndVirtualPath(long id, String virtualPath) {
}
public static native ArrayList<GraphicPackIdAndVirtualPath> getGraphicPackIdsAndVirtualPaths();
public static class GraphicPackPreset {
private final long graphicPackId;
private final String category;
private final ArrayList<String> presets;
private String activePreset;
@Override
public int hashCode() {
return Objects.hash(graphicPackId, category, presets, activePreset);
}
@Override
public boolean equals(Object object) {
if (object == null) return false;
if (object == this) return true;
if (object instanceof GraphicPackPreset preset)
return this.hashCode() == preset.hashCode();
return false;
}
public GraphicPackPreset(long graphicPackId, String category, ArrayList<String> presets, String activePreset) {
this.graphicPackId = graphicPackId;
this.category = category;
this.presets = presets;
this.activePreset = activePreset;
}
public String getActivePreset() {
return activePreset;
}
public void setActivePreset(String activePreset) {
if (presets.stream().noneMatch(s -> s.equals(activePreset)))
throw new RuntimeException("Trying to set an invalid preset: " + activePreset);
setGraphicPackActivePreset(graphicPackId, category, activePreset);
this.activePreset = activePreset;
}
public String getCategory() {
return category;
}
public ArrayList<String> getPresets() {
return presets;
}
}
public static class GraphicPack {
public final long id;
protected boolean active;
public final String name;
public final String description;
public List<GraphicPackPreset> presets;
public GraphicPack(long id, boolean active, String name, String description, ArrayList<GraphicPackPreset> presets) {
this.id = id;
this.active = active;
this.name = name;
this.description = description;
this.presets = presets;
}
public boolean isActive() {
return active;
}
public void reloadPresets() {
presets = NativeLibrary.getGraphicPackPresets(id);
}
public void setActive(boolean active) {
this.active = active;
setGraphicPackActive(id, active);
}
}
public static native void refreshGraphicPacks();
public static native GraphicPack getGraphicPack(long id);
public static native void setGraphicPackActive(long id, boolean active);
public static native void setGraphicPackActivePreset(long id, String category, String preset);
public static native ArrayList<GraphicPackPreset> getGraphicPackPresets(long id);
}
@@ -51,6 +51,7 @@ public class OptionsFragment extends Fragment {
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)));
binding.optionsRecyclerView.setAdapter(genericRecyclerViewAdapter);
return binding.getRoot();
@@ -12,7 +12,7 @@ import java.util.List;
import java.util.OptionalInt;
import java.util.stream.IntStream;
public class SelectionAdapter<T> extends BaseAdapter {
public class SelectionAdapter<T> extends BaseSelectionAdapter<T> {
public static class ChoiceItem<T> {
int choiceTextResourceId;
T choiceValue;
@@ -24,7 +24,6 @@ public class SelectionAdapter<T> extends BaseAdapter {
}
protected List<ChoiceItem<T>> choiceItems = new ArrayList<>();
protected int selectedPosition = 0;
public SelectionAdapter() {
super();
@@ -36,10 +35,12 @@ public class SelectionAdapter<T> extends BaseAdapter {
setSelectedValue(selectedValue);
}
@Override
public void setSelectedValue(T selectedValue) {
selectedPosition = getPositionOf(selectedValue);
}
@Override
public int getPositionOf(T value) {
OptionalInt optionalInt = IntStream.range(0, choiceItems.size()).filter(position -> choiceItems.get(position).choiceValue.equals(value)).findFirst();
if (!optionalInt.isPresent())
@@ -47,6 +48,11 @@ public class SelectionAdapter<T> extends BaseAdapter {
return optionalInt.getAsInt();
}
@Override
protected void setRadioButtonText(MaterialRadioButton radioButton, int position) {
radioButton.setText(choiceItems.get(position).choiceTextResourceId);
}
@Override
public int getCount() {
return choiceItems.size();
@@ -56,20 +62,4 @@ public class SelectionAdapter<T> extends BaseAdapter {
public T getItem(int position) {
return choiceItems.get(position).choiceValue;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View view, ViewGroup viewGroup) {
if (view == null)
view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.layout_single_selection_item, viewGroup, false);
MaterialRadioButton radioButton = view.findViewById(R.id.single_selection_item_radio_button);
radioButton.setEnabled(isEnabled(position));
radioButton.setText(choiceItems.get(position).choiceTextResourceId);
radioButton.setChecked(position == selectedPosition);
return view;
}
}
@@ -4,31 +4,32 @@ import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.navigation.NavController;
import androidx.navigation.NavGraph;
import androidx.navigation.Navigation;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI;
import java.util.Objects;
import info.cemu.Cemu.databinding.ActivitySettingsBinding;
public class SettingsActivity extends AppCompatActivity {
private AppBarConfiguration appBarConfiguration;
private ActivitySettingsBinding binding;
private NavController navController;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivitySettingsBinding.inflate(getLayoutInflater());
info.cemu.Cemu.databinding.ActivitySettingsBinding binding = ActivitySettingsBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbarSettings);
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_settings);
appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).build();
navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_settings);
appBarConfiguration = new AppBarConfiguration.Builder().build();
NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
}
@Override
public boolean onSupportNavigateUp() {
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_settings);
return NavigationUI.navigateUp(navController, appBarConfiguration)
|| super.onSupportNavigateUp();
}
@@ -25,14 +25,14 @@ public class SingleSelectionRecyclerViewItem<T> implements RecyclerViewItem {
void onItemSelected(T selectedValue, SingleSelectionRecyclerViewItem<T> selectionRecyclerViewItem);
}
private final SelectionAdapter<T> selectionAdapter;
private final BaseSelectionAdapter<T> selectionAdapter;
private final String label;
private String description;
private final OnItemSelectedListener<T> onItemSelectedListener;
private SingleSelectionViewHolder singleSelectionViewHolder;
private RecyclerView.Adapter<RecyclerView.ViewHolder> recyclerViewAdapter;
public SingleSelectionRecyclerViewItem(String label, String description, SelectionAdapter<T> selectionAdapter, OnItemSelectedListener<T> onItemSelectedListener) {
public SingleSelectionRecyclerViewItem(String label, String description, BaseSelectionAdapter<T> selectionAdapter, OnItemSelectedListener<T> onItemSelectedListener) {
this.label = label;
this.description = description;
this.selectionAdapter = selectionAdapter;
@@ -0,0 +1,46 @@
package info.cemu.Cemu;
import com.google.android.material.radiobutton.MaterialRadioButton;
import java.util.ArrayList;
import java.util.List;
import java.util.OptionalInt;
import java.util.stream.IntStream;
public class StringSelectionAdapter extends BaseSelectionAdapter<String> {
private final List<String> choices;
public StringSelectionAdapter(List<String> choices, String selectedValue) {
super();
this.choices = choices;
setSelectedValue(selectedValue);
}
@Override
public void setSelectedValue(String selectedValue) {
selectedPosition = getPositionOf(selectedValue);
}
@Override
public int getPositionOf(String value) {
OptionalInt optionalInt = IntStream.range(0, choices.size()).filter(position -> choices.get(position).equals(value)).findFirst();
if (!optionalInt.isPresent())
throw new RuntimeException("value " + value + " was not found in the list of choices");
return optionalInt.getAsInt();
}
@Override
protected void setRadioButtonText(MaterialRadioButton radioButton, int position) {
radioButton.setText(choices.get(position));
}
@Override
public int getCount() {
return choices.size();
}
@Override
public String getItem(int position) {
return choices.get(position);
}
}
@@ -0,0 +1,40 @@
package info.cemu.Cemu;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Paths;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
public class ZipUtil {
public static void unzip(InputStream stream, String targetDir) throws IOException {
try (ZipInputStream zipInputStream = new ZipInputStream(stream)) {
ZipEntry zipEntry;
byte[] buffer = new byte[8192];
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
String filePath = Paths.get(targetDir, zipEntry.getName()).toString();
if (zipEntry.isDirectory()) {
createDir(filePath);
} else {
try (FileOutputStream fileOutputStream = new FileOutputStream(filePath)) {
int bytesRead;
while ((bytesRead = zipInputStream.read(buffer)) > 0) {
fileOutputStream.write(buffer, 0, bytesRead);
}
}
}
zipInputStream.closeEntry();
}
}
}
private static void createDir(String dir) {
File f = new File(dir);
if (!f.isDirectory()) {
f.mkdirs();
}
}
}
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M504,480L320,296L376,240L616,480L376,720L320,664L504,480Z" />
</vector>

Some files were not shown because too many files have changed in this diff Show More