Pandroid UI

This commit is contained in:
GabrielBRDeveloper 2023-12-16 23:22:11 -04:00
parent ab37cb571d
commit 3298bd14db
56 changed files with 1741 additions and 77 deletions

View file

@ -38,5 +38,7 @@ android {
dependencies {
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.8.0")
implementation("androidx.preference:preference:1.2.1")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("com.google.code.gson:gson:2.10.1")
}

View file

@ -24,7 +24,8 @@
tools:targetApi="31">
<activity
android:name=".app.MainActivity"
android:exported="true">
android:exported="true"
android:configChanges="orientation">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@ -34,5 +35,11 @@
android:name=".app.GameActivity"
android:configChanges="screenSize|screenLayout|orientation|density|uiMode">
</activity>
<activity android:name=".app.PreferenceActivity"
android:launchMode="standard"
android:configChanges="screenSize|screenLayout|orientation|density"/>
<activity android:name=".app.preferences.InputMapActivity"
android:configChanges="density|orientation|screenSize"/>
</application>
</manifest>

View file

@ -2,6 +2,8 @@ package com.panda3ds.pandroid.app;
import android.content.Intent;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
@ -12,11 +14,17 @@ import android.widget.Toast;
import androidx.annotation.Nullable;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.app.game.AlberInputListener;
import com.panda3ds.pandroid.input.InputHandler;
import com.panda3ds.pandroid.input.InputMap;
import com.panda3ds.pandroid.utils.Constants;
import com.panda3ds.pandroid.view.PandaGlSurfaceView;
import com.panda3ds.pandroid.view.PandaLayoutController;
public class GameActivity extends BaseActivity {
private final AlberInputListener inputListener = new AlberInputListener();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -33,7 +41,7 @@ public class GameActivity extends BaseActivity {
setContentView(R.layout.game_activity);
((FrameLayout) findViewById(R.id.panda_gl_frame))
.addView(pandaSurface, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
.addView(pandaSurface, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
PandaLayoutController controllerLayout = findViewById(R.id.controller_layout);
controllerLayout.initialize();
@ -46,5 +54,30 @@ public class GameActivity extends BaseActivity {
super.onResume();
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
InputHandler.reset();
InputHandler.setMotionDeadZone(InputMap.getDeadZone());
InputHandler.setEventListener(inputListener);
}
@Override
protected void onPause() {
super.onPause();
InputHandler.reset();
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (InputHandler.processKeyEvent(event))
return true;
return super.dispatchKeyEvent(event);
}
@Override
public boolean dispatchGenericMotionEvent(MotionEvent ev) {
if (InputHandler.processMotionEvent(ev))
return true;
return super.dispatchGenericMotionEvent(ev);
}
}

View file

@ -8,17 +8,27 @@ import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.utils.Constants;
import com.panda3ds.pandroid.utils.PathUtils;
import android.view.MenuItem;
public class MainActivity extends BaseActivity {
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import com.google.android.material.navigation.NavigationBarView;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.app.main.GamesFragment;
import com.panda3ds.pandroid.app.main.SearchFragment;
import com.panda3ds.pandroid.app.main.SettingsFragment;
public class MainActivity extends BaseActivity implements NavigationBarView.OnItemSelectedListener {
private static final int PICK_ROM = 2;
private static final int PERMISSION_REQUEST_CODE = 3;
private final GamesFragment gamesFragment = new GamesFragment();
private final SearchFragment searchFragment = new SearchFragment();
private final SettingsFragment settingsFragment = new SettingsFragment();
private void openFile() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
@ -35,23 +45,35 @@ public class MainActivity extends BaseActivity {
startActivity(intent);
}
} else {
ActivityCompat.requestPermissions(this, new String[] {READ_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE);
ActivityCompat.requestPermissions(this, new String[] {WRITE_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE);
ActivityCompat.requestPermissions(this, new String[]{READ_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE);
ActivityCompat.requestPermissions(this, new String[]{WRITE_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE);
}
setContentView(R.layout.activity_main);
findViewById(R.id.load_rom).setOnClickListener(v -> { openFile(); });
NavigationBarView bar = findViewById(R.id.navigation);
bar.setOnItemSelectedListener(this);
bar.postDelayed(() -> bar.setSelectedItemId(bar.getSelectedItemId()), 5);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == PICK_ROM) {
if (resultCode == RESULT_OK) {
String path = PathUtils.getPath(getApplicationContext(), data.getData());
Toast.makeText(getApplicationContext(), "pandroid opening " + path, Toast.LENGTH_LONG).show();
startActivity(new Intent(this, GameActivity.class).putExtra(Constants.ACTIVITY_PARAMETER_PATH, path));
}
super.onActivityResult(requestCode, resultCode, data);
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
int id = item.getItemId();
FragmentManager manager = getSupportFragmentManager();
Fragment fragment;
if (id == R.id.games) {
fragment = gamesFragment;
} else if (id == R.id.search) {
fragment = searchFragment;
} else if (id == R.id.settings) {
fragment = settingsFragment;
} else {
return false;
}
manager.beginTransaction()
.replace(R.id.fragment_container, fragment)
.commitNow();
return true;
}
}

View file

@ -4,6 +4,8 @@ import android.app.Application;
import android.content.Context;
import com.panda3ds.pandroid.data.config.GlobalConfig;
import com.panda3ds.pandroid.input.InputMap;
import com.panda3ds.pandroid.utils.GameUtils;
public class PandroidApplication extends Application {
private static Context appContext;
@ -13,6 +15,8 @@ public class PandroidApplication extends Application {
super.onCreate();
appContext = this;
GlobalConfig.initialize();
GameUtils.initialize();
InputMap.initialize();
}
public static Context getAppContext() {

View file

@ -0,0 +1,54 @@
package com.panda3ds.pandroid.app;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.MenuItem;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.utils.Constants;
public class PreferenceActivity extends BaseActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
setContentView(R.layout.activity_preference);
setSupportActionBar(findViewById(R.id.toolbar));
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
if (!intent.hasExtra(Constants.ACTIVITY_PARAMETER_FRAGMENT)) {
finish();
return;
}
try {
Class<?> clazz = getClassLoader().loadClass(intent.getStringExtra(Constants.ACTIVITY_PARAMETER_FRAGMENT));
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.fragment_container, (Fragment) clazz.newInstance())
.commitNow();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void launch(Context context, Class<? extends Fragment> clazz) {
context.startActivity(new Intent(context, PreferenceActivity.class)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(Constants.ACTIVITY_PARAMETER_FRAGMENT, clazz.getName()));
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == android.R.id.home)
finish();
return super.onOptionsItemSelected(item);
}
}

View file

@ -0,0 +1,19 @@
package com.panda3ds.pandroid.app.base;
import android.annotation.SuppressLint;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import com.panda3ds.pandroid.lang.Function;
public abstract class BasePreferenceFragment extends PreferenceFragmentCompat {
@SuppressLint("RestrictedApi")
protected void setItemClick(String key, Function<Preference> listener){
findPreference(key).setOnPreferenceClickListener(preference -> {
listener.run(preference);
getPreferenceScreen().performClick();
return false;
});
}
}

View file

@ -0,0 +1,54 @@
package com.panda3ds.pandroid.app.game;
import com.panda3ds.pandroid.AlberDriver;
import com.panda3ds.pandroid.input.InputEvent;
import com.panda3ds.pandroid.input.InputMap;
import com.panda3ds.pandroid.input.KeyName;
import com.panda3ds.pandroid.lang.Function;
import com.panda3ds.pandroid.math.Vector2;
public class AlberInputListener implements Function<InputEvent> {
private final Vector2 axis = new Vector2(0.0f, 0.0f);
@Override
public void run(InputEvent event) {
KeyName key = InputMap.relative(event.getName());
if (key == KeyName.NULL)
return;
boolean axisChanged = false;
switch (key) {
case AXIS_UP:
axis.y = event.getValue();
axisChanged = true;
break;
case AXIS_DOWN:
axis.y = -event.getValue();
axisChanged = true;
break;
case AXIS_LEFT:
axis.x = -event.getValue();
axisChanged = true;
break;
case AXIS_RIGHT:
axis.x = event.getValue();
axisChanged = true;
break;
default:
if (event.isDown()) {
AlberDriver.KeyDown(key.getKeyId());
} else {
AlberDriver.KeyUp(key.getKeyId());
}
break;
}
if (axisChanged) {
AlberDriver.SetCirclepadAxis(Math.round(axis.x * 0x9C), Math.round(axis.y * 0x9C));
}
}
}

View file

@ -0,0 +1,75 @@
package com.panda3ds.pandroid.app.main;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.data.game.GameMetadata;
import com.panda3ds.pandroid.utils.FileUtils;
import com.panda3ds.pandroid.utils.GameUtils;
import com.panda3ds.pandroid.view.gamesgrid.GamesGridView;
public class GamesFragment extends Fragment implements ActivityResultCallback<Uri> {
private final ActivityResultContracts.OpenDocument openRomContract = new ActivityResultContracts.OpenDocument();
private ActivityResultLauncher<String[]> pickFileRequest;
private GamesGridView gameListView;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_games, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
gameListView = view.findViewById(R.id.games);
view.findViewById(R.id.add_rom).setOnClickListener((v) -> pickFileRequest.launch(new String[]{"*/*"}));
}
@Override
public void onResume() {
super.onResume();
gameListView.setGameList(GameUtils.getGames());
}
@Override
public void onActivityResult(Uri result) {
if (result != null) {
String uri = result.toString();
if (GameUtils.findByRomPath(uri) == null) {
FileUtils.makeUriPermanent(uri, FileUtils.MODE_READ);
GameMetadata game = new GameMetadata(FileUtils.getName(uri).split("\\.")[0], uri, "Unknown");
GameUtils.addGame(game);
GameUtils.launch(requireActivity(), game);
}
}
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
pickFileRequest = registerForActivityResult(openRomContract, this);
}
@Override
public void onDestroy() {
if (pickFileRequest != null) {
pickFileRequest.unregister();
pickFileRequest = null;
}
super.onDestroy();
}
}

View file

@ -0,0 +1,67 @@
package com.panda3ds.pandroid.app.main;
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.appcompat.widget.AppCompatEditText;
import androidx.fragment.app.Fragment;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.data.game.GameMetadata;
import com.panda3ds.pandroid.utils.GameUtils;
import com.panda3ds.pandroid.utils.SearchAgent;
import com.panda3ds.pandroid.view.SimpleTextWatcher;
import com.panda3ds.pandroid.view.gamesgrid.GamesGridView;
import java.util.ArrayList;
import java.util.List;
public class SearchFragment extends Fragment {
private final SearchAgent searchAgent = new SearchAgent();
private GamesGridView gamesListView;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_search, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
gamesListView = view.findViewById(R.id.games);
((AppCompatEditText) view.findViewById(R.id.search_bar))
.addTextChangedListener((SimpleTextWatcher) this::search);
}
@Override
public void onResume() {
super.onResume();
searchAgent.clearBuffer();
for (GameMetadata game : GameUtils.getGames()) {
searchAgent.addToBuffer(game.getId(), game.getTitle(), game.getPublisher());
}
search("");
}
private void search(String query) {
List<String> resultIds = searchAgent.search(query);
ArrayList<GameMetadata> games = new ArrayList<>(GameUtils.getGames());
Object[] resultObj = games.stream()
.filter(gameMetadata -> resultIds.contains(gameMetadata.getId()))
.toArray();
games.clear();
for (Object res : resultObj)
games.add((GameMetadata) res);
gamesListView.setGameList(games);
}
}

View file

@ -0,0 +1,18 @@
package com.panda3ds.pandroid.app.main;
import android.os.Bundle;
import androidx.annotation.Nullable;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.app.PreferenceActivity;
import com.panda3ds.pandroid.app.base.BasePreferenceFragment;
import com.panda3ds.pandroid.app.preferences.InputMapPreferences;
public class SettingsFragment extends BasePreferenceFragment {
@Override
public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
setPreferencesFromResource(R.xml.start_preferences, rootKey);
setItemClick("inputMap", (item) -> PreferenceActivity.launch(requireContext(), InputMapPreferences.class));
}
}

View file

@ -0,0 +1,76 @@
package com.panda3ds.pandroid.app.preferences;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.widget.Toast;
import androidx.activity.result.contract.ActivityResultContract;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.app.BaseActivity;
import com.panda3ds.pandroid.input.InputEvent;
import com.panda3ds.pandroid.input.InputHandler;
import java.util.Objects;
public class InputMapActivity extends BaseActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_input_map);
}
@Override
protected void onResume() {
super.onResume();
InputHandler.reset();
InputHandler.setMotionDeadZone(0.8F);
InputHandler.setEventListener(this::onInputEvent);
}
@Override
protected void onPause() {
super.onPause();
InputHandler.reset();
}
@Override
public boolean dispatchGenericMotionEvent(MotionEvent ev) {
return InputHandler.processMotionEvent(ev);
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
return InputHandler.processKeyEvent(event);
}
private void onInputEvent(InputEvent event) {
if (Objects.equals(event.getName(), "KEYCODE_BACK")) {
onBackPressed();
return;
}
setResult(RESULT_OK, new Intent(event.getName()));
Toast.makeText(this, event.getName(), Toast.LENGTH_SHORT).show();
finish();
}
public static final class Contract extends ActivityResultContract<String, String> {
@NonNull
@Override
public Intent createIntent(@NonNull Context context, String s) {
return new Intent(context, InputMapActivity.class);
}
@Override
public String parseResult(int i, @Nullable Intent intent) {
return i == RESULT_OK ? intent.getAction() : null;
}
}
}

View file

@ -0,0 +1,72 @@
package com.panda3ds.pandroid.app.preferences;
import android.content.Context;
import android.os.Bundle;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.app.base.BasePreferenceFragment;
import com.panda3ds.pandroid.input.InputMap;
import com.panda3ds.pandroid.input.KeyName;
public class InputMapPreferences extends BasePreferenceFragment implements ActivityResultCallback<String> {
private ActivityResultLauncher<String> requestKey;
private String currentKey;
@Override
public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
setPreferencesFromResource(R.xml.input_map_preferences, rootKey);
for (KeyName key : KeyName.values()) {
if (key == KeyName.NULL) return;
setItemClick(key.name(), this::onItemPressed);
}
refreshList();
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
requestKey = registerForActivityResult(new InputMapActivity.Contract(), this);
}
@Override
public void onDetach() {
super.onDetach();
if (requestKey != null) {
requestKey.unregister();
requestKey = null;
}
}
private void onItemPressed(Preference pref) {
currentKey = pref.getKey();
requestKey.launch(null);
}
@Override
public void onResume() {
super.onResume();
refreshList();
}
private void refreshList() {
for (KeyName key : KeyName.values()) {
if (key == KeyName.NULL) continue;
findPreference(key.name()).setSummary(InputMap.relative(key));
}
}
@Override
public void onActivityResult(String result) {
if (result != null) {
InputMap.set(KeyName.valueOf(currentKey), result);
refreshList();
}
}
}

