diff --git a/_tmp_lines.txt b/_tmp_lines.txt deleted file mode 100644 index 9b900ce..0000000 --- a/_tmp_lines.txt +++ /dev/null @@ -1,422 +0,0 @@ - 0: package com.izzy2lost.psx2; - 1: - 2: import android.app.Dialog; - 3: import android.content.Context; - 4: import android.net.Uri; - 5: import android.content.SharedPreferences; - 6: import android.os.Bundle; - 7: import android.view.LayoutInflater; - 8: import android.view.View; - 9: import android.view.ViewGroup; - 10: import android.view.Window; - 11: import android.view.WindowManager; - 12: import android.widget.Toast; - 13: import android.os.Build; - 14: import android.view.WindowInsets; - 15: import android.view.WindowInsetsController; - 16: - 17: import androidx.annotation.NonNull; - 18: import androidx.annotation.Nullable; - 19: import androidx.appcompat.app.AlertDialog; - 20: // Import MaterialAlertDialogBuilder for Material 3 styling - 21: import com.google.android.material.dialog.MaterialAlertDialogBuilder; - 22: import androidx.fragment.app.DialogFragment; - 23: import androidx.recyclerview.widget.GridLayoutManager; - 24: import androidx.recyclerview.widget.RecyclerView; - 25: import java.util.Locale; - 26: - 27: public class GamesCoverDialogFragment extends DialogFragment { - 28: private CoversAdapter adapter; - 29: private String[] titles; - 30: private String[] uris; - 31: private String[] coverUrls; - 32: private String[] localPaths; - 33: private RecyclerView rv; - 34: private GridLayoutManager glm; - 35: - 36: public interface OnGameSelectedListener { - 37: void onGameSelected(String gameUri); - 38: } - 39: - 40: private static final String ARG_TITLES = "titles"; - 41: private static final String ARG_URIS = "uris"; - 42: - 43: public static GamesCoverDialogFragment newInstance(String[] titles, String[] uris) { - 44: GamesCoverDialogFragment f = new GamesCoverDialogFragment(); - 45: Bundle b = new Bundle(); - 46: b.putStringArray(ARG_TITLES, titles); - 47: b.putStringArray(ARG_URIS, uris); - 48: f.setArguments(b); - 49: return f; - 50: } - 51: - 52: private OnGameSelectedListener listener; - 53: - 54: @Override - 55: public void onAttach(@NonNull Context context) { - 56: super.onAttach(context); - 57: if (context instanceof OnGameSelectedListener) { - 58: listener = (OnGameSelectedListener) context; - 59: } - 60: } - 61: - 62: @Override - 63: public void onResume() { - 64: super.onResume(); - 65: // Re-assert fixed span (2/4) after resume to avoid any flips - 66: if (rv != null && glm != null) { - 67: int currentOrientation = getResources().getConfiguration().orientation; - 68: int fixedSpan = (currentOrientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) ? 4 : 2; - 69: if (fixedSpan != glm.getSpanCount()) { - 70: glm.setSpanCount(fixedSpan); - 71: } - 72: } - 73: } - 74: - 75: @NonNull - 76: @Override - 77: public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { - 78: LayoutInflater inflater = getLayoutInflater(); - 79: View root = inflater.inflate(R.layout.dialog_covers_grid, null, false); - 80: - 81: rv = root.findViewById(R.id.recycler_covers); - 82: rv.setHasFixedSize(true); - 83: glm = new GridLayoutManager(requireContext(), 3); - 84: rv.setLayoutManager(glm); - 85: // spacing decoration (8dp) using half on each side so the gap between items is exactly spacingPx - 86: final int spacingPx = (int) (8 * getResources().getDisplayMetrics().density); - 87: final int half = Math.max(1, spacingPx / 2); - 88: rv.addItemDecoration(new RecyclerView.ItemDecoration() { - 89: @Override - 90: public void getItemOffsets(android.graphics.Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { - 91: outRect.set(half, half, half, half); - 92: } - 93: }); - 94: // Hard lock spans based on orientation only - 95: rv.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { - 96: int currentOrientation = getResources().getConfiguration().orientation; - 97: int fixedSpan = (currentOrientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) ? 4 : 2; - 98: if (fixedSpan != glm.getSpanCount()) { glm.setSpanCount(fixedSpan); } - 99: }); - 100: // Set initial fixed span as soon as possible - 101: root.post(() -> { - 102: int currentOrientation = getResources().getConfiguration().orientation; - 103: int fixedSpan = (currentOrientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) ? 4 : 2; - 104: if (fixedSpan != glm.getSpanCount()) { glm.setSpanCount(fixedSpan); } - 105: }); - 106: - 107: titles = getArguments() != null ? getArguments().getStringArray(ARG_TITLES) : new String[0]; - 108: uris = getArguments() != null ? getArguments().getStringArray(ARG_URIS) : new String[0]; - 109: coverUrls = new String[uris.length]; - 110: localPaths = new String[uris.length]; - 111: SharedPreferences prefs = requireContext().getSharedPreferences("app_prefs", Context.MODE_PRIVATE); - 112: for (int i = 0; i < uris.length; i++) { - 113: String saved = prefs.getString("serial:" + uris[i], null); - 114: String serial = saved; - 115: if (serial == null || serial.isEmpty()) { - 116: // Ask native core for the real serial (supports ISO/CHD and content://) - 117: try { - 118: String nativeSerial = NativeApp.getGameSerial(uris[i]); - 119: if (nativeSerial != null && !nativeSerial.isEmpty()) { - 120: serial = normalizeSerial(nativeSerial); - 121: prefs.edit().putString("serial:" + uris[i], serial).apply(); - 122: } - 123: } catch (Throwable ignored) {} - 124: } - 125: if (serial == null || serial.isEmpty()) { - 126: // Heuristic fallback from filename - 127: serial = buildSerialFromUri(uris[i]); - 128: } - 129: coverUrls[i] = buildCoverUrlFromSerial(serial); - 130: localPaths[i] = new java.io.File(getCoversDir(), serial + ".png").getAbsolutePath(); - 131: } - 132: - 133: adapter = new CoversAdapter(requireContext(), titles, coverUrls, localPaths, - 134: position -> { - 135: // Regular click - start game - 136: if (listener != null && position >= 0 && position < uris.length) { - 137: listener.onGameSelected(uris[position]); - 138: dismissAllowingStateLoss(); - 139: } - 140: }, - 141: position -> { - 142: // Long click - show game settings - 143: if (position >= 0 && position < uris.length) { - 144: showGameSettings(titles[position], uris[position]); - 145: } - 146: }); - 147: rv.setAdapter(adapter); - 148: - 149: // Toolbar buttons - 150: View btnHome = root.findViewById(R.id.btn_home); - 151: if (btnHome != null) btnHome.setOnClickListener(v -> dismissAllowingStateLoss()); - 152: View btnDownload = root.findViewById(R.id.btn_download); - 153: if (btnDownload != null) btnDownload.setOnClickListener(v -> startDownloadCovers()); - 154: - 155: AlertDialog dialog = new MaterialAlertDialogBuilder(requireContext(), - 156: com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog) - 157: .setView(root) - 158: .create(); - 159: return dialog; - 160: } - 161: - 162: @Override - 163: public void onStart() { - 164: super.onStart(); - 165: Dialog d = getDialog(); - 166: if (d == null) return; - 167: Window w = d.getWindow(); - 168: if (w == null) return; - 169: // Make dialog fullscreen - 170: w.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); - 171: // Enter immersive fullscreen to avoid showing system navigation buttons - 172: if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { - 173: w.setDecorFitsSystemWindows(false); - 174: WindowInsetsController c = w.getInsetsController(); - 175: if (c != null) { - 176: c.hide(WindowInsets.Type.systemBars()); - 177: c.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); - 178: } - 179: } else { - 180: View decor = w.getDecorView(); - 181: int flags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE - 182: | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - 183: | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - 184: | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - 185: | View.SYSTEM_UI_FLAG_FULLSCREEN - 186: | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; - 187: decor.setSystemUiVisibility(flags); - 188: w.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - 189: } - 190: } - 191: - 192: @Override - 193: public void onStart() { - 194: super.onStart(); - 195: Dialog d = getDialog(); - 196: if (d != null) { - 197: Window w = d.getWindow(); - 198: if (w != null) { - 199: w.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); - 200: w.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); - 201: // Hide status bar for true full-screen dialog - 202: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - 203: w.setDecorFitsSystemWindows(false); - 204: WindowInsetsController controller = w.getInsetsController(); - 205: if (controller != null) { - 206: controller.hide(WindowInsets.Type.statusBars()); - 207: controller.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); - 208: } - 209: } else { - 210: w.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - 211: } - 212: } - 213: } - 214: } - 215: - 216: private int calculateSpanForWidth(int rvWidthPx, int itemDp, int spacingPx) { - 217: float density = getResources().getDisplayMetrics().density; - 218: int usable = Math.max(0, rvWidthPx); - 219: int itemPx = (int) (itemDp * density); - 220: // Include spacing in the packing calculation to avoid oscillation - 221: // span = floor((usable + spacing) / (itemPx + spacing)) - 222: int span = (itemPx > 0) ? (int) Math.floor((usable + (double) spacingPx) / (itemPx + (double) spacingPx)) : 1; - 223: return Math.max(2, Math.max(1, span)); - 224: } - 225: - 226: private void preloadCovers(String[] urls) { - 227: // Use Glide to warm cache - 228: for (String url : urls) { - 229: if (url == null) continue; - 230: com.bumptech.glide.Glide.with(requireContext()).load(url).preload(); - 231: } - 232: } - 233: - 234: private void startDownloadCovers() { - 235: Toast.makeText(requireContext(), "Downloading covers in background", Toast.LENGTH_SHORT).show(); - 236: new Thread(() -> { - 237: // Try to refine serials/URLs by scanning disc contents first - 238: SharedPreferences prefs = requireContext().getSharedPreferences("app_prefs", Context.MODE_PRIVATE); - 239: SharedPreferences.Editor editor = prefs.edit(); - 240: for (int i = 0; i < uris.length; i++) { - 241: try { - 242: // Prefer native serial extraction so CHDs work - 243: String better = null; - 244: try { better = NativeApp.getGameSerial(uris[i]); } catch (Throwable ignored) {} - 245: if (better == null) better = extractSerialFromUri(uris[i]); - 246: if (better != null && !better.equalsIgnoreCase(serialFromUrl(coverUrls[i]))) { - 247: String serial = normalizeSerial(better); - 248: coverUrls[i] = buildCoverUrlFromSerial(serial); - 249: localPaths[i] = new java.io.File(getCoversDir(), serial + ".png").getAbsolutePath(); - 250: editor.putString("serial:" + uris[i], serial); - 251: } - 252: } catch (Exception ignored) { } - 253: } - 254: editor.apply(); - 255: - 256: int total = coverUrls.length; - 257: int ok = 0; - 258: java.io.File dir = getCoversDir(); - 259: if (!dir.exists()) dir.mkdirs(); - 260: for (int i = 0; i < total; i++) { - 261: String url = coverUrls[i]; - 262: String outPath = localPaths[i]; - 263: if (isFileValid(outPath)) { ok++; continue; } - 264: try { - 265: if (downloadToFile(url, outPath)) ok++; - 266: } catch (Exception ignored) { } - 267: } - 268: final int downloaded = ok; - 269: if (isAdded()) requireActivity().runOnUiThread(() -> { - 270: Toast.makeText(requireContext(), "Covers ready: " + downloaded + "/" + total, Toast.LENGTH_SHORT).show(); - 271: // refresh adapter to prefer local files now - 272: if (adapter != null) adapter.notifyDataSetChanged(); - 273: }); - 274: }).start(); - 275: } - 276: - 277: private static String serialFromUrl(String url) { - 278: if (url == null) return null; - 279: int slash = url.lastIndexOf('/'); - 280: int dot = url.lastIndexOf('.'); - 281: if (slash >= 0 && dot > slash) return url.substring(slash + 1, dot); - 282: return null; - 283: } - 284: - 285: private java.io.File getCoversDir() { - 286: java.io.File base = requireContext().getExternalFilesDir("covers"); - 287: if (base == null) base = new java.io.File(requireContext().getFilesDir(), "covers"); - 288: return base; - 289: } - 290: - 291: private static boolean isFileValid(String path) { - 292: if (path == null) return false; - 293: java.io.File f = new java.io.File(path); - 294: return f.exists() && f.length() > 0; - 295: } - 296: - 297: private static boolean downloadToFile(String urlStr, String outPath) throws Exception { - 298: java.net.URL url = new java.net.URL(urlStr); - 299: java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url.openConnection(); - 300: conn.setConnectTimeout(10000); - 301: conn.setReadTimeout(15000); - 302: conn.setInstanceFollowRedirects(true); - 303: conn.connect(); - 304: int code = conn.getResponseCode(); - 305: if (code != 200) { conn.disconnect(); return false; } - 306: java.io.File outFile = new java.io.File(outPath); - 307: java.io.File parent = outFile.getParentFile(); - 308: if (parent != null && !parent.exists()) parent.mkdirs(); - 309: java.io.InputStream in = conn.getInputStream(); - 310: java.io.FileOutputStream fos = new java.io.FileOutputStream(outFile); - 311: byte[] buf = new byte[8192]; - 312: int n; - 313: while ((n = in.read(buf)) != -1) fos.write(buf, 0, n); - 314: fos.flush(); - 315: fos.close(); - 316: in.close(); - 317: conn.disconnect(); - 318: return true; - 319: } - 320: - 321: private static String buildSerialFromUri(String gameUri) { - 322: // Try to infer PS2 serial from file name: e.g., SLUS-20312 or SLPS_123.45 style - 323: String last = Uri.parse(gameUri).getLastPathSegment(); - 324: if (last == null) last = ""; - 325: last = last.replace('_', '-'); - 326: // remove extension - 327: int dot = last.lastIndexOf('.'); - 328: if (dot > 0) last = last.substring(0, dot); - 329: String serial = null; - 330: // Very simple heuristic: find token like XXXX-XXXXX - 331: String upper = last.toUpperCase(Locale.ROOT); - 332: java.util.regex.Matcher m = java.util.regex.Pattern.compile("([A-Z]{4,5}-[0-9]{3,5})").matcher(upper); - 333: if (m.find()) { - 334: serial = m.group(1); - 335: } - 336: if (serial == null) { - 337: serial = upper; - 338: } - 339: return serial; - 340: } - 341: - 342: private static String buildCoverUrlFromSerial(String serial) { - 343: return "https://raw.githubusercontent.com/izzy2lost/ps2-covers/main/covers/3d/" + serial + ".png"; - 344: } - 345: - 346: private String extractSerialFromUri(String gameUri) { - 347: try { - 348: java.io.InputStream in = requireContext().getContentResolver().openInputStream(Uri.parse(gameUri)); - 349: if (in == null) return null; - 350: // Read first 8MB searching for SYSTEM.CNF contents, e.g., "BOOT2 = cdrom0:\\SLUS_203.12;1" - 351: final int MAX_BYTES = 8 * 1024 * 1024; - 352: final byte[] buf = new byte[64 * 1024]; - 353: int read; - 354: int total = 0; - 355: StringBuilder sb = new StringBuilder(); - 356: while ((read = in.read(buf)) != -1 && total < MAX_BYTES) { - 357: total += read; - 358: // append as ASCII - 359: sb.append(new String(buf, 0, read)); - 360: // try to match as we go to avoid huge strings - 361: String found = findSerialInString(sb); - 362: if (found != null) { in.close(); return found; } - 363: if (sb.length() > 512 * 1024) sb.delete(0, sb.length() - 128 * 1024); // keep window - 364: } - 365: in.close(); - 366: } catch (Exception ignored) { } - 367: return null; - 368: } - 369: - 370: private static String findSerialInString(CharSequence cs) { - 371: // Match common forms: SLUS_203.12, SLPM_650.51, SCES_123.45 etc. - 372: java.util.regex.Matcher m = java.util.regex.Pattern - 373: .compile("([A-Z]{4,5})[_-]([0-9]{3})\\.([0-9]{2})") - 374: .matcher(cs); - 375: if (m.find()) { - 376: String prefix = m.group(1); - 377: String part1 = m.group(2); - 378: String part2 = m.group(3); - 379: return prefix + "-" + part1 + part2; // SLUS-20312 - 380: } - 381: return null; - 382: } - 383: - 384: private static String normalizeSerial(String serial) { - 385: if (serial == null) return null; - 386: String s = serial.toUpperCase(Locale.ROOT).replace('_', '-'); - 387: // If form like XXXX-123.45 -> XXXX-12345 - 388: s = s.replaceAll("([A-Z]{4,5})-([0-9]{3})\\.([0-9]{2})", "$1-$2$3"); - 389: return s; - 390: } - 391: - 392: private void showGameSettings(String gameTitle, String gameUri) { - 393: // Prefer native extraction so CHDs work - 394: String gameSerial = null; - 395: try { gameSerial = NativeApp.getGameSerial(gameUri); } catch (Throwable ignored) {} - 396: if (gameSerial == null || gameSerial.isEmpty()) { - 397: gameSerial = extractSerialFromUri(gameUri); - 398: } - 399: if (gameSerial == null || gameSerial.isEmpty()) { - 400: gameSerial = buildSerialFromUri(gameUri); - 401: } - 402: gameSerial = normalizeSerial(gameSerial); - 403: - 404: // CRC (native if available) - 405: String gameCrc = null; - 406: try { gameCrc = NativeApp.getGameCrc(gameUri); } catch (Throwable ignored) {} - 407: if (gameCrc == null || gameCrc.isEmpty()) { - 408: gameCrc = String.format("%08X", Math.abs(gameUri.hashCode())); - 409: } - 410: - 411: // Debug logging - 412: android.util.Log.d("GameSettings", "Opening game settings for: " + gameTitle); - 413: android.util.Log.d("GameSettings", "URI: " + gameUri); - 414: android.util.Log.d("GameSettings", "Extracted Serial: " + gameSerial); - 415: android.util.Log.d("GameSettings", "Generated CRC: " + gameCrc); - 416: - 417: GameSettingsDialogFragment dialog = GameSettingsDialogFragment.newInstance( - 418: gameTitle, gameUri, gameSerial, gameCrc); - 419: dialog.show(getParentFragmentManager(), "game_settings"); - 420: } - 421: } diff --git a/app/src/main/java/com/izzy2lost/psx2/CoversAdapter.java b/app/src/main/java/com/izzy2lost/psx2/CoversAdapter.java index b3950e5..ecdd2fa 100644 --- a/app/src/main/java/com/izzy2lost/psx2/CoversAdapter.java +++ b/app/src/main/java/com/izzy2lost/psx2/CoversAdapter.java @@ -112,11 +112,21 @@ public class CoversAdapter extends RecyclerView.Adapter { } } 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; diff --git a/app/src/main/java/com/izzy2lost/psx2/GamesCoverDialogFragment.java b/app/src/main/java/com/izzy2lost/psx2/GamesCoverDialogFragment.java index f6f5c18..a810f1c 100644 --- a/app/src/main/java/com/izzy2lost/psx2/GamesCoverDialogFragment.java +++ b/app/src/main/java/com/izzy2lost/psx2/GamesCoverDialogFragment.java @@ -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 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 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 A–Z 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 ? "A–Z" : "RECENT"); + btn.setContentDescription(alpha ? "Sort A–Z" : "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 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; diff --git a/app/src/main/java/com/izzy2lost/psx2/LettersAdapter.java b/app/src/main/java/com/izzy2lost/psx2/LettersAdapter.java new file mode 100644 index 0000000..c8cbae2 --- /dev/null +++ b/app/src/main/java/com/izzy2lost/psx2/LettersAdapter.java @@ -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 { + public interface OnClick { + void onClick(char letter); + } + + private final List letters; + private final OnClick onClick; + + public LettersAdapter(List 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); + } + } +} diff --git a/app/src/main/java/com/izzy2lost/psx2/MainActivity.java b/app/src/main/java/com/izzy2lost/psx2/MainActivity.java index 4882eab..83bc0b1 100644 --- a/app/src/main/java/com/izzy2lost/psx2/MainActivity.java +++ b/app/src/main/java/com/izzy2lost/psx2/MainActivity.java @@ -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; diff --git a/app/src/main/res/drawable/home_24px.xml b/app/src/main/res/drawable/home_24px.xml new file mode 100644 index 0000000..16b0538 --- /dev/null +++ b/app/src/main/res/drawable/home_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/info_24px.xml b/app/src/main/res/drawable/info_24px.xml new file mode 100644 index 0000000..3186ebf --- /dev/null +++ b/app/src/main/res/drawable/info_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/lightbulb_2_24px.xml b/app/src/main/res/drawable/lightbulb_2_24px.xml new file mode 100644 index 0000000..c11422a --- /dev/null +++ b/app/src/main/res/drawable/lightbulb_2_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/search_24px.xml b/app/src/main/res/drawable/search_24px.xml new file mode 100644 index 0000000..390774b --- /dev/null +++ b/app/src/main/res/drawable/search_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/sort_24px.xml b/app/src/main/res/drawable/sort_24px.xml new file mode 100644 index 0000000..1f06ff7 --- /dev/null +++ b/app/src/main/res/drawable/sort_24px.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/layout-land/dialog_covers_grid.xml b/app/src/main/res/layout-land/dialog_covers_grid.xml index b195c11..944abbf 100644 --- a/app/src/main/res/layout-land/dialog_covers_grid.xml +++ b/app/src/main/res/layout-land/dialog_covers_grid.xml @@ -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" /> - + android:text="" + android:contentDescription="Search" + app:icon="@drawable/search_24px" + app:iconTint="@color/brand_primary" + app:iconGravity="textStart" + app:iconPadding="8dp" + app:iconSize="24dp" /> - + + + + android:layout_height="56dp" + android:layout_marginTop="0dp" + android:paddingStart="24dp" + android:paddingEnd="24dp" + android:clipToPadding="false" + android:overScrollMode="never"/> + + + + + + + + + diff --git a/app/src/main/res/layout-port/dialog_covers_grid.xml b/app/src/main/res/layout-port/dialog_covers_grid.xml index b195c11..59d3dab 100644 --- a/app/src/main/res/layout-port/dialog_covers_grid.xml +++ b/app/src/main/res/layout-port/dialog_covers_grid.xml @@ -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" /> - + android:text="" + android:contentDescription="Search" + app:icon="@drawable/search_24px" + app:iconTint="@color/brand_primary" + app:iconGravity="textStart" + app:iconPadding="8dp" + app:iconSize="24dp" /> - + + + + android:layout_height="56dp" + android:layout_marginTop="0dp" + android:paddingStart="24dp" + android:paddingEnd="24dp" + android:clipToPadding="false" + android:overScrollMode="never"/> + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_covers_grid.xml b/app/src/main/res/layout/dialog_covers_grid.xml index 8032cbb..ab81c0b 100644 --- a/app/src/main/res/layout/dialog_covers_grid.xml +++ b/app/src/main/res/layout/dialog_covers_grid.xml @@ -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" /> + + - + + android:layout_height="56dp" + android:layout_marginTop="0dp" + android:paddingStart="24dp" + android:paddingEnd="24dp" + android:clipToPadding="false" + android:overScrollMode="never"/> + + + + + + + + + diff --git a/app/src/main/res/layout/item_letter.xml b/app/src/main/res/layout/item_letter.xml new file mode 100644 index 0000000..2f7f7d2 --- /dev/null +++ b/app/src/main/res/layout/item_letter.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 430b364..1b1f7f0 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -140,4 +140,5 @@ #DFE2EA #D1D4DC #C3C6CE + #FFEB3B diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml new file mode 100644 index 0000000..7657784 --- /dev/null +++ b/app/src/main/res/values/ids.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7705927..dedd5d1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/graduated-gradient-rainbow-vinyl-vertical-black-to-blue-260-10.jpg b/graduated-gradient-rainbow-vinyl-vertical-black-to-blue-260-10.jpg deleted file mode 100644 index 5813c81..0000000 Binary files a/graduated-gradient-rainbow-vinyl-vertical-black-to-blue-260-10.jpg and /dev/null differ diff --git a/graduated-gradient-rainbow-vinyl-vertical-black-to-purple-263-10.jpg b/graduated-gradient-rainbow-vinyl-vertical-black-to-purple-263-10.jpg deleted file mode 100644 index 4c99892..0000000 Binary files a/graduated-gradient-rainbow-vinyl-vertical-black-to-purple-263-10.jpg and /dev/null differ