added sort game options

This commit is contained in:
izzy2lost
2025-09-01 15:36:45 -04:00
parent 5807d6cce6
commit a5e5b934e1
19 changed files with 787 additions and 516 deletions
@@ -112,11 +112,21 @@ public class CoversAdapter extends RecyclerView.Adapter<CoversAdapter.VH> {
}
}
holder.itemView.setOnClickListener(v -> {
if (onItemClick != null) onItemClick.onClick(real);
if (onItemClick != null) {
int pos = holder.getBindingAdapterPosition();
if (pos == RecyclerView.NO_POSITION) pos = position; // fallback
int count2 = titles.length;
int realNow = (count2 == 0) ? 0 : (pos % count2);
onItemClick.onClick(realNow);
}
});
holder.itemView.setOnLongClickListener(v -> {
if (onItemLongClick != null) {
onItemLongClick.onLongClick(real);
int pos = holder.getBindingAdapterPosition();
if (pos == RecyclerView.NO_POSITION) pos = position; // fallback
int count2 = titles.length;
int realNow = (count2 == 0) ? 0 : (pos % count2);
onItemLongClick.onLongClick(realNow);
return true;
}
return false;
@@ -14,6 +14,9 @@ import android.widget.Toast;
import android.os.Build;
import android.view.WindowInsets;
import android.view.WindowInsetsController;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -24,6 +27,9 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.PagerSnapHelper;
import androidx.recyclerview.widget.RecyclerView;
import java.util.Locale;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
public class GamesCoverDialogFragment extends DialogFragment {
private boolean didInitialNudge = false;
@@ -37,11 +43,34 @@ public class GamesCoverDialogFragment extends DialogFragment {
private String[] uris;
private String[] coverUrls;
private String[] localPaths;
private String[] origTitles;
private String[] origUris;
private String[] origCoverUrls;
private String[] origLocalPaths;
private RecyclerView rv;
private LinearLayoutManager llm;
private PagerSnapHelper snapHelper;
private int lastRvW = -1, lastRvH = -1;
private boolean pendingResnap = false;
private int lastItemWidthPx = 0;
private RecyclerView rvLetters;
private LinearLayoutManager llmLetters;
private PagerSnapHelper lettersSnapHelper;
private LettersAdapter lettersAdapter;
private java.util.ArrayList<Character> letters = new java.util.ArrayList<>();
private Character pendingLetterJump = null;
private static final int SORT_ALPHA = 0;
private static final int SORT_RECENT = 1;
private int sortMode = SORT_ALPHA;
private String query = null;
// Simpler grouping/sort helpers (revert)
private static char firstLetter(String t) {
if (t == null) return '#';
String s = t.trim();
if (s.isEmpty()) return '#';
char c = Character.toUpperCase(s.charAt(0));
return Character.isLetter(c) ? c : '#';
}
public interface OnGameSelectedListener {
void onGameSelected(String gameUri);
@@ -98,6 +127,55 @@ public class GamesCoverDialogFragment extends DialogFragment {
// Snap to center item
snapHelper = new PagerSnapHelper();
snapHelper.attachToRecyclerView(rv);
// Letters row setup
rvLetters = root.findViewById(R.id.recycler_letters);
if (rvLetters != null) {
rvLetters.setHasFixedSize(true);
llmLetters = new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false);
rvLetters.setLayoutManager(llmLetters);
rvLetters.setClipToPadding(false);
int basePad = (int)(24*getResources().getDisplayMetrics().density);
rvLetters.setPadding(basePad, 0, basePad, 0);
lettersSnapHelper = new PagerSnapHelper();
lettersSnapHelper.attachToRecyclerView(rvLetters);
// Add spacing between letters for off-screen effect
final int letterSpace = (int)(12 * getResources().getDisplayMetrics().density);
rvLetters.addItemDecoration(new RecyclerView.ItemDecoration() {
@Override
public void getItemOffsets(@NonNull android.graphics.Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
int pos = parent.getChildAdapterPosition(view);
if (pos == RecyclerView.NO_POSITION) return;
outRect.left = letterSpace;
outRect.right = letterSpace;
}
});
rvLetters.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
applyLetterTransforms(recyclerView);
}
@Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE) applyLetterTransforms(recyclerView);
}
});
// Initial snap + dynamic side padding after first layout to keep center item centered
rvLetters.getViewTreeObserver().addOnGlobalLayoutListener(new android.view.ViewTreeObserver.OnGlobalLayoutListener() {
@Override public void onGlobalLayout() {
int w = rvLetters.getWidth();
if (w > 0) {
float d = getResources().getDisplayMetrics().density;
int itemPx = (int) (72 * d); // item_letter width
int letterSpace = (int) (12 * d);
int totalItem = itemPx + 2 * letterSpace;
int side = Math.max((int)(24 * d), (w - totalItem) / 2);
rvLetters.setPadding(side, 0, side, 0);
}
rvLetters.getViewTreeObserver().removeOnGlobalLayoutListener(this);
rvLetters.post(() -> { resnapLetters(); applyLetterTransforms(rvLetters); });
}
});
}
// Scale/alpha transform based on distance from center
rv.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
@@ -142,6 +220,14 @@ public class GamesCoverDialogFragment extends DialogFragment {
localPaths[i] = new java.io.File(getCoversDir(), serial + ".png").getAbsolutePath();
}
// cache originals for sorting/filtering
origTitles = Arrays.copyOf(titles, titles.length);
origUris = Arrays.copyOf(uris, uris.length);
origCoverUrls = Arrays.copyOf(coverUrls, coverUrls.length);
origLocalPaths = Arrays.copyOf(localPaths, localPaths.length);
// restore sort pref if any
sortMode = prefs.getInt("covers_sort_mode", SORT_ALPHA);
adapter = new CoversAdapter(requireContext(), titles, coverUrls, localPaths, R.layout.item_coverflow,
position -> {
if (listener != null && position >= 0 && position < uris.length) {
@@ -155,6 +241,30 @@ public class GamesCoverDialogFragment extends DialogFragment {
}
});
rv.setAdapter(adapter);
// Build letters list if row present (RecyclerView variant)
if (rvLetters != null) buildLettersAndBind();
// Hook sort/search buttons if present
View btnSortV = root.findViewById(R.id.btn_sort);
if (btnSortV instanceof com.google.android.material.button.MaterialButton) {
com.google.android.material.button.MaterialButton btnSort = (com.google.android.material.button.MaterialButton) btnSortV;
updateSortButtonUi(btnSort);
btnSort.setOnClickListener(v -> {
sortMode = (sortMode == SORT_ALPHA) ? SORT_RECENT : SORT_ALPHA;
requireContext().getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
.edit().putInt("covers_sort_mode", sortMode).apply();
applyFilterAndSort();
updateSortButtonUi(btnSort);
});
}
View btnSearch = root.findViewById(R.id.btn_search);
if (btnSearch != null) {
btnSearch.setOnClickListener(v -> showSearchDialog());
}
// apply initial sort/filter if needed
if (sortMode != SORT_ALPHA || (query != null && !query.isEmpty())) {
applyFilterAndSort();
}
// One-time tiny nudge to force snap/transform on some devices
rv.post(() -> {
if (!isAdded() || didInitialNudge) return;
@@ -192,11 +302,19 @@ public class GamesCoverDialogFragment extends DialogFragment {
String t = TitleResolver.resolveTitleForUri(requireContext(), uris[i], titles[i]);
if (t != null && !t.isEmpty() && i < titles.length && !t.equals(titles[i])) {
titles[i] = t;
if (origTitles != null && i < origTitles.length) origTitles[i] = t;
changed = true;
}
} catch (Throwable ignored) {}
}
if (changed && isAdded()) requireActivity().runOnUiThread(() -> adapter.notifyDataSetChanged());
if (changed && isAdded()) requireActivity().runOnUiThread(() -> {
if (sortMode != SORT_ALPHA || (query != null && !query.isEmpty())) {
applyFilterAndSort();
} else {
adapter.notifyDataSetChanged();
if (rvLetters != null) buildLettersAndBind();
}
});
}).start();
// Dynamically size items based on RecyclerView size and orientation
@@ -219,7 +337,18 @@ public class GamesCoverDialogFragment extends DialogFragment {
int widthFromHeight = (int) (availableH * ratio);
int widthFromWidth = (int) (rvW * (landscape ? 0.35f : 0.55f));
int itemWidth = Math.max(160, Math.min(widthFromHeight, widthFromWidth));
lastItemWidthPx = itemWidth;
adapter.setItemWidthPx(itemWidth);
// Center vertically in portrait by adjusting top/bottom padding
if (!landscape) {
int imageH = (int) (itemWidth / (567f / 878f));
int titlePx = (int) ((titleDp + extraDp) * density);
int contentH = imageH + titlePx;
int desiredPad = Math.max(vertPad, Math.max(0, (rvH - contentH) / 2));
rv.setPadding(sidePad, desiredPad, sidePad, desiredPad);
} else {
rv.setPadding(sidePad, vertPad, sidePad, vertPad);
}
// If user is scrolling, defer resnap until idle to avoid fighting gesture
if (rv.getScrollState() == RecyclerView.SCROLL_STATE_IDLE) {
resnapToCenter(rv);
@@ -238,6 +367,267 @@ public class GamesCoverDialogFragment extends DialogFragment {
return root;
}
private void buildLettersAndBind() {
letters.clear();
boolean hasHash = false;
boolean[] present = new boolean[26];
if (titles != null) {
for (String t : titles) {
if (t == null) continue;
char c = firstLetter(t);
if (c >= 'A' && c <= 'Z') present[c - 'A'] = true;
else hasHash = true;
}
}
if (hasHash) letters.add('#');
for (int i = 0; i < 26; i++) if (present[i]) letters.add((char)('A'+i));
if (rvLetters != null) {
lettersAdapter = new LettersAdapter(letters, this::onLetterTapped);
rvLetters.setAdapter(lettersAdapter);
// Anchor to a large middle position for infinite wrap-around
int n = letters.size();
if (n > 0) {
int center = (1 << 29);
int startPos = center - (center % n);
llmLetters.scrollToPosition(startPos);
}
rvLetters.post(() -> { resnapLetters(); applyLetterTransforms(rvLetters); });
}
}
private void applyLetterTransforms(@NonNull RecyclerView recyclerView) {
int rvCenterX = (recyclerView.getLeft() + recyclerView.getRight()) / 2;
final float maxScale = 1.0f;
final float minScale = 0.85f;
final float maxAlpha = 1.0f;
final float minAlpha = 0.6f;
for (int i = 0; i < recyclerView.getChildCount(); i++) {
View child = recyclerView.getChildAt(i);
int childCenterX = (child.getLeft() + child.getRight()) / 2;
float dx = Math.abs(childCenterX - rvCenterX);
float norm = Math.min(1f, dx / (recyclerView.getWidth() * 0.5f));
float scale = maxScale - (maxScale - minScale) * norm;
float alpha = maxAlpha - (maxAlpha - minAlpha) * norm;
child.setScaleX(scale);
child.setScaleY(scale);
child.setAlpha(alpha);
}
}
private void buildLetterIndexLinear(@NonNull LinearLayout container) {
container.removeAllViews();
// Build present letters with normalization
java.util.LinkedHashSet<Character> set = new java.util.LinkedHashSet<>();
if (titles != null) {
for (String t : titles) {
char c = firstLetter(t);
if (c == '#') { set.add('#'); }
else if (c >= 'A' && c <= 'Z') set.add(c);
}
}
float d = getResources().getDisplayMetrics().density;
int padH = (int) (14 * d);
int padV = (int) (6 * d);
for (Character ch : set) {
TextView tv = new TextView(requireContext());
tv.setText(String.valueOf(ch));
tv.setTextSize(18);
tv.setTextColor(getResources().getColor(R.color.brand_primary));
tv.setPadding(padH, padV, padH, padV);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
lp.setMargins((int)(8*d), 0, (int)(8*d), 0); // extra spacing
tv.setLayoutParams(lp);
tv.setOnClickListener(v -> onLetterTapped(ch));
container.addView(tv);
}
}
private void onLetterTapped(char letter) {
android.util.Log.d("LetterTap", "Letter tapped: " + letter + ", sortMode: " + sortMode + ", titles.length: " + (titles != null ? titles.length : 0));
// Force AZ sorting for predictable letter navigation
if (sortMode != SORT_ALPHA) {
android.util.Log.d("LetterTap", "Switching to ALPHA sort mode");
sortMode = SORT_ALPHA;
requireContext().getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
.edit().putInt("covers_sort_mode", sortMode).apply();
pendingLetterJump = letter;
applyFilterAndSort();
} else {
android.util.Log.d("LetterTap", "Already in ALPHA mode, jumping to letter");
// Jump to first index for that letter in the current (alphabetical) list
jumpToLetter(letter);
}
// Center letters row on selected
if (rvLetters != null && letters != null && !letters.isEmpty()) {
int idx = letters.indexOf(letter);
if (idx >= 0) {
centerLettersOnIndex(idx);
}
}
}
private void centerLettersOnIndex(int idx) {
if (rvLetters == null || llmLetters == null) return;
View snapView = lettersSnapHelper != null ? lettersSnapHelper.findSnapView(llmLetters) : null;
int centerPos = (snapView != null) ? rvLetters.getChildAdapterPosition(snapView) : llmLetters.findFirstVisibleItemPosition();
if (centerPos == RecyclerView.NO_POSITION) centerPos = 0;
int n = letters != null ? letters.size() : 0;
if (n <= 0) return;
int centerIdx = centerPos % n;
int forward = (idx - centerIdx + n) % n;
int backward = (centerIdx - idx + n) % n;
int delta = (forward <= backward) ? forward : -backward;
llmLetters.scrollToPosition(centerPos + delta);
rvLetters.post(() -> { resnapLetters(); applyLetterTransforms(rvLetters); });
}
private void resnapLetters() {
if (rvLetters == null || llmLetters == null || lettersSnapHelper == null) return;
View snap = lettersSnapHelper.findSnapView(llmLetters);
if (snap == null) return;
int[] dist = lettersSnapHelper.calculateDistanceToFinalSnap(llmLetters, snap);
if (dist != null && (dist[0] != 0 || dist[1] != 0)) rvLetters.scrollBy(dist[0], dist[1]);
}
private void updateSortButtonUi(@NonNull com.google.android.material.button.MaterialButton btn) {
btn.setIconResource(R.drawable.sort_24px);
boolean alpha = (sortMode == SORT_ALPHA);
btn.setText(alpha ? "AZ" : "RECENT");
btn.setContentDescription(alpha ? "Sort AZ" : "Sort Recent");
}
private void showSearchDialog() {
final EditText input = new EditText(requireContext());
input.setHint("Search games");
int pad = (int) (16 * getResources().getDisplayMetrics().density);
input.setPadding(pad, pad, pad, pad);
new com.google.android.material.dialog.MaterialAlertDialogBuilder(requireContext(),
com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog)
.setTitle("Search")
.setView(input)
.setNegativeButton("Clear", (d, w) -> {
query = null;
applyFilterAndSort();
})
.setPositiveButton("Apply", (d, w) -> {
query = input.getText() != null ? input.getText().toString().trim() : null;
if (query != null && query.isEmpty()) query = null;
applyFilterAndSort();
})
.show();
}
private void jumpToLetter(char letterRaw) {
android.util.Log.d("LetterJump", "jumpToLetter called with: " + letterRaw);
if (titles == null || titles.length == 0) {
android.util.Log.d("LetterJump", "No titles available");
return;
}
char letter = Character.toUpperCase(letterRaw);
int target = -1;
for (int i = 0; i < titles.length; i++) {
char c = firstLetter(titles[i]);
if ((letter == '#') ? (c == '#') : (c == letter)) {
target = i;
android.util.Log.d("LetterJump", "Found target at index " + i + " for letter " + letter + ", title: " + titles[i]);
break;
}
}
if (target >= 0) {
android.util.Log.d("LetterJump", "Scrolling to index: " + target);
scrollToIndex(target);
} else {
android.util.Log.d("LetterJump", "No target found for letter: " + letter);
}
}
private void scrollToIndex(int idx) {
if (llm == null || rv == null || titles == null || titles.length == 0) return;
View snap = snapHelper != null ? snapHelper.findSnapView(llm) : null;
int centerPos = (snap != null) ? rv.getChildAdapterPosition(snap) : llm.findFirstVisibleItemPosition();
if (centerPos == RecyclerView.NO_POSITION) centerPos = 0;
int n = titles.length;
int centerIdx = centerPos % n;
int forward = (idx - centerIdx + n) % n;
int backward = (centerIdx - idx + n) % n;
int delta = (forward <= backward) ? forward : -backward;
llm.scrollToPosition(centerPos + delta);
rv.post(() -> { resnapToCenter(rv); applyCoverflowTransforms(rv); });
}
private void applyFilterAndSort() {
int n = origTitles != null ? origTitles.length : 0;
ArrayList<Integer> idxs = new ArrayList<>(n);
for (int i = 0; i < n; i++) {
if (query == null || query.isEmpty()) {
idxs.add(i);
} else {
String t = origTitles[i] != null ? origTitles[i] : "";
if (t.toLowerCase(Locale.ROOT).contains(query.toLowerCase(Locale.ROOT))) idxs.add(i);
}
}
SharedPreferences prefs = requireContext().getSharedPreferences("app_prefs", Context.MODE_PRIVATE);
if (sortMode == SORT_RECENT) {
idxs.sort((a, b) -> {
long ta = prefs.getLong("last_played:" + origUris[a], 0L);
long tb = prefs.getLong("last_played:" + origUris[b], 0L);
if (ta == tb) return origTitles[a].compareToIgnoreCase(origTitles[b]);
return Long.compare(tb, ta);
});
} else {
idxs.sort(Comparator.comparing(i -> {
String t = origTitles[i];
return t == null ? "" : t.toLowerCase(Locale.ROOT);
}));
}
titles = new String[idxs.size()];
uris = new String[idxs.size()];
coverUrls = new String[idxs.size()];
localPaths = new String[idxs.size()];
for (int k = 0; k < idxs.size(); k++) {
int i = idxs.get(k);
titles[k] = origTitles[i];
uris[k] = origUris[i];
coverUrls[k] = origCoverUrls[i];
localPaths[k] = origLocalPaths[i];
}
adapter = new CoversAdapter(requireContext(), titles, coverUrls, localPaths, R.layout.item_coverflow,
position -> {
if (listener != null && position >= 0 && position < uris.length) {
listener.onGameSelected(uris[position]);
dismissAllowingStateLoss();
}
},
position -> {
if (position >= 0 && position < uris.length) {
showGameSettings(titles[position], uris[position]);
}
});
rv.setAdapter(adapter);
if (lastItemWidthPx > 0) adapter.setItemWidthPx(lastItemWidthPx);
int n2 = titles.length;
if (n2 > 0) {
int center = (1 << 29);
int startPos = center - (center % n2);
llm.scrollToPosition(startPos);
}
rv.post(() -> { resnapToCenter(rv); applyCoverflowTransforms(rv); });
if (rvLetters != null) {
buildLettersAndBind();
if (pendingLetterJump != null) {
final char l = pendingLetterJump;
pendingLetterJump = null;
rv.postDelayed(() -> {
jumpToLetter(l);
if (letters != null) {
int idx = letters.indexOf(l);
if (idx >= 0) centerLettersOnIndex(idx);
}
}, 10);
}
}
}
private void applyCoverflowTransforms(@NonNull RecyclerView recyclerView) {
int rvCenterX = (recyclerView.getLeft() + recyclerView.getRight()) / 2;
boolean landscape = getResources().getConfiguration().orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE;
@@ -0,0 +1,69 @@
package com.izzy2lost.psx2;
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.List;
public class LettersAdapter extends RecyclerView.Adapter<LettersAdapter.VH> {
public interface OnClick {
void onClick(char letter);
}
private final List<Character> letters;
private final OnClick onClick;
public LettersAdapter(List<Character> letters, OnClick onClick) {
this.letters = letters;
this.onClick = onClick;
setHasStableIds(true);
}
@Override
public long getItemId(int position) {
// Ensure uniqueness across the infinite range by including position
return position;
}
@NonNull
@Override
public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_letter, parent, false);
return new VH(v);
}
@Override
public void onBindViewHolder(@NonNull VH holder, int position) {
int n = letters != null ? letters.size() : 0;
if (n == 0) return;
int real = position % n;
char ch = letters.get(real);
holder.text.setText(String.valueOf(ch));
holder.itemView.setOnClickListener(v -> {
android.util.Log.d("LettersAdapter", "Letter clicked: " + ch + " at position " + position);
if (onClick != null) {
onClick.onClick(ch);
} else {
android.util.Log.d("LettersAdapter", "onClick is null!");
}
});
}
@Override
public int getItemCount() {
return (letters != null && !letters.isEmpty()) ? Integer.MAX_VALUE : 0;
}
static class VH extends RecyclerView.ViewHolder {
final TextView text;
VH(@NonNull View itemView) {
super(itemView);
text = itemView.findViewById(R.id.text_letter);
}
}
}
@@ -246,6 +246,11 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
}
}
// Enable immersive mode to hide navigation and status bars
private void enableImmersiveMode() {
hideStatusBar();
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
@@ -297,6 +302,11 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
if (!TextUtils.isEmpty(gameUri)) {
// Avoid any pre-VM native calls here; just set the game and launch.
m_szGamefile = gameUri;
// Record last played timestamp for sorting
try {
SharedPreferences prefs = getSharedPreferences("app_prefs", MODE_PRIVATE);
prefs.edit().putLong("last_played:" + gameUri, System.currentTimeMillis()).apply();
} catch (Throwable ignored) {}
restartEmuThread();
}
}
@@ -372,7 +382,7 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
getSupportActionBar().hide();
}
setContentView(R.layout.activity_main);
hideStatusBar();
enableImmersiveMode();
// Setup back button handler
setupBackPressedHandler();
@@ -397,14 +407,10 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
// Ensure consistent ripple across all MaterialButtons
tintAllMaterialButtonOutlines();
// Settings are already applied in Initialize() -> loadAndApplySettings()
// No need to call applySavedSettings() again here
// Apply orientation-specific constraints once at startup
int currentOrientation = getResources().getConfiguration().orientation;
applyConstraintsForOrientation(currentOrientation);
// Prompt for BIOS if missing
maybePromptForBios();
@@ -437,10 +443,6 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
getOnBackPressedDispatcher().addCallback(this, callback);
}
private void makeButtonTouch() {
MaterialButton btn_file = findViewById(R.id.btn_file);
if(btn_file != null) {
@@ -493,8 +495,6 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
});
}
// Games button handled above
// Settings button opens dialog
MaterialButton btn_settings = findViewById(R.id.btn_settings);
if (btn_settings != null) {
@@ -513,10 +513,6 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
});
}
// HUD toggle moved to Settings (Developer section)
// Hide UI button (removed - functionality moved to toggle button)
// Small unhide button (appears when all UI is hidden)
MaterialButton btn_unhide_ui = findViewById(R.id.btn_unhide_ui);
if(btn_unhide_ui != null) {
@@ -525,9 +521,7 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
});
}
//////
// RENDERER
MaterialButton btn_ogl = findViewById(R.id.btn_ogl);
if(btn_ogl != null) {
btn_ogl.setOnClickListener(v -> {
@@ -588,9 +582,7 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
});
}
//////
// PAD
MaterialButton btn_pad_select = findViewById(R.id.btn_pad_select);
if(btn_pad_select != null) {
btn_pad_select.setOnTouchListener((v, event) -> {
@@ -605,7 +597,6 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
return true;
});
}
MaterialButton btn_pad_a = findViewById(R.id.btn_pad_a);
if(btn_pad_a != null) {
btn_pad_a.setOnTouchListener((v, event) -> {
@@ -635,8 +626,7 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
});
}
////
//// Shoulder buttons
MaterialButton btn_pad_l1 = findViewById(R.id.btn_pad_l1);
if(btn_pad_l1 != null) {
btn_pad_l1.setOnTouchListener((v, event) -> {
@@ -682,8 +672,7 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
});
}
////
//// D-Pad
final int PAD_L_UP = 110;
final int PAD_L_RIGHT = 111;
final int PAD_L_DOWN = 112;
@@ -796,8 +785,7 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
});
}
////
//// D-Pad buttons
MaterialButton btn_pad_dir_top = findViewById(R.id.btn_pad_dir_top);
if(btn_pad_dir_top != null) {
btn_pad_dir_top.setOnTouchListener((v, event) -> {
@@ -1204,6 +1192,10 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
if(_intent != null) {
m_szGamefile = _intent.getDataString();
if(!TextUtils.isEmpty(m_szGamefile)) {
try {
SharedPreferences prefs = getSharedPreferences("app_prefs", MODE_PRIVATE);
prefs.edit().putLong("last_played:" + m_szGamefile, System.currentTimeMillis()).apply();
} catch (Throwable ignored) {}
restartEmuThread();
}
}
@@ -1393,7 +1385,7 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
mHIDDeviceManager.setFrozen(false);
}
// Re-assert full screen when returning to the activity
hideStatusBar();
enableImmersiveMode();
updateUiForControllerPresence();
}
@@ -1423,8 +1415,6 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
android.os.Process.killProcess(appPid);
}
//////////////////////////////////////////////////////////////////////////////////////////////
public void Initialize() {
NativeApp.initializeOnce(getApplicationContext());
@@ -1441,8 +1431,6 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
// Initialize HID device manager for USB and Bluetooth controllers
mHIDDeviceManager.initialize(true, true);
// Avoid forcing renderer init when no game/surface is active.
// Renderer is applied on game start (restartEmuThread) and from settings when appropriate.
}
private void setSurfaceView(Object p_value) {
@@ -1499,8 +1487,6 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
}
}
//////////////////////////////////////////////////////////////////////////////////////////////
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
// Use only our controller handler - disable SDL fallback to avoid conflicts
@@ -1581,7 +1567,6 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
}
}
//////////////////////////////////////////////////////////////////////////////////////////////
// Asset copy helpers used on first launch to seed default resources
private void copyAssetAll(Context context, String srcPath) {
AssetManager assetMgr = context.getAssets();
@@ -1630,8 +1615,6 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
}
}
@Override
public void onBackPressed() {
// Fallback for older Android versions
@@ -1658,8 +1641,6 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
.show();
}
// ControllerInputHandler.ControllerInputListener implementation
@Override
public void onControllerButtonPressed(int controllerId, int button, boolean pressed) {
@@ -1746,9 +1727,7 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
}
private void handleAnalogInput(int axis, float value) {
// Convert analog input to button presses for the native interface
// This matches how AetherSX2 handles analog input
// Convert analog input to button presses for the native interface
// For analog sticks, only send positive values (negative values are handled by opposite direction)
int intensity = Math.max(0, Math.round(Math.abs(value) * 255));
boolean pressed = Math.abs(value) > 0.1f;
+10
View File
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M240,760L360,760L360,520L600,520L600,760L720,760L720,400L480,220L240,400L240,760ZM160,840L160,360L480,120L800,360L800,840L520,840L520,600L440,600L440,840L160,840ZM480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490Z"/>
</vector>
+10
View File
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M440,680L520,680L520,440L440,440L440,680ZM480,360Q497,360 508.5,348.5Q520,337 520,320Q520,303 508.5,291.5Q497,280 480,280Q463,280 451.5,291.5Q440,303 440,320Q440,337 451.5,348.5Q463,360 480,360ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M400,720Q367,720 343.5,696.5Q320,673 320,640L320,590Q263,551 231.5,490Q200,429 200,360Q200,243 281.5,161.5Q363,80 480,80Q597,80 678.5,161.5Q760,243 760,360Q760,429 728.5,489.5Q697,550 640,590L640,640Q640,673 616.5,696.5Q593,720 560,720L400,720ZM400,640L560,640Q560,640 560,640Q560,640 560,640L560,548L594,524Q635,496 657.5,452.5Q680,409 680,360Q680,277 621.5,218.5Q563,160 480,160Q397,160 338.5,218.5Q280,277 280,360Q280,409 302.5,452.5Q325,496 366,524L400,548L400,640Q400,640 400,640Q400,640 400,640ZM400,880Q383,880 371.5,868.5Q360,857 360,840L360,800L600,800L600,840Q600,857 588.5,868.5Q577,880 560,880L400,880ZM480,360Q480,360 480,360Q480,360 480,360L480,360L480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360Q480,360 480,360L480,360L480,360Q480,360 480,360Q480,360 480,360Z"/>
</vector>
+10
View File
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M784,840L532,588Q502,612 463,626Q424,640 380,640Q271,640 195.5,564.5Q120,489 120,380Q120,271 195.5,195.5Q271,120 380,120Q489,120 564.5,195.5Q640,271 640,380Q640,424 626,463Q612,502 588,532L840,784L784,840ZM380,560Q455,560 507.5,507.5Q560,455 560,380Q560,305 507.5,252.5Q455,200 380,200Q305,200 252.5,252.5Q200,305 200,380Q200,455 252.5,507.5Q305,560 380,560Z"/>
</vector>
+11
View File
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M120,720L120,640L360,640L360,720L120,720ZM120,520L120,440L600,440L600,520L120,520ZM120,320L120,240L840,240L840,320L120,320Z"/>
</vector>
@@ -24,7 +24,7 @@
android:background="@android:color/transparent"
android:contentDescription="Home"
android:padding="4dp"
android:src="@drawable/ic_home"
android:src="@drawable/home_24px"
app:tint="@color/brand_primary" />
<View
@@ -33,32 +33,90 @@
android:layout_weight="1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_download"
android:id="@+id/btn_search"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Download Covers" />
</LinearLayout>
android:text=""
android:contentDescription="Search"
app:icon="@drawable/search_24px"
app:iconTint="@color/brand_primary"
app:iconGravity="textStart"
app:iconPadding="8dp"
app:iconSize="24dp" />
<TextView
android:id="@+id/tv_covers_hint"
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_sort"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:contentDescription="Sort"
app:icon="@drawable/sort_24px"
app:iconTint="@color/brand_primary"
app:iconGravity="textStart"
app:iconPadding="8dp"
app:iconSize="24dp" />
</LinearLayout>
<!-- Letters row above covers -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_letters"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="4dp"
android:text="@string/covers_hint_long_press"
android:textColor="@color/brand_primary"
android:textStyle="bold"
android:drawableStart="@drawable/ic_info"
android:drawablePadding="8dp"/>
android:layout_height="56dp"
android:layout_marginTop="0dp"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:clipToPadding="false"
android:overScrollMode="never"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_covers"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:padding="12dp"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="6dp"
android:paddingBottom="0dp"
android:clipToPadding="false" />
<!-- Hint below covers grid: yellow bulb right next to text -->
<LinearLayout
android:id="@+id/hint_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<ImageView
android:id="@+id/img_hint_bulb"
android:layout_width="24dp"
android:layout_height="24dp"
android:tint="@color/covers_hint_yellow"
android:src="@drawable/lightbulb_2_24px"
android:layout_marginEnd="4dp"/>
<TextView
android:id="@+id/tv_covers_hint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/covers_hint_long_press"
android:textColor="@color/brand_primary"
android:textStyle="bold"
android:textSize="12sp"/>
</LinearLayout>
<!-- Download button below covers, centered with COVERS text -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_download"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="COVERS"
android:contentDescription="Download Covers"
app:icon="@drawable/download_24px"
app:iconTint="@color/brand_primary"
app:iconGravity="textStart"
app:iconPadding="20dp"
app:iconSize="34dp" />
</LinearLayout>
@@ -24,7 +24,7 @@
android:background="@android:color/transparent"
android:contentDescription="Home"
android:padding="4dp"
android:src="@drawable/ic_home"
android:src="@drawable/home_24px"
app:tint="@color/brand_primary" />
<View
@@ -33,32 +33,90 @@
android:layout_weight="1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_download"
android:id="@+id/btn_search"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Download Covers" />
</LinearLayout>
android:text=""
android:contentDescription="Search"
app:icon="@drawable/search_24px"
app:iconTint="@color/brand_primary"
app:iconGravity="textStart"
app:iconPadding="8dp"
app:iconSize="24dp" />
<TextView
android:id="@+id/tv_covers_hint"
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_sort"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:contentDescription="Sort"
app:icon="@drawable/sort_24px"
app:iconTint="@color/brand_primary"
app:iconGravity="textStart"
app:iconPadding="8dp"
app:iconSize="24dp" />
</LinearLayout>
<!-- Letters row above covers -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_letters"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="4dp"
android:text="@string/covers_hint_long_press"
android:textColor="@color/brand_primary"
android:textStyle="bold"
android:drawableStart="@drawable/ic_info"
android:drawablePadding="8dp"/>
android:layout_height="56dp"
android:layout_marginTop="0dp"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:clipToPadding="false"
android:overScrollMode="never"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_covers"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:padding="12dp"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="6dp"
android:paddingBottom="0dp"
android:clipToPadding="false" />
<!-- Hint below covers grid: yellow bulb right next to text -->
<LinearLayout
android:id="@+id/hint_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<ImageView
android:id="@+id/img_hint_bulb"
android:layout_width="24dp"
android:layout_height="24dp"
android:tint="@color/covers_hint_yellow"
android:src="@drawable/lightbulb_2_24px"
android:layout_marginEnd="4dp"/>
<TextView
android:id="@+id/tv_covers_hint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/covers_hint_long_press"
android:textColor="@color/brand_primary"
android:textStyle="bold"
android:textSize="12sp"/>
</LinearLayout>
<!-- Download button below covers, centered with COVERS text -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_download"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="COVERS"
android:contentDescription="Download Covers"
app:icon="@drawable/download_24px"
app:iconTint="@color/brand_primary"
app:iconGravity="textStart"
app:iconPadding="20dp"
app:iconSize="34dp" />
</LinearLayout>
+71 -16
View File
@@ -24,7 +24,7 @@
android:background="@android:color/transparent"
android:contentDescription="Home"
android:padding="4dp"
android:src="@drawable/ic_home"
android:src="@drawable/home_24px"
app:tint="#883DA4" />
<View
@@ -33,37 +33,92 @@
android:layout_weight="1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_download"
android:id="@+id/btn_search"
style="@style/PSX2.ElevatedTransparentButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Download Covers"
android:text=""
android:contentDescription="Search"
android:textColor="#883DA4"
app:icon="@drawable/download_24px"
app:icon="@drawable/search_24px"
app:iconTint="#883DA4"
app:iconGravity="textStart"
app:iconPadding="8dp"
app:iconSize="24dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_sort"
style="@style/PSX2.ElevatedTransparentButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:contentDescription="Sort"
android:textColor="#883DA4"
app:icon="@drawable/sort_24px"
app:iconTint="#883DA4"
app:iconGravity="textStart"
app:iconPadding="8dp"
app:iconSize="24dp" />
</LinearLayout>
<TextView
android:id="@+id/tv_covers_hint"
<!-- Letters row above covers -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_letters"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="4dp"
android:text="@string/covers_hint_long_press"
android:textColor="@color/brand_primary"
android:textStyle="bold"
android:drawableStart="@drawable/ic_info"
android:drawablePadding="8dp"/>
android:layout_height="56dp"
android:layout_marginTop="0dp"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:clipToPadding="false"
android:overScrollMode="never"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_covers"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:padding="12dp"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="6dp"
android:paddingBottom="0dp"
android:clipToPadding="false" />
<!-- Hint below covers grid: yellow bulb right next to text -->
<LinearLayout
android:id="@+id/hint_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<ImageView
android:id="@+id/img_hint_bulb"
android:layout_width="24dp"
android:layout_height="24dp"
android:tint="@color/covers_hint_yellow"
android:src="@drawable/lightbulb_2_24px"
android:layout_marginEnd="4dp"/>
<TextView
android:id="@+id/tv_covers_hint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/covers_hint_long_press"
android:textColor="@color/brand_primary"
android:textStyle="bold"
android:textSize="12sp"/>
</LinearLayout>
<!-- Download button below covers, centered with COVERS text -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_download"
style="@style/PSX2.ElevatedTransparentButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="COVERS"
android:contentDescription="Download Covers"
app:icon="@drawable/download_24px"
app:iconTint="#883DA4"
app:iconGravity="textStart"
app:iconPadding="20dp"
app:iconSize="34dp" />
</LinearLayout>
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="72dp"
android:layout_height="52dp"
android:padding="4dp">
<TextView
android:id="@+id/text_letter"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:textSize="20sp"
android:textColor="@color/brand_primary"
android:text="A"/>
</FrameLayout>
+1
View File
@@ -140,4 +140,5 @@
<color name="md_theme_surfaceContainer_highContrast">#DFE2EA</color>
<color name="md_theme_surfaceContainerHigh_highContrast">#D1D4DC</color>
<color name="md_theme_surfaceContainerHighest_highContrast">#C3C6CE</color>
<color name="covers_hint_yellow">#FFEB3B</color>
</resources>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Ensure these IDs exist even if not present in some layout variants -->
<item type="id" name="recycler_letters" />
</resources>