View file

@ -0,0 +1,44 @@
package com.panda3ds.pandroid.data.game;
import java.util.UUID;
public class GameMetadata {
private final String id;
private final String romPath;
private final String title;
private final int[] icon = new int[48 * 48];
private final String publisher;
private final GameRegion[] regions = new GameRegion[]{GameRegion.None};
public GameMetadata(String title, String romPath, String publisher) {
this.id = UUID.randomUUID().toString();
this.title = title;
this.publisher = publisher;
this.romPath = romPath;
}
public String getRomPath() {
return romPath;
}
public String getId() {
return id;
}
public String getTitle() {
return title;
}
public String getPublisher() {
return publisher;
}
public int[] getIcon() {
return icon;
}
public GameRegion[] getRegions() {
return regions;
}
}

View file

@ -0,0 +1,12 @@
package com.panda3ds.pandroid.data.game;
public enum GameRegion {
NorthAmerican,
Japan,
Europe,
Australia,
China,
Korean,
Taiwan,
None
}

View file

@ -0,0 +1,23 @@
package com.panda3ds.pandroid.input;
public class InputEvent {
private final String name;
private final float value;
public InputEvent(String name, float value) {
this.name = name;
this.value = Math.max(0.0f, Math.min(1.0f, value));
}
public boolean isDown() {
return value > 0.0f;
}
public String getName() {
return name;
}
public float getValue() {
return value;
}
}

View file

@ -0,0 +1,116 @@
package com.panda3ds.pandroid.input;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import com.panda3ds.pandroid.lang.Function;
import java.util.HashMap;
public class InputHandler {
private static Function<InputEvent> eventListener;
private static float motionDeadZone = 0.0f;
private static final int[] gamepadSources = {
InputDevice.SOURCE_GAMEPAD,
InputDevice.SOURCE_JOYSTICK
};
private static final int[] validSources = {
InputDevice.SOURCE_GAMEPAD,
InputDevice.SOURCE_JOYSTICK,
InputDevice.SOURCE_DPAD,
InputDevice.SOURCE_KEYBOARD
};
private static final HashMap<String, Float> motionDownEvents = new HashMap<>();
private static boolean containsSource(int[] sources, int sourceMasked) {
for (int source : sources) {
if ((sourceMasked & source) == source)
return true;
}
return false;
}
private static boolean isGamepadSource(int sourceMask) {
return containsSource(gamepadSources, sourceMask);
}
private static boolean isSourceValid(int sourceMasked) {
return containsSource(validSources, sourceMasked);
}
public static void setEventListener(Function<InputEvent> eventListener) {
InputHandler.eventListener = eventListener;
}
private static void handleEvent(InputEvent event) {
if (eventListener != null) {
eventListener.run(event);
}
}
public static void setMotionDeadZone(float motionDeadZone) {
InputHandler.motionDeadZone = motionDeadZone;
}
public static boolean processMotionEvent(MotionEvent event) {
if (!isSourceValid(event.getSource()))
return false;
if (isGamepadSource(event.getSource())) {
for (InputDevice.MotionRange range : event.getDevice().getMotionRanges()) {
float axisValue = event.getAxisValue(range.getAxis());
float value = Math.abs(axisValue);
String name = (MotionEvent.axisToString(range.getAxis()) + (axisValue >= 0 ? "+" : "-")).toUpperCase();
String reverseName = (MotionEvent.axisToString(range.getAxis()) + (axisValue >= 0 ? "-" : "+")).toUpperCase();
if (motionDownEvents.containsKey(reverseName)) {
motionDownEvents.remove(reverseName);
handleEvent(new InputEvent(reverseName.toUpperCase(), 0.0f));
}
if (value > motionDeadZone) {
motionDownEvents.put(name, value);
handleEvent(new InputEvent(name.toUpperCase(), (value - motionDeadZone) / (1.0f - motionDeadZone)));
} else if (motionDownEvents.containsKey(name)) {
motionDownEvents.remove(name);
handleEvent(new InputEvent(name.toUpperCase(), 0.0f));
}
}
}
return true;
}
public static boolean processKeyEvent(KeyEvent event) {
if (!isSourceValid(event.getSource()))
return false;
if (isGamepadSource(event.getSource())) {
// Dpad return motion event + key event, this remove the key event
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_DPAD_UP:
case KeyEvent.KEYCODE_DPAD_UP_LEFT:
case KeyEvent.KEYCODE_DPAD_UP_RIGHT:
case KeyEvent.KEYCODE_DPAD_DOWN:
case KeyEvent.KEYCODE_DPAD_DOWN_LEFT:
case KeyEvent.KEYCODE_DPAD_DOWN_RIGHT:
case KeyEvent.KEYCODE_DPAD_RIGHT:
case KeyEvent.KEYCODE_DPAD_LEFT:
return true;
}
}
handleEvent(new InputEvent(KeyEvent.keyCodeToString(event.getKeyCode()), event.getAction() == KeyEvent.ACTION_UP ? 0.0f : 1.0f));
return true;
}
public static void reset() {
eventListener = null;
motionDeadZone = 0.0f;
motionDownEvents.clear();
}
}

View file

@ -0,0 +1,42 @@
package com.panda3ds.pandroid.input;
import android.content.Context;
import android.content.SharedPreferences;
import com.panda3ds.pandroid.app.PandroidApplication;
import com.panda3ds.pandroid.utils.Constants;
public class InputMap {
private static SharedPreferences data;
private static final String KEY_DEAD_ZONE = "deadZone";
public static void initialize() {
data = PandroidApplication.getAppContext().getSharedPreferences(Constants.PREF_INPUT_MAP, Context.MODE_PRIVATE);
}
public static float getDeadZone() {
return data.getFloat(KEY_DEAD_ZONE, 0.2f);
}
public static void set(KeyName key, String name) {
data.edit().putString(key.name(), name).apply();
}
public static String relative(KeyName key) {
return data.getString(key.name(), "-");
}
public static KeyName relative(String name) {
for (KeyName key : KeyName.values()) {
if (relative(key).equalsIgnoreCase(name))
return key;
}
return KeyName.NULL;
}
public static void setDeadZone(float value) {
data.edit().putFloat(KEY_DEAD_ZONE, Math.max(0, Math.min(1.0F, value))).apply();
}
}

View file

@ -0,0 +1,38 @@
package com.panda3ds.pandroid.input;
import com.panda3ds.pandroid.utils.Constants;
public enum KeyName {
A(Constants.INPUT_KEY_A),
B(Constants.INPUT_KEY_B),
X(Constants.INPUT_KEY_X),
Y(Constants.INPUT_KEY_Y),
UP(Constants.INPUT_KEY_UP),
DOWN(Constants.INPUT_KEY_DOWN),
LEFT(Constants.INPUT_KEY_LEFT),
RIGHT(Constants.INPUT_KEY_RIGHT),
AXIS_LEFT,
AXIS_RIGHT,
AXIS_UP,
AXIS_DOWN,
START(Constants.INPUT_KEY_START),
SELECT(Constants.INPUT_KEY_SELECT),
L(Constants.INPUT_KEY_L),
R(Constants.INPUT_KEY_R),
NULL;
private final int keyId;
KeyName() {
this(-1);
}
KeyName(int keyId) {
this.keyId = keyId;
}
public int getKeyId() {
return keyId;
}
}

View file

@ -0,0 +1,5 @@
package com.panda3ds.pandroid.lang;
public interface Function<T> {
void run(T arg);
}

View file

@ -19,7 +19,10 @@ public class Constants {
public static final int N3DS_HALF_HEIGHT = N3DS_FULL_HEIGHT / 2;
public static final String ACTIVITY_PARAMETER_PATH = "path";
public static final String ACTIVITY_PARAMETER_FRAGMENT = "fragment";
public static final String LOG_TAG = "pandroid";
public static final String PREF_GLOBAL_CONFIG = "app.GlobalConfig";
public static final String PREF_GAME_UTILS = "app.GameUtils";
public static final String PREF_INPUT_MAP = "app.InputMap";
}

View file

@ -0,0 +1,63 @@
package com.panda3ds.pandroid.utils;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import androidx.documentfile.provider.DocumentFile;
import com.panda3ds.pandroid.app.PandroidApplication;
public class FileUtils {
public static final String MODE_READ = "r";
private static Uri parseUri(String value) {
return Uri.parse(value);
}
private static Context getContext() {
return PandroidApplication.getAppContext();
}
public static String getName(String path) {
DocumentFile file = DocumentFile.fromSingleUri(getContext(), parseUri(path));
return file.getName();
}
public static long getSize(String path) {
return DocumentFile.fromSingleUri(getContext(), parseUri(path)).length();
}
public static String getCacheDir() {
return getContext().getCacheDir().getAbsolutePath();
}
public static void makeUriPermanent(String uri, String mode) {
int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION;
if (mode.toLowerCase().contains("w"))
flags &= Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
getContext().getContentResolver().takePersistableUriPermission(parseUri(uri), flags);
}
public static int openContentUri(String path, String mode) {
try {
Uri uri = parseUri(path);
ParcelFileDescriptor descriptor = getContext().getContentResolver().openFileDescriptor(uri, mode);
int fd = descriptor.getFd();
descriptor.detachFd();
descriptor.close();
return fd;
} catch (Exception e) {
Log.e(Constants.LOG_TAG, "Error on openContentUri: " + e);
}
return -1;
}
}

View file

@ -0,0 +1,64 @@
package com.panda3ds.pandroid.utils;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import com.google.gson.Gson;
import com.panda3ds.pandroid.app.GameActivity;
import com.panda3ds.pandroid.app.PandroidApplication;
import com.panda3ds.pandroid.data.game.GameMetadata;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Objects;
public class GameUtils {
private static final String KEY_GAME_LIST = "gameList";
private static final ArrayList<GameMetadata> games = new ArrayList<>();
private static SharedPreferences data;
private static final Gson gson = new Gson();
public static void initialize() {
data = PandroidApplication.getAppContext().getSharedPreferences(Constants.PREF_GAME_UTILS, Context.MODE_PRIVATE);
GameMetadata[] list = gson.fromJson(data.getString(KEY_GAME_LIST, "[]"), GameMetadata[].class);
games.clear();
games.addAll(Arrays.asList(list));
}
public static GameMetadata findByRomPath(String romPath) {
for (GameMetadata game : games) {
if (Objects.equals(romPath, game.getRomPath())) {
return game;
}
}
return null;
}
public static void launch(Context context, GameMetadata game) {
String path = PathUtils.getPath(Uri.parse(game.getRomPath()));
context.startActivity(new Intent(context, GameActivity.class).putExtra(Constants.ACTIVITY_PARAMETER_PATH, path));
}
public static void removeGame(GameMetadata game) {
games.remove(game);
saveAll();
}
public static void addGame(GameMetadata game) {
games.add(game);
saveAll();
}
public static ArrayList<GameMetadata> getGames() {
return new ArrayList<>(games);
}
private static synchronized void saveAll() {
data.edit()
.putString(KEY_GAME_LIST, gson.toJson(games.toArray(new GameMetadata[0])))
.apply();
}
}

View file

@ -9,8 +9,11 @@ import android.os.Environment;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import com.panda3ds.pandroid.app.PandroidApplication;
public class PathUtils {
public static String getPath(final Context context, final Uri uri) {
public static String getPath(final Uri uri) {
final Context context = PandroidApplication.getAppContext();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && DocumentsContract.isDocumentUri(context, uri)) {
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);

View file

@ -0,0 +1,91 @@
package com.panda3ds.pandroid.utils;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
public class SearchAgent {
/**
* Store all possibles results in map
* id->words
*/
private final HashMap<String, String> searchBuffer = new HashMap<>();
// Add search item to list
public void addToBuffer(String id, String... words) {
StringBuilder string = new StringBuilder();
for (String word : words) {
string.append(normalize(word)).append(" ");
}
searchBuffer.put(id, string.toString());
}
/**
* Convert string to simple string with only a-z 0-9 for do this first it get the input string
* and apply lower case, after convert all chars to ASCII
* Ex: ç => c, á => a
* after replace all double space for single space
*/
private String normalize(String string) {
string = Normalizer.normalize(string, Normalizer.Form.NFD).replaceAll("[^\\p{ASCII}]", "");
return string.toLowerCase()
.replaceAll("(?!([a-z0-9 ])).*", "")
.replaceAll("\\s\\s", " ");
}
// Execute search and return array with item id.
public List<String> search(String query) {
String[] words = normalize(query).split("\\s");
if (words.length == 0)
return Collections.emptyList();
// Map for add all search result: id -> probability
HashMap<String, Integer> results = new HashMap<>();
for (String key : searchBuffer.keySet()) {
int probability = 0;
String value = searchBuffer.get(key);
for (String word : words) {
if (value.contains(word))
probability++;
}
if (probability > 0)
results.put(key, probability);
}
// Filter by probability average
// Ex: A = 10% B = 30% C = 70% (calc is (10+30+70)/3=36)
// After remove all result with probability < 36
int average = 0;
for (String key : results.keySet()) {
average += results.get(key);
}
average = average / Math.max(1, results.size());
int i = 0;
ArrayList<String> resultKeys = new ArrayList<>(Arrays.asList(results.keySet().toArray(new String[0])));
while ((i < resultKeys.size() && resultKeys.size() > 1)) {
if (results.get(resultKeys.get(i)) < average) {
String key = resultKeys.get(i);
resultKeys.remove(i);
results.remove(key);
i = 0;
continue;
}
i++;
}
return Arrays.asList(results.keySet().toArray(new String[0]));
}
// Clear search buffer
public void clearBuffer() {
searchBuffer.clear();
}
}

View file

@ -0,0 +1,19 @@
package com.panda3ds.pandroid.view;
import android.text.Editable;
import android.text.TextWatcher;
public interface SimpleTextWatcher extends TextWatcher {
void onChange(String value);
@Override
default void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
default void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
default void afterTextChanged(Editable s){
onChange(s.toString());
}
}

View file

@ -0,0 +1,42 @@
package com.panda3ds.pandroid.view.gamesgrid;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.data.game.GameMetadata;
import java.util.ArrayList;
import java.util.List;
class GameAdapter extends RecyclerView.Adapter<ItemHolder> {
private final ArrayList<GameMetadata> games = new ArrayList<>();
@NonNull
@Override
public ItemHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ItemHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.holder_game, parent, false));
}
@Override
public void onBindViewHolder(@NonNull ItemHolder holder, int position) {
holder.apply(games.get(position));
}
public void replace(List<GameMetadata> games) {
int oldCount = getItemCount();
this.games.clear();
notifyItemRangeRemoved(0, oldCount);
this.games.addAll(games);
notifyItemRangeInserted(0, getItemCount());
}
@Override
public int getItemCount() {
return games.size();
}
}

View file

@ -0,0 +1,42 @@
package com.panda3ds.pandroid.view.gamesgrid;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView;
public class GameIconView extends AppCompatImageView {
public GameIconView(@NonNull Context context) {
super(context);
}
public GameIconView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public GameIconView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int size = getMeasuredWidth();
setMeasuredDimension(size, size);
}
@Override
public void setImageBitmap(Bitmap bm) {
super.setImageBitmap(bm);
Drawable bitmapDrawable = getDrawable();
if (bitmapDrawable instanceof BitmapDrawable) {
bitmapDrawable.setFilterBitmap(false);
}
}
}

View file

@ -0,0 +1,58 @@
package com.panda3ds.pandroid.view.gamesgrid;
import android.content.Context;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.panda3ds.pandroid.data.game.GameMetadata;
import java.util.List;
public class GamesGridView extends RecyclerView {
private int iconSize = 170;
private final GameAdapter adapter;
public GamesGridView(@NonNull Context context) {
this(context, null);
}
public GamesGridView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public GamesGridView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setLayoutManager(new AutoFitLayout());
setAdapter(adapter = new GameAdapter());
}
public void setGameList(List<GameMetadata> games) {
adapter.replace(games);
}
public void setIconSize(int iconSize) {
this.iconSize = iconSize;
requestLayout();
measure(MeasureSpec.EXACTLY, MeasureSpec.EXACTLY);
}
private final class AutoFitLayout extends GridLayoutManager {
public AutoFitLayout() {
super(GamesGridView.this.getContext(), 1);
}
@Override
public void onMeasure(@NonNull Recycler recycler, @NonNull State state, int widthSpec, int heightSpec) {
super.onMeasure(recycler, state, widthSpec, heightSpec);
int width = getMeasuredWidth();
int iconSize = (int) (GamesGridView.this.iconSize * getResources().getDisplayMetrics().density);
int iconCount = Math.max(1, width / iconSize);
if (getSpanCount() != iconCount)
setSpanCount(iconCount);
}
}
}

View file

@ -0,0 +1,28 @@
package com.panda3ds.pandroid.view.gamesgrid;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.recyclerview.widget.RecyclerView;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.data.game.GameMetadata;
import com.panda3ds.pandroid.utils.GameUtils;
class ItemHolder extends RecyclerView.ViewHolder {
public ItemHolder(@NonNull View itemView) {
super(itemView);
}
public void apply(GameMetadata game) {
((AppCompatTextView) itemView.findViewById(R.id.title))
.setText(game.getTitle());
((AppCompatTextView) itemView.findViewById(R.id.description))
.setText(game.getPublisher());
itemView.setOnClickListener((v) -> {
GameUtils.launch(v.getContext(), game);
});
}
}

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M9.93,13.5h4.14L12,7.98zM20,2L4,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM15.95,18.5l-1.14,-3L9.17,15.5l-1.12,3L5.96,18.5l5.11,-13h1.86l5.11,13h-2.09z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M21,6L3,6c-1.1,0 -2,0.9 -2,2v8c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,8c0,-1.1 -0.9,-2 -2,-2zM11,13L8,13v3L6,16v-3L3,13v-2h3L6,8h2v3h3v2zM15.5,15c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5zM19.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S18.67,9 19.5,9s1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z"/>
</vector>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<solid android:color="#FFF"/>
<corners android:radius="9999dp"/>
</shape>
</item>
</selector>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<corners android:radius="8dp"/>
<solid android:color="#FFF"/>
</shape>
</item>
</selector>

View file

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<solid android:color="#5FFF"/>
<stroke android:color="#6FFF" android:width="1dp"/>
<corners android:topLeftRadius="8dp" android:topRightRadius="8dp"
android:bottomLeftRadius="8dp"
android:bottomRightRadius="8dp"/>
</shape>
</item>
</selector>

View file

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<solid android:color="#5FFF"/>
<stroke android:color="#6FFF" android:width="1dp"/>
<corners android:topLeftRadius="8dp" android:topRightRadius="8dp"
android:bottomLeftRadius="8dp"
android:bottomRightRadius="16dp"/>
</shape>
</item>
</selector>

View file

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<solid android:color="#5FFF"/>
<stroke android:color="#6FFF" android:width="1dp"/>
<corners android:topLeftRadius="8dp" android:topRightRadius="8dp"
android:bottomLeftRadius="16dp"
android:bottomRightRadius="8dp"/>
</shape>
</item>
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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"
tools:context=".app.MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/navigation"
app:layout_constraintBottom_toBottomOf="parent"/>
<com.google.android.material.navigationrail.NavigationRailView
android:id="@+id/navigation"
android:layout_width="wrap_content"
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/main_activity_navigation"
app:labelVisibilityMode="selected"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center">
<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@drawable/ic_key_a"
android:backgroundTint="?colorOnSurfaceVariant"/>
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:textSize="20sp"
android:textStyle="bold"
android:text="@string/press_any_key"/>
</androidx.appcompat.widget.LinearLayoutCompat>

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
<androidx.constraintlayout.widget.ConstraintLayout
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"
@ -7,16 +7,22 @@
android:layout_height="match_parent"
tools:context=".app.MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="bottom|end"
android:padding="17dp">
<Button
android:id="@+id/load_rom"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:text="@string/load_rom"/>
</LinearLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@id/navigation"/>
</FrameLayout>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/fragment_container"
app:labelVisibilityMode="selected"
app:menu="@menu/main_activity_navigation"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="64dp"
app:title="@string/settings" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.appcompat.widget.LinearLayoutCompat>

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.panda3ds.pandroid.view.gamesgrid.GamesGridView
android:id="@+id/games"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="15dp"
android:paddingEnd="15dp"/>
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/add_rom"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginEnd="20dp"
android:layout_marginBottom="20dp"
android:layout_gravity="end|bottom"
android:src="@drawable/ic_add"
android:tint="?colorOnPrimary"
android:background="@drawable/simple_card_background"
android:backgroundTint="?colorPrimary"/>
</FrameLayout>

View file

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="64dp"
android:paddingHorizontal="20dp">
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_gravity="center"
android:hint="@string/search"
android:paddingEnd="10dp"
android:paddingStart="50dp"
android:textSize="16sp"
android:background="@drawable/search_bar_background"
android:backgroundTint="?colorSurfaceVariant" />
<View
android:layout_width="24dp"
android:layout_height="24dp"
android:background="@drawable/ic_search"
android:backgroundTint="?colorOnSurfaceVariant"
android:layout_gravity="start|center"
android:layout_marginStart="15dp"/>
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="10dp"
android:paddingEnd="10dp">
<com.panda3ds.pandroid.view.gamesgrid.GamesGridView
android:id="@+id/games"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
</LinearLayout>

View file

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat
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:orientation="vertical"
android:padding="15dp">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="16dp">
<com.panda3ds.pandroid.view.gamesgrid.GameIconView
android:id="@+id/cover"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:src="@drawable/temp_thumb"/>
</androidx.cardview.widget.CardView>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:layout_marginTop="8dp"
android:textStyle="bold"
android:gravity="center"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textSize="10sp"
android:gravity="center"/>
</androidx.appcompat.widget.LinearLayoutCompat>

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="320dp"
android:orientation="vertical"
android:gravity="center">
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="128dp"
android:layout_height="128dp"
android:src="@mipmap/ic_launcher"/>
<TextView
android:id="@android:id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:textSize="24sp"
android:textColor="?colorOnSurface"/>
<TextView
android:id="@android:id/summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:alpha="0.5"
android:textColor="?colorOnSurface"/>
</androidx.appcompat.widget.LinearLayoutCompat>

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="70dp"
android:layout_marginVertical="10dp"
android:layout_marginHorizontal="10dp"
android:background="@drawable/simple_card_background"
android:backgroundTint="?colorSurfaceVariant">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatImageView
android:id="@android:id/icon"
android:layout_width="70dp"
android:layout_height="70dp"
android:src="@drawable/ic_add"
android:tint="?colorOnSurfaceVariant"
android:padding="20dp"/>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_marginStart="5dp"
android:gravity="start|center">
<androidx.appcompat.widget.AppCompatTextView
android:id="@android:id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?colorOnSurfaceVariant"
android:textSize="18sp"
android:textStyle="bold"
tools:text="@string/app_name"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@android:id/summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?colorOnSurfaceVariant"
tools:text="@string/app_name"
android:alpha="0.7"/>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</FrameLayout>

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<group>
<item
android:id="@+id/games"
android:icon="@drawable/ic_videogame"
android:title="@string/games"
android:checkable="true"/>
<item
android:id="@+id/search"
android:icon="@drawable/ic_search"
android:title="@string/search"
android:checkable="true"/>
<item
android:id="@+id/settings"
android:icon="@drawable/ic_settings"
android:title="@string/settings"
android:checkable="true"/>
</group>
</menu>

View file

@ -1,7 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.Pandroid" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your dark theme here. -->
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
</style>
</resources>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">pandroid</string>
<string name="load_rom">Carregar ROM</string>
<string name="games">Jogos</string>
<string name="settings">Configurações</string>
<string name="search">Pesquisar</string>
<string name="unknown">Desconhecido</string>
<string name="left">Esquerda</string>
<string name="right">Direita</string>
<string name="up">Cima</string>
<string name="down">Baixo</string>
<string name="others">Outros</string>
<string name="press_any_key">Pressione qualquer tecla</string>
<string name="axis">Eixos</string>
</resources>

View file

@ -1,4 +1,17 @@
<resources>
<string name="app_name">pandroid</string>
<string name="load_rom">Load ROM</string>
<string name="games">Games</string>
<string name="settings">Settings</string>
<string name="search">Search</string>
<string name="unknown">Unknown</string>
<string name="left">Left</string>
<string name="right">Right</string>
<string name="up">Up</string>
<string name="down">Down</string>
<string name="others">Others</string>
<string name="press_any_key">Press any key</string>
<string name="axis">Axis</string>
</resources>

View file

@ -9,4 +9,7 @@
<item name="android:backgroundTintMode">multiply</item>
<item name="android:backgroundTint">#FFF</item>
</style>
<style name="HiddenTextAppearance">
<item name="android:textSize">0.1px</item>
</style>
</resources>

View file

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
android:title="@string/axis"
app:iconSpaceReserved="false">
<Preference
app:title="@string/up"
app:key="AXIS_UP"
app:summary="none"
app:iconSpaceReserved="false"/>
<Preference
app:title="@string/down"
app:key="AXIS_DOWN"
app:summary="none"
app:iconSpaceReserved="false"/>
<Preference
app:title="@string/left"
app:key="AXIS_LEFT"
app:summary="none"
app:iconSpaceReserved="false"/>
<Preference
app:title="@string/right"
app:key="AXIS_RIGHT"
app:summary="none"
app:iconSpaceReserved="false"/>
</PreferenceCategory>
<PreferenceCategory
android:title="Dpad"
app:iconSpaceReserved="false">
<Preference
app:title="@string/up"
app:key="UP"
app:summary="none"
app:iconSpaceReserved="false"/>
<Preference
app:title="@string/down"
app:key="DOWN"
app:summary="none"
app:iconSpaceReserved="false"/>
<Preference
app:title="@string/left"
app:key="LEFT"
app:summary="none"
app:iconSpaceReserved="false"/>
<Preference
app:title="@string/right"
app:key="RIGHT"
app:summary="none"
app:iconSpaceReserved="false"/>
</PreferenceCategory>
<PreferenceCategory
android:title="Buttons"
app:iconSpaceReserved="false">
<Preference
app:title="A"
app:key="A"
app:summary="none"
app:iconSpaceReserved="false"/>
<Preference
app:title="B"
app:key="B"
app:summary="none"
app:iconSpaceReserved="false"/>
<Preference
app:title="X"
app:key="X"
app:summary="none"
app:iconSpaceReserved="false"/>
<Preference
app:title="Y"
app:key="Y"
app:summary="none"
app:iconSpaceReserved="false"/>
</PreferenceCategory>
<PreferenceCategory
android:title="@string/others"
app:iconSpaceReserved="false">
<Preference
app:title="L"
app:key="L"
app:summary="none"
app:iconSpaceReserved="false"/>
<Preference
app:title="R"
app:key="R"
app:summary="none"
app:iconSpaceReserved="false"/>
<Preference
app:title="Start"
app:key="START"
app:summary="none"
app:iconSpaceReserved="false"/>
<Preference
app:title="Select"
app:key="SELECT"
app:summary="none"
app:iconSpaceReserved="false"/>
</PreferenceCategory>
</PreferenceScreen>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<Preference
android:title="@string/app_name"
android:enabled="false"
android:summary="1.0"
android:layout="@layout/preference_simple_about"/>
<Preference
android:key="inputMap"
android:icon="@drawable/ic_key_a"
android:title="Controller Mapping"
android:summary="Map physics controller or keyboard"
android:layout="@layout/preference_start_item"/>
</PreferenceScreen>