diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp index 3a1c615..98176b4 100644 --- a/app/src/main/cpp/native-lib.cpp +++ b/app/src/main/cpp/native-lib.cpp @@ -1211,6 +1211,12 @@ Java_com_izzy2lost_psx2_NativeApp_resume(JNIEnv *env, jclass clazz) { }).detach(); } +extern "C" +JNIEXPORT jboolean JNICALL +Java_com_izzy2lost_psx2_NativeApp_isPaused(JNIEnv *env, jclass clazz) { + return VMManager::GetState() == VMState::Paused; +} + extern "C" JNIEXPORT void JNICALL Java_com_izzy2lost_psx2_NativeApp_shutdown(JNIEnv *env, jclass clazz) { diff --git a/app/src/main/java/com/izzy2lost/psx2/GameSettingsDialogFragment.java b/app/src/main/java/com/izzy2lost/psx2/GameSettingsDialogFragment.java index a05ea40..312df2e 100644 --- a/app/src/main/java/com/izzy2lost/psx2/GameSettingsDialogFragment.java +++ b/app/src/main/java/com/izzy2lost/psx2/GameSettingsDialogFragment.java @@ -114,6 +114,13 @@ public class GameSettingsDialogFragment extends DialogFragment { serialView.setVisibility(View.GONE); } + // Aspect Ratio Spinner + Spinner spAspectRatio = view.findViewById(R.id.sp_aspect_ratio); + ArrayAdapter aspectAdapter = ArrayAdapter.createFromResource(ctx, + R.array.aspect_ratio_entries, android.R.layout.simple_spinner_item); + aspectAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spAspectRatio.setAdapter(aspectAdapter); + // Blending Accuracy Spinner Spinner spBlendingAccuracy = view.findViewById(R.id.sp_blending_accuracy); ArrayAdapter blendingAdapter = ArrayAdapter.createFromResource(ctx, @@ -147,6 +154,7 @@ public class GameSettingsDialogFragment extends DialogFragment { android.content.SharedPreferences gp = ctx.getSharedPreferences("app_prefs", Context.MODE_PRIVATE); int gRenderer = gp.getInt("renderer", -1); float gScale = gp.getFloat("upscale_multiplier", 1.0f); + int gAspect = gp.getInt("aspect_ratio", 1); int gBlend = gp.getInt("blending_accuracy", 1); boolean gWide = gp.getBoolean("widescreen_patches", true); boolean gNoInt = gp.getBoolean("no_interlacing_patches", true); @@ -157,6 +165,7 @@ public class GameSettingsDialogFragment extends DialogFragment { // Map to indices int defaultRendererIdx = (gRenderer == -1 ? 0 : (gRenderer == 14 ? 1 : (gRenderer == 12 ? 2 : 3))); int defaultScaleIdx = Math.max(0, Math.min(7, Math.round(gScale) - 1)); + spAspectRatio.setSelection(Math.max(0, Math.min(4, gAspect))); spRenderer.setSelection(defaultRendererIdx); spResolution.setSelection(defaultScaleIdx); spBlendingAccuracy.setSelection(Math.max(0, Math.min(5, gBlend))); @@ -206,6 +215,22 @@ public class GameSettingsDialogFragment extends DialogFragment { if (m.find()) { try { float mult = Float.parseFloat(m.group(1)); int sel = Math.max(0, Math.min(7, Math.round(mult - 1))); spResolution.setSelection(sel); } catch (Exception ignored) {} } + m = java.util.regex.Pattern.compile("(?m)^AspectRatio=\\s*(.+)$").matcher(content); + if (m.find()) { + String av = m.group(1).trim(); + int idx = 1; // default Auto 4:3/3:2 + try { + int num = Integer.parseInt(av); + if (num >= 0 && num <= 4) idx = num; + } catch (Exception e) { + if ("Stretch".equalsIgnoreCase(av)) idx = 0; + else if ("Auto".equalsIgnoreCase(av)) idx = 1; + else if ("4:3".equalsIgnoreCase(av)) idx = 2; + else if ("16:9".equalsIgnoreCase(av)) idx = 3; + else if ("10:7".equalsIgnoreCase(av)) idx = 4; + } + spAspectRatio.setSelection(idx); + } m = java.util.regex.Pattern.compile("(?m)^accurate_blending_unit=\\s*(.+)$").matcher(content); if (m.find()) { String bv = m.group(1).trim(); @@ -255,6 +280,7 @@ public class GameSettingsDialogFragment extends DialogFragment { } } catch (Throwable ignored) { // Fallback to defaults if loading fails + spAspectRatio.setSelection(1); spBlendingAccuracy.setSelection(1); // Mirror global default when error android.content.SharedPreferences prefs = ctx.getSharedPreferences("app_prefs", Context.MODE_PRIVATE); @@ -276,6 +302,7 @@ public class GameSettingsDialogFragment extends DialogFragment { .setView(view) .setNegativeButton("Cancel", (d, w) -> d.dismiss()) .setPositiveButton("Save", (d, w) -> { + final int aspectIdx = spAspectRatio.getSelectedItemPosition(); final int blendLevel = spBlendingAccuracy.getSelectedItemPosition(); final int rendererIdx = spRenderer.getSelectedItemPosition(); final int resIdx = spResolution.getSelectedItemPosition(); @@ -288,7 +315,7 @@ public class GameSettingsDialogFragment extends DialogFragment { // Persist per-game INI explicitly (supports Auto as well) writeGameSettingsIni(ctx, gameSerial, gameCrc, - blendLevel, rendererIdx, resIdx, wide, noInt, + aspectIdx, blendLevel, rendererIdx, resIdx, wide, noInt, /*enablePatches*/ enablePatches, enableCheats); // Live-apply per-game settings in one batch @@ -301,6 +328,7 @@ public class GameSettingsDialogFragment extends DialogFragment { else renderer = 13; float scale = Math.max(1, Math.min(8, resIdx + 1)); + NativeApp.setAspectRatio(aspectIdx); NativeApp.setLoadTextures(loadTex); NativeApp.setAsyncTextureLoading(asyncTex); NativeApp.applyPerGameSettingsBatch(renderer, scale, blendLevel, wide, noInt, enablePatches, enableCheats); @@ -385,6 +413,7 @@ public class GameSettingsDialogFragment extends DialogFragment { private static void writeGameSettingsIni(Context ctx, String gameSerial, String gameCrc, + int aspectRatioIdx, int blendingAccuracyIdx, int rendererIdx, int resolutionIdx, @@ -403,11 +432,13 @@ public class GameSettingsDialogFragment extends DialogFragment { String rendererName = (rendererIdx == 0) ? "Auto" : (rendererIdx == 1 ? "Vulkan" : (rendererIdx == 2 ? "OpenGL" : "Software")); float upscale = Math.max(1, Math.min(8, resolutionIdx + 1)); int abl = Math.max(0, Math.min(5, blendingAccuracyIdx)); + int aspectRatio = Math.max(0, Math.min(4, aspectRatioIdx)); StringBuilder sb = new StringBuilder(); sb.append("[EmuCore/GS]\n"); sb.append("Renderer=").append(rendererName).append('\n'); sb.append("upscale_multiplier=").append((int) upscale).append('\n'); + sb.append("AspectRatio=").append(aspectRatio).append('\n'); sb.append("accurate_blending_unit=").append(abl).append('\n'); sb.append('\n'); sb.append("[EmuCore]\n"); diff --git a/app/src/main/java/com/izzy2lost/psx2/GamesCoverDialogFragment.java b/app/src/main/java/com/izzy2lost/psx2/GamesCoverDialogFragment.java index ae2d541..a174316 100644 --- a/app/src/main/java/com/izzy2lost/psx2/GamesCoverDialogFragment.java +++ b/app/src/main/java/com/izzy2lost/psx2/GamesCoverDialogFragment.java @@ -33,6 +33,7 @@ import java.util.Locale; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; +import java.io.File; public class GamesCoverDialogFragment extends DialogFragment { private boolean didInitialNudge = false; @@ -56,24 +57,13 @@ public class GamesCoverDialogFragment extends DialogFragment { 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); @@ -104,6 +94,8 @@ public class GamesCoverDialogFragment extends DialogFragment { @Override public void onResume() { super.onResume(); + // Keep dialog immersive like the rest of the app + forceDialogImmersive(); if (rv != null) applyCoverflowTransforms(rv); } @@ -111,13 +103,18 @@ public class GamesCoverDialogFragment extends DialogFragment { @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { // Return a styled dialog; content is provided by onCreateView - return new Dialog(requireContext(), R.style.PSX2_FullScreenDialog); + Dialog d = new Dialog(requireContext(), R.style.PSX2_FullScreenDialog); + // Ensure immersive as soon as window exists + try { applyImmersiveToWindow(d.getWindow()); } catch (Throwable ignored) {} + return d; } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View root = inflater.inflate(R.layout.dialog_covers_grid, container, false); + // Post to re-assert immersive after layout + try { root.post(this::forceDialogImmersive); } catch (Throwable ignored) {} rv = root.findViewById(R.id.recycler_covers); rv.setHasFixedSize(true); @@ -130,56 +127,6 @@ 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); - // Snap to center item in the letters row - lettersSnapHelper = new PagerSnapHelper(); - lettersSnapHelper.attachToRecyclerView(rvLetters); - rvLetters.setClipToPadding(false); - int basePad = (int)(24*getResources().getDisplayMetrics().density); - rvLetters.setPadding(basePad, 0, basePad, 0); - // 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 @@ -259,7 +206,6 @@ 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); @@ -329,7 +275,6 @@ public class GamesCoverDialogFragment extends DialogFragment { applyFilterAndSort(); } else { adapter.notifyDataSetChanged(); - if (rvLetters != null) buildLettersAndBind(); } }); }).start(); @@ -379,6 +324,8 @@ public class GamesCoverDialogFragment extends DialogFragment { View btnHome = root.findViewById(R.id.btn_home); if (btnHome != null) btnHome.setOnClickListener(v -> { try { + // Refresh drawer settings before opening + refreshDialogDrawerSettings(); androidx.drawerlayout.widget.DrawerLayout drawer = root.findViewById(R.id.dlg_drawer_layout); if (drawer != null) drawer.openDrawer(GravityCompat.START); } catch (Throwable ignored) {} @@ -439,12 +386,27 @@ public class GamesCoverDialogFragment extends DialogFragment { } catch (Throwable ignored) {} }); } + View btnGames = header.findViewById(R.id.drawer_btn_games); + if (btnGames != null) { + btnGames.setOnClickListener(v -> { + try { + // Close drawer first + androidx.drawerlayout.widget.DrawerLayout drawer = root.findViewById(R.id.dlg_drawer_layout); + if (drawer != null) drawer.closeDrawer(androidx.core.view.GravityCompat.START); + // Since we're already in the games dialog, just close this drawer + // The user is already viewing games, so no need to open another dialog + } catch (Throwable ignored) {} + }); + } View btnGameState = header.findViewById(R.id.drawer_btn_game_state); if (btnGameState != null) { btnGameState.setOnClickListener(v -> { try { new SavesDialogFragment().show(getParentFragmentManager(), "saves_dialog"); } catch (Throwable ignored) {} }); } + + // Setup drawer settings controls to mirror quick actions + setupDialogDrawerSettings(header); } } } catch (Throwable ignored) {} @@ -454,55 +416,208 @@ public class GamesCoverDialogFragment extends DialogFragment { return root; } + @Override + public void onStart() { + super.onStart(); + // Re-assert immersive when dialog shows + forceDialogImmersive(); + } + + private void forceDialogImmersive() { + try { + Dialog dlg = getDialog(); + if (dlg != null) applyImmersiveToWindow(dlg.getWindow()); + } catch (Throwable ignored) {} + } + + private void applyImmersiveToWindow(@Nullable Window w) { + if (w == null) return; + // Match activity behavior: full-screen and transient bars by swipe + w.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + + final int legacyFlags = View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + w.setDecorFitsSystemWindows(false); + WindowInsetsController controller = w.getInsetsController(); + if (controller != null) { + controller.hide(WindowInsets.Type.systemBars()); + controller.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + } + } + View decor = w.getDecorView(); + if (decor != null) { + decor.setSystemUiVisibility(legacyFlags); + decor.setOnSystemUiVisibilityChangeListener(vis -> { + if ((vis & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { + decor.setSystemUiVisibility(legacyFlags); + } + }); + } + } + // Public API to open the in-dialog navigation drawer from other components public void openDialogDrawer() { try { View root = getView(); if (root == null) return; + // Refresh drawer settings before opening + refreshDialogDrawerSettings(); androidx.drawerlayout.widget.DrawerLayout drawer = root.findViewById(R.id.dlg_drawer_layout); if (drawer != null) drawer.openDrawer(androidx.core.view.GravityCompat.START); } catch (Throwable ignored) {} } - 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; + private void setupDialogDrawerSettings(View header) { + if (header == null) return; + android.content.SharedPreferences prefs = requireContext().getSharedPreferences("app_prefs", Context.MODE_PRIVATE); + + // Aspect Ratio spinner + android.widget.Spinner spAspect = header.findViewById(R.id.drawer_sp_aspect_ratio); + if (spAspect != null) { + if (spAspect.getAdapter() == null) { + android.widget.ArrayAdapter aspectAdapter = android.widget.ArrayAdapter.createFromResource(requireContext(), + R.array.aspect_ratio_entries, android.R.layout.simple_spinner_item); + aspectAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spAspect.setAdapter(aspectAdapter); + spAspect.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() { + @Override public void onItemSelected(android.widget.AdapterView parent, View view, int position, long id) { + prefs.edit().putInt("aspect_ratio", position).apply(); + try { NativeApp.setAspectRatio(position); } catch (Throwable ignored) {} + } + @Override public void onNothingSelected(android.widget.AdapterView parent) {} + }); + } + android.widget.ArrayAdapter adapter = (android.widget.ArrayAdapter) spAspect.getAdapter(); + if (adapter != null) { + int savedAspect = prefs.getInt("aspect_ratio", 1); + if (savedAspect < 0 || savedAspect >= adapter.getCount()) savedAspect = 1; + spAspect.setSelection(savedAspect); } } - 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); + + // Blending Accuracy spinner + android.widget.Spinner spBlending = header.findViewById(R.id.drawer_sp_blending_accuracy); + if (spBlending != null) { + if (spBlending.getAdapter() == null) { + android.widget.ArrayAdapter blendAdapter = android.widget.ArrayAdapter.createFromResource(requireContext(), + R.array.blending_accuracy_entries, android.R.layout.simple_spinner_item); + blendAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spBlending.setAdapter(blendAdapter); + spBlending.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() { + @Override public void onItemSelected(android.widget.AdapterView parent, View view, int position, long id) { + prefs.edit().putInt("blending_accuracy", position).apply(); + try { NativeApp.setBlendingAccuracy(position); } catch (Throwable ignored) {} + } + @Override public void onNothingSelected(android.widget.AdapterView parent) {} + }); } - rvLetters.post(() -> { resnapLetters(); applyLetterTransforms(rvLetters); }); + android.widget.ArrayAdapter adapter = (android.widget.ArrayAdapter) spBlending.getAdapter(); + if (adapter != null) { + int savedBlend = prefs.getInt("blending_accuracy", 1); + if (savedBlend < 0 || savedBlend >= adapter.getCount()) savedBlend = 1; + spBlending.setSelection(savedBlend); + } + } + + // Resolution Scale spinner + android.widget.Spinner spScale = header.findViewById(R.id.drawer_sp_scale); + if (spScale != null) { + if (spScale.getAdapter() == null) { + android.widget.ArrayAdapter scaleAdapter = android.widget.ArrayAdapter.createFromResource(requireContext(), + R.array.scale_entries, android.R.layout.simple_spinner_item); + scaleAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spScale.setAdapter(scaleAdapter); + spScale.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() { + @Override public void onItemSelected(android.widget.AdapterView parent, View view, int position, long id) { + float scale = Math.max(1, Math.min(8, position + 1)); + prefs.edit().putFloat("upscale_multiplier", scale).apply(); + try { NativeApp.renderUpscalemultiplier(scale); } catch (Throwable ignored) {} + } + @Override public void onNothingSelected(android.widget.AdapterView parent) {} + }); + } + android.widget.ArrayAdapter adapter = (android.widget.ArrayAdapter) spScale.getAdapter(); + if (adapter != null) { + float savedScale = prefs.getFloat("upscale_multiplier", 1.0f); + int scaleIndex = Math.max(0, Math.min(adapter.getCount() - 1, Math.round(savedScale) - 1)); + spScale.setSelection(scaleIndex); + } + } + + // Setup switch listeners only once + setupDialogDrawerSwitchListeners(header, prefs); + }private void setupDialogDrawerSwitchListeners(View header, android.content.SharedPreferences prefs) { + // Widescreen Patches switch + com.google.android.material.materialswitch.MaterialSwitch swWide = header.findViewById(R.id.drawer_sw_widescreen); + if (swWide != null && swWide.getTag() == null) { + swWide.setTag("setup"); // Mark as setup to avoid duplicate listeners + swWide.setOnCheckedChangeListener((buttonView, isChecked) -> { + prefs.edit().putBoolean("widescreen_patches", isChecked).apply(); + try { NativeApp.setWidescreenPatches(isChecked); } catch (Throwable ignored) {} + }); + } + + // No Interlacing switch + com.google.android.material.materialswitch.MaterialSwitch swNoInt = header.findViewById(R.id.drawer_sw_no_interlacing); + if (swNoInt != null && swNoInt.getTag() == null) { + swNoInt.setTag("setup"); + swNoInt.setOnCheckedChangeListener((buttonView, isChecked) -> { + prefs.edit().putBoolean("no_interlacing_patches", isChecked).apply(); + try { NativeApp.setNoInterlacingPatches(isChecked); } catch (Throwable ignored) {} + }); + } + + // Load Textures switch + com.google.android.material.materialswitch.MaterialSwitch swLoadTex = header.findViewById(R.id.drawer_sw_load_textures); + if (swLoadTex != null && swLoadTex.getTag() == null) { + swLoadTex.setTag("setup"); + swLoadTex.setOnCheckedChangeListener((buttonView, isChecked) -> { + prefs.edit().putBoolean("load_textures", isChecked).apply(); + try { NativeApp.setLoadTextures(isChecked); } catch (Throwable ignored) {} + }); + } + + // Async Textures switch + com.google.android.material.materialswitch.MaterialSwitch swAsyncTex = header.findViewById(R.id.drawer_sw_async_textures); + if (swAsyncTex != null && swAsyncTex.getTag() == null) { + swAsyncTex.setTag("setup"); + swAsyncTex.setOnCheckedChangeListener((buttonView, isChecked) -> { + prefs.edit().putBoolean("async_texture_loading", isChecked).apply(); + try { NativeApp.setAsyncTextureLoading(isChecked); } catch (Throwable ignored) {} + }); + } + + // Precache Textures switch + com.google.android.material.materialswitch.MaterialSwitch swPrecache = header.findViewById(R.id.drawer_sw_precache_textures); + if (swPrecache != null && swPrecache.getTag() == null) { + swPrecache.setTag("setup"); + swPrecache.setOnCheckedChangeListener((buttonView, isChecked) -> { + prefs.edit().putBoolean("precache_textures", isChecked).apply(); + try { NativeApp.setPrecacheTextureReplacements(isChecked); } catch (Throwable ignored) {} + }); } } - private void applyLetterTransforms(@NonNull RecyclerView recyclerView) { - int rvCenterX = (recyclerView.getLeft() + recyclerView.getRight()) / 2; + // --- Helper methods reintroduced after letters-row removal --- + private void applyCoverflowTransforms(@NonNull RecyclerView recyclerView) { + int width = recyclerView.getWidth(); + if (width <= 0) return; + int centerX = width / 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)); + int childCenter = (child.getLeft() + child.getRight()) / 2; + float dist = Math.abs(childCenter - centerX); + float norm = Math.min(1f, dist / (width * 0.5f)); float scale = maxScale - (maxScale - minScale) * norm; float alpha = maxAlpha - (maxAlpha - minAlpha) * norm; child.setScaleX(scale); @@ -511,156 +626,127 @@ public class GamesCoverDialogFragment extends DialogFragment { } } - 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); + private void resnapToCenter(@NonNull RecyclerView recyclerView) { + try { + if (snapHelper == null) return; + RecyclerView.LayoutManager lm = recyclerView.getLayoutManager(); + View snap = snapHelper.findSnapView(lm); + if (snap == null) return; + int[] dist = snapHelper.calculateDistanceToFinalSnap(lm, snap); + if (dist != null && (dist[0] != 0 || dist[1] != 0)) recyclerView.scrollBy(dist[0], dist[1]); + } catch (Throwable ignored) {} + } + + private static String normalizeSerial(String s) { + if (s == null) return ""; + String t = s.trim().toUpperCase(java.util.Locale.ROOT); + // Keep letters, digits and dash + return t.replaceAll("[^A-Z0-9-]", ""); + } + + private String buildSerialFromUri(String uriStr) { + try { + Uri u = Uri.parse(uriStr); + String last = u.getLastPathSegment(); + if (last == null) last = uriStr; + int dot = last.lastIndexOf('.') +; if (dot > 0) last = last.substring(0, dot); + return normalizeSerial(last); + } catch (Throwable ignored) { + return String.format(java.util.Locale.ROOT, "%08X", Math.abs(uriStr != null ? uriStr.hashCode() : 0)); + } + } + + private String extractSerialFromUri(String gameUri) { + try { + java.io.InputStream in = requireContext().getContentResolver().openInputStream(Uri.parse(gameUri)); + if (in == null) return null; + final int MAX_BYTES = 8 * 1024 * 1024; // scan first 8MB + final byte[] buf = new byte[64 * 1024]; + int read; + int total = 0; + StringBuilder sb = new StringBuilder(); + while ((read = in.read(buf)) != -1 && total < MAX_BYTES) { + total += read; + sb.append(new String(buf, 0, read)); + String found = findSerialInString(sb); + if (found != null) { in.close(); return found; } + if (sb.length() > 512 * 1024) sb.delete(0, sb.length() - 128 * 1024); } - } - 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); - } + in.close(); + } catch (Exception ignored) { } + return null; } - 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); + private static String findSerialInString(CharSequence cs) { + // Match common forms: SLUS_203.12, SLPM_650.51, SCES_123.45 etc. + java.util.regex.Matcher m = java.util.regex.Pattern + .compile("([A-Z]{4,5})[_-]([0-9]{3})\\.([0-9]{2})") + .matcher(cs); + if (m.find()) { + String prefix = m.group(1); + String part1 = m.group(2); + String part2 = m.group(3); + return prefix + "-" + part1 + part2; // SLUS-20312 } - // Center letters row on selected - if (rvLetters != null && letters != null && !letters.isEmpty()) { - int idx = letters.indexOf(letter); - if (idx >= 0) { - centerLettersOnIndex(idx); + return null; + } + + private String buildCoverUrlFromSerial(String serial) { + // Use repo-backed PS2 cover set (3D covers) + return "https://raw.githubusercontent.com/izzy2lost/ps2-covers/main/covers/3d/" + serial + ".png"; + } + + private File getCoversDir() { + File base = requireContext().getExternalFilesDir("covers"); + if (base == null) base = new File(requireContext().getFilesDir(), "covers"); + if (!base.exists()) base.mkdirs(); + return base; + } + + private void showGameSettings(String gameTitle, String gameUri) { + try { + // Prefer native extraction so CHDs work + String gameSerial = null; + try { gameSerial = NativeApp.getGameSerialSafe(gameUri); } catch (Throwable ignored) {} + if (gameSerial == null || gameSerial.isEmpty()) { + gameSerial = extractSerialFromUri(gameUri); } - } + if (gameSerial == null || gameSerial.isEmpty()) { + gameSerial = buildSerialFromUri(gameUri); + } + gameSerial = normalizeSerial(gameSerial); + + // CRC (native if available) + String gameCrc = null; + try { gameCrc = NativeApp.getGameCrc(gameUri); } catch (Throwable ignored) {} + if (gameCrc == null || gameCrc.isEmpty()) { + gameCrc = String.format(java.util.Locale.ROOT, "%08X", Math.abs(gameUri != null ? gameUri.hashCode() : 0)); + } + + GameSettingsDialogFragment dialog = GameSettingsDialogFragment.newInstance( + gameTitle, gameUri, gameSerial, gameCrc); + dialog.show(getParentFragmentManager(), "game_settings"); + } catch (Throwable ignored) {} } - 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) { + private void updateSortButtonUi(com.google.android.material.button.MaterialButton btn) { + if (btn == null) return; 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); + java.util.ArrayList idxs = new java.util.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); + if (t.toLowerCase(java.util.Locale.ROOT).contains(query.toLowerCase(java.util.Locale.ROOT))) idxs.add(i); } } SharedPreferences prefs = requireContext().getSharedPreferences("app_prefs", Context.MODE_PRIVATE); @@ -668,14 +754,26 @@ public class GamesCoverDialogFragment extends DialogFragment { 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]); + if (ta == tb) return (origTitles[a] != null ? origTitles[a] : "").compareToIgnoreCase(origTitles[b] != null ? origTitles[b] : ""); return Long.compare(tb, ta); }); } else { - idxs.sort(Comparator.comparing(i -> { + java.text.Normalizer.Form form = java.text.Normalizer.Form.NFD; + java.util.function.Function keyFn = (Integer i) -> { String t = origTitles[i]; - return t == null ? "" : t.toLowerCase(Locale.ROOT); - })); + if (t == null) return ""; + String s = t.trim().toLowerCase(java.util.Locale.ROOT); + // Drop leading articles commonly used in titles + if (s.startsWith("the ")) s = s.substring(4); + else if (s.startsWith("an ")) s = s.substring(3); + else if (s.startsWith("a ")) s = s.substring(2); + // Remove diacritics + s = java.text.Normalizer.normalize(s, form).replaceAll("\\p{M}+", ""); + // Strip non-alphanumeric at start + s = s.replaceFirst("^[^a-z0-9]+", ""); + return s; + }; + idxs.sort((a,b) -> keyFn.apply(a).compareTo(keyFn.apply(b))); } titles = new String[idxs.size()]; uris = new String[idxs.size()]; @@ -709,129 +807,36 @@ public class GamesCoverDialogFragment extends DialogFragment { 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; - final float maxScale = 1.0f; - final float minScale = landscape ? 0.70f : 0.82f; - final float maxAlpha = 1.0f; - final float minAlpha = landscape ? 0.50f : 0.60f; - final float maxTiltDeg = landscape ? 22f : 16f; - final float density = getResources().getDisplayMetrics().density; - final float maxParallax = (landscape ? 14f : 18f) * density; - for (int i = 0; i < recyclerView.getChildCount(); i++) { - View child = recyclerView.getChildAt(i); - int childCenterX = (child.getLeft() + child.getRight()) / 2; - float dx = childCenterX - rvCenterX; - float dist = Math.abs(dx); - float norm = Math.min(1f, dist / (recyclerView.getWidth() * 0.5f)); - float scale = maxScale - (maxScale - minScale) * norm; - float alpha = maxAlpha - (maxAlpha - minAlpha) * norm; - float tilt = Math.signum(dx) * maxTiltDeg * norm; // tilt away from center - child.setCameraDistance(8000f * density); - child.setScaleX(scale); - child.setScaleY(scale); - child.setAlpha(alpha); - child.setTranslationZ((1f - norm) * 10f); - child.setRotationY(tilt); - - // Title parallax (gentle) - View title = child.findViewById(R.id.text_title); - if (title != null) { - float parallax = Math.max(-maxParallax, Math.min(maxParallax, -dx / (recyclerView.getWidth() * 0.5f) * maxParallax)); - title.setTranslationX(parallax); - title.setAlpha(0.85f + 0.15f * (1f - norm)); - } - - // Shadow intensity scales with centeredness - View shadow = child.findViewById(R.id.view_shadow); - if (shadow != null) { - shadow.setAlpha((1f - norm) * 0.7f); - shadow.setScaleX(scale + 0.2f); - } - } - } - - private void resnapToCenter(@NonNull RecyclerView rv) { - if (snapHelper == null || llm == null) return; - View snapView = snapHelper.findSnapView(llm); - if (snapView == null) return; - int[] dist = snapHelper.calculateDistanceToFinalSnap(llm, snapView); - if (dist != null && (dist[0] != 0 || dist[1] != 0)) { - rv.scrollBy(dist[0], dist[1]); - } - } - - public void onStart() { - super.onStart(); - Dialog d = getDialog(); - if (d == null) return; - Window w = d.getWindow(); - if (w == null) return; - w.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - w.setDecorFitsSystemWindows(false); - WindowInsetsController controller = w.getInsetsController(); - if (controller != null) { - controller.hide(WindowInsets.Type.systemBars()); - controller.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); - } - } else { - w.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - View decor = w.getDecorView(); - int flags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_FULLSCREEN - | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; - decor.setSystemUiVisibility(flags); - } - } - - private int calculateSpanForWidth(int rvWidthPx, int itemDp, int spacingPx) { - float density = getResources().getDisplayMetrics().density; - int usable = Math.max(0, rvWidthPx); - int itemPx = (int) (itemDp * density); - // Include spacing in the packing calculation to avoid oscillation - // span = floor((usable + spacing) / (itemPx + spacing)) - int span = (itemPx > 0) ? (int) Math.floor((usable + (double) spacingPx) / (itemPx + (double) spacingPx)) : 1; - return Math.max(2, Math.max(1, span)); - } - - private void preloadCovers(String[] urls) { - // Use Glide to warm cache - for (String url : urls) { - if (url == null) continue; - com.bumptech.glide.Glide.with(requireContext()).load(url).preload(); - } + 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 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 startDownloadCovers() { Toast.makeText(requireContext(), "Downloading covers in background", Toast.LENGTH_SHORT).show(); new Thread(() -> { - // Try to refine serials/URLs by scanning disc contents first SharedPreferences prefs = requireContext().getSharedPreferences("app_prefs", Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); for (int i = 0; i < uris.length; i++) { try { - // Prefer native serial extraction so CHDs work String better = null; try { better = NativeApp.getGameSerialSafe(uris[i]); } catch (Throwable ignored) {} if (better == null) better = extractSerialFromUri(uris[i]); @@ -866,7 +871,6 @@ public class GamesCoverDialogFragment extends DialogFragment { final int downloaded = ok; if (isAdded()) requireActivity().runOnUiThread(() -> { Toast.makeText(requireContext(), "Covers ready: " + downloaded + "/" + total, Toast.LENGTH_SHORT).show(); - // refresh adapter to prefer local files now if (adapter != null) adapter.notifyDataSetChanged(); }); }).start(); @@ -880,12 +884,6 @@ public class GamesCoverDialogFragment extends DialogFragment { return null; } - private java.io.File getCoversDir() { - java.io.File base = requireContext().getExternalFilesDir("covers"); - if (base == null) base = new java.io.File(requireContext().getFilesDir(), "covers"); - return base; - } - private boolean isFileValid(String path) { if (path == null) return false; if (path.startsWith("content://")) { @@ -934,104 +932,46 @@ public class GamesCoverDialogFragment extends DialogFragment { return true; } - private static String buildSerialFromUri(String gameUri) { - // Try to infer PS2 serial from file name: e.g., SLUS-20312 or SLPS_123.45 style - String last = Uri.parse(gameUri).getLastPathSegment(); - if (last == null) last = ""; - last = last.replace('_', '-'); - // remove extension - int dot = last.lastIndexOf('.'); - if (dot > 0) last = last.substring(0, dot); - String serial = null; - // Very simple heuristic: find token like XXXX-XXXXX - String upper = last.toUpperCase(Locale.ROOT); - java.util.regex.Matcher m = java.util.regex.Pattern.compile("([A-Z]{4,5}-[0-9]{3,5})").matcher(upper); - if (m.find()) { - serial = m.group(1); - } - if (serial == null) { - serial = upper; - } - return serial; - } - - private static String buildCoverUrlFromSerial(String serial) { - return "https://raw.githubusercontent.com/izzy2lost/ps2-covers/main/covers/3d/" + serial + ".png"; - } - - private String extractSerialFromUri(String gameUri) { + private void refreshDialogDrawerSettings() { try { - java.io.InputStream in = requireContext().getContentResolver().openInputStream(Uri.parse(gameUri)); - if (in == null) return null; - // Read first 8MB searching for SYSTEM.CNF contents, e.g., "BOOT2 = cdrom0:\\SLUS_203.12;1" - final int MAX_BYTES = 8 * 1024 * 1024; - final byte[] buf = new byte[64 * 1024]; - int read; - int total = 0; - StringBuilder sb = new StringBuilder(); - while ((read = in.read(buf)) != -1 && total < MAX_BYTES) { - total += read; - // append as ASCII - sb.append(new String(buf, 0, read)); - // try to match as we go to avoid huge strings - String found = findSerialInString(sb); - if (found != null) { in.close(); return found; } - if (sb.length() > 512 * 1024) sb.delete(0, sb.length() - 128 * 1024); // keep window + View root = getView(); + if (root == null) return; + + com.google.android.material.navigation.NavigationView nav = root.findViewById(R.id.dialog_nav_view); + if (nav != null && nav.getHeaderCount() > 0) { + View header = nav.getHeaderView(0); + if (header != null) { + android.content.SharedPreferences prefs = requireContext().getSharedPreferences("app_prefs", Context.MODE_PRIVATE); + + // Refresh switch states only (spinners are set up once in setupDialogDrawerSettings) + com.google.android.material.materialswitch.MaterialSwitch swWide = header.findViewById(R.id.drawer_sw_widescreen); + if (swWide != null) { + swWide.setChecked(prefs.getBoolean("widescreen_patches", true)); + } + + com.google.android.material.materialswitch.MaterialSwitch swNoInt = header.findViewById(R.id.drawer_sw_no_interlacing); + if (swNoInt != null) { + swNoInt.setChecked(prefs.getBoolean("no_interlacing_patches", true)); + } + + com.google.android.material.materialswitch.MaterialSwitch swLoadTex = header.findViewById(R.id.drawer_sw_load_textures); + if (swLoadTex != null) { + swLoadTex.setChecked(prefs.getBoolean("load_textures", false)); + } + + com.google.android.material.materialswitch.MaterialSwitch swAsyncTex = header.findViewById(R.id.drawer_sw_async_textures); + if (swAsyncTex != null) { + swAsyncTex.setChecked(prefs.getBoolean("async_texture_loading", true)); + } + + com.google.android.material.materialswitch.MaterialSwitch swPrecache = header.findViewById(R.id.drawer_sw_precache_textures); + if (swPrecache != null) { + swPrecache.setChecked(prefs.getBoolean("precache_textures", false)); + } + } } - in.close(); - } catch (Exception ignored) { } - return null; - } - - private static String findSerialInString(CharSequence cs) { - // Match common forms: SLUS_203.12, SLPM_650.51, SCES_123.45 etc. - java.util.regex.Matcher m = java.util.regex.Pattern - .compile("([A-Z]{4,5})[_-]([0-9]{3})\\.([0-9]{2})") - .matcher(cs); - if (m.find()) { - String prefix = m.group(1); - String part1 = m.group(2); - String part2 = m.group(3); - return prefix + "-" + part1 + part2; // SLUS-20312 - } - return null; - } - - private static String normalizeSerial(String serial) { - if (serial == null) return null; - String s = serial.toUpperCase(Locale.ROOT).replace('_', '-'); - // If form like XXXX-123.45 -> XXXX-12345 - s = s.replaceAll("([A-Z]{4,5})-([0-9]{3})\\.([0-9]{2})", "$1-$2$3"); - return s; - } - - private void showGameSettings(String gameTitle, String gameUri) { - // Prefer native extraction so CHDs work - String gameSerial = null; - try { gameSerial = NativeApp.getGameSerialSafe(gameUri); } catch (Throwable ignored) {} - if (gameSerial == null || gameSerial.isEmpty()) { - gameSerial = extractSerialFromUri(gameUri); - } - if (gameSerial == null || gameSerial.isEmpty()) { - gameSerial = buildSerialFromUri(gameUri); - } - gameSerial = normalizeSerial(gameSerial); - - // CRC (native if available) - String gameCrc = null; - try { gameCrc = NativeApp.getGameCrc(gameUri); } catch (Throwable ignored) {} - if (gameCrc == null || gameCrc.isEmpty()) { - gameCrc = String.format("%08X", Math.abs(gameUri.hashCode())); - } - - // Debug logging - android.util.Log.d("GameSettings", "Opening game settings for: " + gameTitle); - android.util.Log.d("GameSettings", "URI: " + gameUri); - android.util.Log.d("GameSettings", "Extracted Serial: " + gameSerial); - android.util.Log.d("GameSettings", "Generated CRC: " + gameCrc); - - GameSettingsDialogFragment dialog = GameSettingsDialogFragment.newInstance( - gameTitle, gameUri, gameSerial, gameCrc); - dialog.show(getParentFragmentManager(), "game_settings"); + } catch (Throwable ignored) {} } } + + diff --git a/app/src/main/java/com/izzy2lost/psx2/LettersAdapter.java b/app/src/main/java/com/izzy2lost/psx2/LettersAdapter.java deleted file mode 100644 index c8cbae2..0000000 --- a/app/src/main/java/com/izzy2lost/psx2/LettersAdapter.java +++ /dev/null @@ -1,69 +0,0 @@ -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 f1e5180..18c1acc 100644 --- a/app/src/main/java/com/izzy2lost/psx2/MainActivity.java +++ b/app/src/main/java/com/izzy2lost/psx2/MainActivity.java @@ -18,6 +18,8 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.widget.FrameLayout; +import android.widget.ArrayAdapter; +import android.widget.Spinner; import android.view.ViewGroup; import android.content.res.ColorStateList; import android.graphics.Color; @@ -74,6 +76,7 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF private boolean joyRightPressed = false; private boolean controllerUiApplied = false; private AlertDialog mBiosPromptDialog = null; + private boolean mControllerHintShowing = false; private boolean isThread() { if (mEmulationThread != null) { @@ -223,6 +226,15 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; w.setAttributes(lp); } + + // Common flags for legacy immersive (works as belt-and-suspenders on newer API too) + final int legacyFlags = View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // Tell Window to lay out edge-to-edge and hide all system bars w.setDecorFitsSystemWindows(false); @@ -231,15 +243,22 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF controller.hide(WindowInsets.Type.systemBars()); controller.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); } + // Also apply legacy flags to decor; helps on 3-button nav devices + View decor = w.getDecorView(); + decor.setSystemUiVisibility(legacyFlags); + decor.setOnSystemUiVisibilityChangeListener(vis -> { + if ((vis & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { + decor.setSystemUiVisibility(legacyFlags); + } + }); } else { View decor = w.getDecorView(); - int flags = View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - | View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_FULLSCREEN - | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; - decor.setSystemUiVisibility(flags); + decor.setSystemUiVisibility(legacyFlags); + decor.setOnSystemUiVisibilityChangeListener(vis -> { + if ((vis & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { + decor.setSystemUiVisibility(legacyFlags); + } + }); } } @@ -314,7 +333,7 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF } private static final String[] GAME_EXTS = new String[]{ - ".iso", ".bin", ".img", ".mdf", ".nrg", ".chd" + ".iso", ".bin", ".img", ".mdf", ".nrg", ".chd", ".cso", ".zso", ".gz" }; private static boolean hasGameExt(String name) { @@ -483,11 +502,23 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF if (btn_settings != null) { btn_settings.setOnClickListener(v -> { try { + // Refresh drawer settings before opening + refreshDrawerSettings(); DrawerLayout drawer = findViewById(R.id.drawer_layout); if (drawer != null) drawer.openDrawer(androidx.core.view.GravityCompat.START); } catch (Throwable ignored) {} }); } + + // Pause/Play button toggles emulation pause state + MaterialButton btn_pause_play = findViewById(R.id.btn_pause_play); + if (btn_pause_play != null) { + btn_pause_play.setOnClickListener(v -> { + try { + togglePauseState(); + } catch (Throwable ignored) {} + }); + } // Wire left drawer header controls (title/power/reboot/renderer) try { NavigationView nav = findViewById(R.id.nav_view); @@ -542,6 +573,18 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF } catch (Throwable ignored) {} }); } + View btnGames = header.findViewById(R.id.drawer_btn_games); + if (btnGames != null) { + btnGames.setOnClickListener(v -> { + try { + // Close drawer first + DrawerLayout drawer = findViewById(R.id.drawer_layout); + if (drawer != null) drawer.closeDrawer(androidx.core.view.GravityCompat.START); + // Open games dialog + openGamesDialog(); + } catch (Throwable ignored) {} + }); + } if (btnGameState != null) { btnGameState.setOnClickListener(v -> { try { @@ -550,6 +593,9 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF } catch (Throwable ignored) {} }); } + + // Setup drawer settings controls to mirror quick actions + setupDrawerSettings(header); } } } catch (Throwable ignored) {} @@ -809,6 +855,11 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF // Hide on-screen touch controls when a physical controller is connected setControlsVisible(!hasController); + + // Show controller hint when no controller is connected (only once) + if (!hasController) { + showControllerHintIfNeeded(); + } } private int getCurrentRendererPref() { @@ -1333,7 +1384,7 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF SDLControllerManager.initialize(); // Load and apply saved settings - SettingsDialogFragment.loadAndApplySettings(this); + loadAndApplyStoredSettings(); mHIDDeviceManager = HIDDeviceManager.acquire(this); // Initialize HID device manager for USB and Bluetooth controllers @@ -1360,6 +1411,14 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF if(!isThread()) { mEmulationThread = new Thread(() -> NativeApp.runVMThread(m_szGamefile)); mEmulationThread.start(); + // Show pause button when game starts (game is always running initially) + runOnUiThread(() -> { + MaterialButton btn_pause_play = findViewById(R.id.btn_pause_play); + if (btn_pause_play != null) { + btn_pause_play.setVisibility(View.VISIBLE); + btn_pause_play.setIcon(ContextCompat.getDrawable(this, R.drawable.pause_circle_24px)); + } + }); } } @@ -1395,6 +1454,7 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF } else { // No game loaded; just shutdown for safety NativeApp.shutdown(); + updatePausePlayButton(); } } @@ -1704,10 +1764,320 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF break; } } + + private void setupDrawerSettings(View header) { + if (header == null) return; + + SharedPreferences prefs = getSharedPreferences("app_prefs", MODE_PRIVATE); + + // Aspect Ratio spinner - setup like QuickActions + Spinner spAspect = header.findViewById(R.id.drawer_sp_aspect_ratio); + if (spAspect != null) { + if (spAspect.getAdapter() == null) { + ArrayAdapter aspectAdapter = ArrayAdapter.createFromResource(this, + R.array.aspect_ratio_entries, android.R.layout.simple_spinner_item); + aspectAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spAspect.setAdapter(aspectAdapter); + spAspect.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() { + @Override public void onItemSelected(android.widget.AdapterView parent, View view, int position, long id) { + prefs.edit().putInt("aspect_ratio", position).apply(); + try { NativeApp.setAspectRatio(position); } catch (Throwable ignored) {} + } + @Override public void onNothingSelected(android.widget.AdapterView parent) {} + }); + } + ArrayAdapter adapter = (ArrayAdapter) spAspect.getAdapter(); + if (adapter != null) { + int savedAspect = prefs.getInt("aspect_ratio", 1); + if (savedAspect < 0 || savedAspect >= adapter.getCount()) savedAspect = 1; + spAspect.setSelection(savedAspect); + } + } + + // Resolution Scale spinner + Spinner spScale = header.findViewById(R.id.drawer_sp_scale); + if (spScale != null) { + if (spScale.getAdapter() == null) { + ArrayAdapter scaleAdapter = ArrayAdapter.createFromResource(this, + R.array.scale_entries, android.R.layout.simple_spinner_item); + scaleAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spScale.setAdapter(scaleAdapter); + spScale.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() { + @Override public void onItemSelected(android.widget.AdapterView parent, View view, int position, long id) { + float scale = Math.max(1, Math.min(8, position + 1)); + prefs.edit().putFloat("upscale_multiplier", scale).apply(); + try { NativeApp.renderUpscalemultiplier(scale); } catch (Throwable ignored) {} + } + @Override public void onNothingSelected(android.widget.AdapterView parent) {} + }); + } + ArrayAdapter adapter = (ArrayAdapter) spScale.getAdapter(); + if (adapter != null) { + float savedScale = prefs.getFloat("upscale_multiplier", 1.0f); + int scaleIndex = Math.max(0, Math.min(adapter.getCount() - 1, Math.round(savedScale) - 1)); + spScale.setSelection(scaleIndex); + } + } + + // Blending Accuracy spinner - setup like QuickActions + Spinner spBlending = header.findViewById(R.id.drawer_sp_blending_accuracy); + if (spBlending != null) { + if (spBlending.getAdapter() == null) { + ArrayAdapter blendAdapter = ArrayAdapter.createFromResource(this, + R.array.blending_accuracy_entries, android.R.layout.simple_spinner_item); + blendAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spBlending.setAdapter(blendAdapter); + spBlending.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() { + @Override public void onItemSelected(android.widget.AdapterView parent, View view, int position, long id) { + prefs.edit().putInt("blending_accuracy", position).apply(); + try { NativeApp.setBlendingAccuracy(position); } catch (Throwable ignored) {} + } + @Override public void onNothingSelected(android.widget.AdapterView parent) {} + }); + } + ArrayAdapter adapter = (ArrayAdapter) spBlending.getAdapter(); + if (adapter != null) { + int savedBlend = prefs.getInt("blending_accuracy", 1); + if (savedBlend < 0 || savedBlend >= adapter.getCount()) savedBlend = 1; + spBlending.setSelection(savedBlend); + } + } + + // Setup switch listeners only once + setupDrawerSwitchListeners(header, prefs); + } + + private void setupDrawerSwitchListeners(View header, SharedPreferences prefs) { + // Widescreen Patches switch + com.google.android.material.materialswitch.MaterialSwitch swWide = header.findViewById(R.id.drawer_sw_widescreen); + if (swWide != null && swWide.getTag() == null) { + swWide.setTag("setup"); // Mark as setup to avoid duplicate listeners + swWide.setOnCheckedChangeListener((buttonView, isChecked) -> { + prefs.edit().putBoolean("widescreen_patches", isChecked).apply(); + try { NativeApp.setWidescreenPatches(isChecked); } catch (Throwable ignored) {} + }); + } + + // No Interlacing switch + com.google.android.material.materialswitch.MaterialSwitch swNoInt = header.findViewById(R.id.drawer_sw_no_interlacing); + if (swNoInt != null && swNoInt.getTag() == null) { + swNoInt.setTag("setup"); + swNoInt.setOnCheckedChangeListener((buttonView, isChecked) -> { + prefs.edit().putBoolean("no_interlacing_patches", isChecked).apply(); + try { NativeApp.setNoInterlacingPatches(isChecked); } catch (Throwable ignored) {} + }); + } + + // Load Textures switch + com.google.android.material.materialswitch.MaterialSwitch swLoadTex = header.findViewById(R.id.drawer_sw_load_textures); + if (swLoadTex != null && swLoadTex.getTag() == null) { + swLoadTex.setTag("setup"); + swLoadTex.setOnCheckedChangeListener((buttonView, isChecked) -> { + prefs.edit().putBoolean("load_textures", isChecked).apply(); + try { NativeApp.setLoadTextures(isChecked); } catch (Throwable ignored) {} + }); + } + + // Async Textures switch + com.google.android.material.materialswitch.MaterialSwitch swAsyncTex = header.findViewById(R.id.drawer_sw_async_textures); + if (swAsyncTex != null && swAsyncTex.getTag() == null) { + swAsyncTex.setTag("setup"); + swAsyncTex.setOnCheckedChangeListener((buttonView, isChecked) -> { + prefs.edit().putBoolean("async_texture_loading", isChecked).apply(); + try { NativeApp.setAsyncTextureLoading(isChecked); } catch (Throwable ignored) {} + }); + } + + // Precache Textures switch + com.google.android.material.materialswitch.MaterialSwitch swPrecache = header.findViewById(R.id.drawer_sw_precache_textures); + if (swPrecache != null && swPrecache.getTag() == null) { + swPrecache.setTag("setup"); + swPrecache.setOnCheckedChangeListener((buttonView, isChecked) -> { + prefs.edit().putBoolean("precache_textures", isChecked).apply(); + try { NativeApp.setPrecacheTextureReplacements(isChecked); } catch (Throwable ignored) {} + }); + } + + // HUD Developer switch + com.google.android.material.materialswitch.MaterialSwitch swDevHud = header.findViewById(R.id.drawer_sw_dev_hud); + if (swDevHud != null && swDevHud.getTag() == null) { + swDevHud.setTag("setup"); + swDevHud.setOnCheckedChangeListener((buttonView, isChecked) -> { + prefs.edit().putBoolean("hud_visible", isChecked).apply(); + try { NativeApp.setHudVisible(isChecked); } catch (Throwable ignored) {} + }); + } + } + + private void refreshDrawerSettings() { + try { + NavigationView nav = findViewById(R.id.nav_view); + if (nav != null && nav.getHeaderCount() > 0) { + View header = nav.getHeaderView(0); + if (header != null) { + SharedPreferences prefs = getSharedPreferences("app_prefs", MODE_PRIVATE); + + // Refresh switch states only (spinners are set up once in setupDrawerSettings) + com.google.android.material.materialswitch.MaterialSwitch swWide = header.findViewById(R.id.drawer_sw_widescreen); + if (swWide != null) { + swWide.setChecked(prefs.getBoolean("widescreen_patches", true)); + } + + com.google.android.material.materialswitch.MaterialSwitch swNoInt = header.findViewById(R.id.drawer_sw_no_interlacing); + if (swNoInt != null) { + swNoInt.setChecked(prefs.getBoolean("no_interlacing_patches", true)); + } + + com.google.android.material.materialswitch.MaterialSwitch swLoadTex = header.findViewById(R.id.drawer_sw_load_textures); + if (swLoadTex != null) { + swLoadTex.setChecked(prefs.getBoolean("load_textures", false)); + } + + com.google.android.material.materialswitch.MaterialSwitch swAsyncTex = header.findViewById(R.id.drawer_sw_async_textures); + if (swAsyncTex != null) { + swAsyncTex.setChecked(prefs.getBoolean("async_texture_loading", true)); + } + + com.google.android.material.materialswitch.MaterialSwitch swPrecache = header.findViewById(R.id.drawer_sw_precache_textures); + if (swPrecache != null) { + swPrecache.setChecked(prefs.getBoolean("precache_textures", false)); + } + + com.google.android.material.materialswitch.MaterialSwitch swDevHud = header.findViewById(R.id.drawer_sw_dev_hud); + if (swDevHud != null) { + swDevHud.setChecked(prefs.getBoolean("hud_visible", false)); + } + } + } + } catch (Throwable ignored) {} + } + + private void loadAndApplyStoredSettings() { + try { + SharedPreferences prefs = getSharedPreferences("app_prefs", MODE_PRIVATE); + + // Apply aspect ratio setting + int aspectRatio = prefs.getInt("aspect_ratio", 1); + NativeApp.setAspectRatio(aspectRatio); + + // Apply blending accuracy setting + int blendingAccuracy = prefs.getInt("blending_accuracy", 1); + NativeApp.setBlendingAccuracy(blendingAccuracy); + + // Apply other settings + boolean widescreenPatches = prefs.getBoolean("widescreen_patches", true); + NativeApp.setWidescreenPatches(widescreenPatches); + + boolean noInterlacing = prefs.getBoolean("no_interlacing_patches", true); + NativeApp.setNoInterlacingPatches(noInterlacing); + + boolean loadTextures = prefs.getBoolean("load_textures", false); + NativeApp.setLoadTextures(loadTextures); + + boolean asyncTextures = prefs.getBoolean("async_texture_loading", true); + NativeApp.setAsyncTextureLoading(asyncTextures); + + boolean precacheTextures = prefs.getBoolean("precache_textures", false); + NativeApp.setPrecacheTextureReplacements(precacheTextures); + + // Apply renderer setting + int renderer = prefs.getInt("renderer", -1); + NativeApp.renderGpu(renderer); + + } catch (Throwable ignored) {} + } + + private void showControllerHintIfNeeded() { + // Prevent multiple dialogs from showing + if (mControllerHintShowing) return; + + SharedPreferences prefs = getSharedPreferences("app_prefs", MODE_PRIVATE); + boolean hasShownHint = prefs.getBoolean("controller_hint_shown", false); + + // Only show the hint once, and only if setup is complete + if (!hasShownHint && prefs.getBoolean("first_run_done", false)) { + mControllerHintShowing = true; + // Small delay to avoid showing immediately on startup + findViewById(android.R.id.content).postDelayed(() -> { + if (!isFinishing() && !mSetupWizardActive && mControllerHintShowing) { + showControllerHintDialog(); + } + }, 2000); // 2 second delay + } + } + + private void showControllerHintDialog() { + // Double-check to prevent multiple dialogs + if (!mControllerHintShowing) return; + + View dialogView = getLayoutInflater().inflate(R.layout.dialog_controller_hint, null); + + com.google.android.material.materialswitch.MaterialSwitch swDontShow = dialogView.findViewById(R.id.sw_dont_show_again); + + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this, + com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog) + .setCustomTitle(UiUtils.centeredDialogTitle(this, "Controller Tip")) + .setView(dialogView) + .setPositiveButton("Got it!", (dialog, which) -> { + SharedPreferences prefs = getSharedPreferences("app_prefs", MODE_PRIVATE); + if (swDontShow != null && swDontShow.isChecked()) { + prefs.edit().putBoolean("controller_hint_shown", true).apply(); + } + mControllerHintShowing = false; + dialog.dismiss(); + }) + .setCancelable(true) + .setOnDismissListener(dialog -> { + mControllerHintShowing = false; + }); + + AlertDialog dialog = builder.create(); + dialog.show(); + } + + // Pause/Play toggle functionality + public void togglePauseState() { + try { + boolean isPaused = NativeApp.isPaused(); + if (isPaused) { + NativeApp.resume(); + } else { + NativeApp.pause(); + } + updatePausePlayButton(); + } catch (Throwable ignored) {} + } + + private void updatePausePlayButton() { + MaterialButton btn_pause_play = findViewById(R.id.btn_pause_play); + if (btn_pause_play != null) { + try { + boolean isPaused = NativeApp.isPaused(); + boolean hasGame = hasSelectedGame() && isThread(); + + // Show button only when a game is running + btn_pause_play.setVisibility(hasGame ? View.VISIBLE : View.GONE); + + if (hasGame) { + // Update icon to show the action it will perform + if (isPaused) { + // Game is paused, show pause icon (will pause more/stay paused) + btn_pause_play.setIcon(ContextCompat.getDrawable(this, R.drawable.pause_circle_24px)); + } else { + // Game is running, show play icon (will keep playing) + btn_pause_play.setIcon(ContextCompat.getDrawable(this, R.drawable.play_circle_24px)); + } + } + } catch (Throwable ignored) {} + } + } + + // Call this method when game starts/stops to update button visibility + public void updateGameState() { + updatePausePlayButton(); + } + + + + } - - - - - - diff --git a/app/src/main/java/com/izzy2lost/psx2/NativeApp.java b/app/src/main/java/com/izzy2lost/psx2/NativeApp.java index 131040f..e49fc0c 100644 --- a/app/src/main/java/com/izzy2lost/psx2/NativeApp.java +++ b/app/src/main/java/com/izzy2lost/psx2/NativeApp.java @@ -159,6 +159,7 @@ public class NativeApp { public static native void pause(); public static native void resume(); + public static native boolean isPaused(); public static native void shutdown(); public static native boolean saveStateToSlot(int slot); diff --git a/app/src/main/java/com/izzy2lost/psx2/QuickActionsDialogFragment.java b/app/src/main/java/com/izzy2lost/psx2/QuickActionsDialogFragment.java index d896653..4d836e7 100644 --- a/app/src/main/java/com/izzy2lost/psx2/QuickActionsDialogFragment.java +++ b/app/src/main/java/com/izzy2lost/psx2/QuickActionsDialogFragment.java @@ -10,6 +10,7 @@ import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; import com.google.android.material.button.MaterialButton; +import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.materialswitch.MaterialSwitch; import android.widget.Spinner; @@ -22,13 +23,14 @@ public class QuickActionsDialogFragment extends DialogFragment { public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { View view = getLayoutInflater().inflate(R.layout.dialog_quick_actions, null, false); - MaterialButton btnPower = view.findViewById(R.id.btn_quick_power); - MaterialButton btnReboot = view.findViewById(R.id.btn_quick_reboot); + FloatingActionButton btnPower = view.findViewById(R.id.btn_quick_power); + FloatingActionButton btnReboot = view.findViewById(R.id.btn_quick_reboot); com.google.android.material.button.MaterialButtonToggleGroup tgRenderer = view.findViewById(R.id.qa_tg_renderer); View tbAt = view.findViewById(R.id.qa_tb_at); View tbVk = view.findViewById(R.id.qa_tb_vk); View tbGl = view.findViewById(R.id.qa_tb_gl); View tbSw = view.findViewById(R.id.qa_tb_sw); + FloatingActionButton btnPausePlay = view.findViewById(R.id.btn_quick_pause_play); MaterialButton btnGames = view.findViewById(R.id.btn_quick_games); MaterialButton btnSaves = view.findViewById(R.id.btn_quick_saves); MaterialButton btnCancel = view.findViewById(R.id.btn_cancel); @@ -51,6 +53,26 @@ public class QuickActionsDialogFragment extends DialogFragment { .show()); } + // Pause/Play toggle + if (btnPausePlay != null) { + try { + boolean isPaused = NativeApp.isPaused(); + btnPausePlay.setImageResource(R.drawable.play_pause_24px); + } catch (Throwable ignored) {} + btnPausePlay.setOnClickListener(v -> { + try { + if (requireActivity() instanceof MainActivity) { + ((MainActivity) requireActivity()).togglePauseState(); + } else { + boolean paused = NativeApp.isPaused(); + if (paused) NativeApp.resume(); else NativeApp.pause(); + } + boolean isPausedNow = NativeApp.isPaused(); + btnPausePlay.setImageResource(R.drawable.play_pause_24px); + } catch (Throwable ignored) {} + }); + } + // Reboot game if (btnReboot != null) { btnReboot.setOnClickListener(v -> new MaterialAlertDialogBuilder(requireContext(), @@ -208,3 +230,5 @@ public class QuickActionsDialogFragment extends DialogFragment { System.exit(0); } } + + diff --git a/app/src/main/java/com/izzy2lost/psx2/SetupWizardDialogFragment.java b/app/src/main/java/com/izzy2lost/psx2/SetupWizardDialogFragment.java index 2482beb..5e22c6c 100644 --- a/app/src/main/java/com/izzy2lost/psx2/SetupWizardDialogFragment.java +++ b/app/src/main/java/com/izzy2lost/psx2/SetupWizardDialogFragment.java @@ -56,12 +56,16 @@ public class SetupWizardDialogFragment extends DialogFragment { int pad = (int)(24 * getResources().getDisplayMetrics().density); root.setPadding(pad, pad, pad, pad); root.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + // Use theme background + root.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.md_theme_surface)); titleView = new TextView(requireContext()); titleView.setText("Welcome! Let's set up PSX2"); - titleView.setTextSize(22f); + titleView.setTextSize(26f); titleView.setGravity(Gravity.CENTER_HORIZONTAL); titleView.setTextAlignment(View.TEXT_ALIGNMENT_CENTER); + titleView.setTextColor(ContextCompat.getColor(requireContext(), R.color.md_theme_tertiaryFixedDim)); + titleView.setTypeface(titleView.getTypeface(), android.graphics.Typeface.BOLD); root.addView(titleView); // Subtitle removed; using inline hint near the Done button instead. @@ -144,7 +148,8 @@ public class SetupWizardDialogFragment extends DialogFragment { hintView.setTextSize(14f); hintView.setGravity(Gravity.CENTER_HORIZONTAL); hintView.setTextAlignment(View.TEXT_ALIGNMENT_CENTER); - hintView.setAlpha(0.8f); + hintView.setTextColor(ContextCompat.getColor(requireContext(), R.color.brand_primary)); + hintView.setAlpha(0.9f); LinearLayout.LayoutParams hintLp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); hintLp.topMargin = (int)(8 * getResources().getDisplayMetrics().density); hintView.setLayoutParams(hintLp); @@ -199,11 +204,37 @@ public class SetupWizardDialogFragment extends DialogFragment { btnGames.setIcon(step2 ? ContextCompat.getDrawable(requireContext(), R.drawable.check_circle_24px) : null); btnBios.setIcon(step3 ? ContextCompat.getDrawable(requireContext(), R.drawable.check_circle_24px) : null); + // Add theme accent to completed buttons + int themeAccent = ContextCompat.getColor(requireContext(), R.color.md_theme_tertiary); + int defaultColor = ContextCompat.getColor(requireContext(), R.color.md_theme_onSurface); + + btnData.setIconTint(step1 ? android.content.res.ColorStateList.valueOf(themeAccent) : null); + btnGames.setIconTint(step2 ? android.content.res.ColorStateList.valueOf(themeAccent) : null); + btnBios.setIconTint(step3 ? android.content.res.ColorStateList.valueOf(themeAccent) : null); + btnGames.setEnabled(step1); btnBios.setEnabled(step1 && step2); boolean doneEnabled = (step1 && step2 && step3); btnDone.setEnabled(doneEnabled); - if (hintView != null) hintView.setVisibility(doneEnabled ? View.GONE : View.VISIBLE); + + // Style the Done button when ready + if (doneEnabled) { + btnDone.setBackgroundTintList(android.content.res.ColorStateList.valueOf(themeAccent)); + btnDone.setTextColor(ContextCompat.getColor(requireContext(), R.color.md_theme_onTertiary)); + if (hintView != null) { + hintView.setText("🎉 Ready to go! Tap Done to start."); + hintView.setTextColor(ContextCompat.getColor(requireContext(), R.color.md_theme_tertiaryFixedDim)); + } + } else { + btnDone.setBackgroundTintList(null); + btnDone.setTextColor(defaultColor); + if (hintView != null) { + hintView.setText("Complete all steps to finish."); + hintView.setTextColor(ContextCompat.getColor(requireContext(), R.color.brand_primary)); + } + } + + if (hintView != null) hintView.setVisibility(View.VISIBLE); } } diff --git a/app/src/main/res/drawable/pause_circle_24px.xml b/app/src/main/res/drawable/pause_circle_24px.xml new file mode 100644 index 0000000..d55af66 --- /dev/null +++ b/app/src/main/res/drawable/pause_circle_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/play_circle_24px.xml b/app/src/main/res/drawable/play_circle_24px.xml new file mode 100644 index 0000000..bc3ff87 --- /dev/null +++ b/app/src/main/res/drawable/play_circle_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/play_pause_24px.xml b/app/src/main/res/drawable/play_pause_24px.xml new file mode 100644 index 0000000..78262ca --- /dev/null +++ b/app/src/main/res/drawable/play_pause_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/toggle_button_background.xml b/app/src/main/res/drawable/toggle_button_background.xml new file mode 100644 index 0000000..d4af765 --- /dev/null +++ b/app/src/main/res/drawable/toggle_button_background.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/toggle_button_text_color.xml b/app/src/main/res/drawable/toggle_button_text_color.xml new file mode 100644 index 0000000..a6f451b --- /dev/null +++ b/app/src/main/res/drawable/toggle_button_text_color.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml index 533ec6f..90c3be5 100644 --- a/app/src/main/res/layout-land/activity_main.xml +++ b/app/src/main/res/layout-land/activity_main.xml @@ -39,6 +39,29 @@ app:layout_constraintStart_toStartOf="parent" android:layout_margin="12dp"/> + + + 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 0a622e3..ede9c81 100644 --- a/app/src/main/res/layout-land/dialog_covers_grid.xml +++ b/app/src/main/res/layout-land/dialog_covers_grid.xml @@ -1,4 +1,4 @@ - + - - + + diff --git a/app/src/main/res/layout-port/activity_main.xml b/app/src/main/res/layout-port/activity_main.xml index 4aa38e3..0352770 100644 --- a/app/src/main/res/layout-port/activity_main.xml +++ b/app/src/main/res/layout-port/activity_main.xml @@ -39,6 +39,29 @@ app:layout_constraintStart_toStartOf="parent" android:layout_margin="12dp"/> + + + - - + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_game_settings.xml b/app/src/main/res/layout/dialog_game_settings.xml index c6d3cd8..9b4060d 100644 --- a/app/src/main/res/layout/dialog_game_settings.xml +++ b/app/src/main/res/layout/dialog_game_settings.xml @@ -15,11 +15,13 @@ @@ -28,7 +30,7 @@ android:layout_height="wrap_content" android:text="Per-Game Settings (Long-press from game list)" android:textSize="12sp" - android:textColor="#666666" + android:textColor="@color/md_theme_tertiaryFixedDim" android:layout_marginBottom="4dp"/> @@ -38,7 +40,7 @@ android:layout_height="wrap_content" android:text="Serial: SLUS-12345 | CRC: 12345678" android:textSize="12sp" - android:textColor="#888888" + android:textColor="@color/md_theme_tertiaryFixedDim" android:layout_marginBottom="16dp"/> @@ -58,13 +60,29 @@ android:text="Graphics" android:textStyle="bold" android:textSize="16sp" + android:textColor="@color/brand_primary" android:paddingBottom="8dp"/> + + + + @@ -121,6 +143,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="No Interlacing Patches" + android:textColor="@android:color/white" style="@style/Widget.Material3.CompoundButton.MaterialSwitch" android:layout_marginBottom="8dp"/> @@ -139,6 +162,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Enable Cheats" + android:textColor="@android:color/white" style="@style/Widget.Material3.CompoundButton.MaterialSwitch" android:layout_marginBottom="16dp"/> @@ -148,6 +172,7 @@ android:layout_height="wrap_content" android:text="Texture Packs" android:textSize="14sp" + android:textColor="@color/brand_primary" android:paddingBottom="4dp"/> @@ -163,6 +189,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Async Texture Loading" + android:textColor="@android:color/white" style="@style/Widget.Material3.CompoundButton.MaterialSwitch" android:layout_marginBottom="16dp"/> diff --git a/app/src/main/res/layout/dialog_quick_actions.xml b/app/src/main/res/layout/dialog_quick_actions.xml index f05fc2b..e7ffea2 100644 --- a/app/src/main/res/layout/dialog_quick_actions.xml +++ b/app/src/main/res/layout/dialog_quick_actions.xml @@ -1,6 +1,11 @@ - - + + + + android:textColor="@color/brand_primary" /> - + - + + + android:contentDescription="Power" + app:srcCompat="@drawable/ic_power" + android:layout_marginEnd="12dp" /> - - - + android:contentDescription="Reboot" + app:srcCompat="@drawable/ic_reboot" /> @@ -109,8 +115,10 @@ android:layout_height="wrap_content" android:layout_weight="1" android:text="GAMES" + android:textColor="@android:color/white" android:textAllCaps="true" android:drawableStart="@drawable/stadia_controller_24px" + android:drawableTint="@color/brand_primary" android:drawablePadding="8dp" /> @@ -135,6 +145,7 @@ android:layout_height="wrap_content" android:text="Aspect Ratio" android:textStyle="bold" + android:textColor="@color/brand_primary" android:layout_marginBottom="4dp"/> - + + + + diff --git a/app/src/main/res/layout/dialog_settings.xml b/app/src/main/res/layout/dialog_settings.xml index 45bcc42..1dce719 100644 --- a/app/src/main/res/layout/dialog_settings.xml +++ b/app/src/main/res/layout/dialog_settings.xml @@ -32,7 +32,7 @@ android:contentDescription="Reboot" android:padding="8dp" android:src="@drawable/ic_reboot" - app:tint="@android:color/holo_red_light" + android:tint="@android:color/holo_red_light" android:layout_marginEnd="8dp" /> + android:tint="@android:color/holo_red_light" /> @@ -86,6 +90,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:minWidth="0dp" + android:textColor="@color/md_theme_tertiary" android:text="SW" android:layout_marginStart="16dp"/> @@ -99,6 +104,7 @@ android:layout_height="wrap_content" android:text="Blending Accuracy" android:textStyle="bold" + android:textColor="@color/brand_primary" android:paddingBottom="8dp"/> @@ -203,6 +213,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Load Texture Replacements" + android:textColor="@android:color/white" style="@style/Widget.Material3.CompoundButton.MaterialSwitch"/> @@ -218,6 +230,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Precache Texture Replacements" + android:textColor="@android:color/white" android:layout_marginTop="8dp" style="@style/Widget.Material3.CompoundButton.MaterialSwitch"/> @@ -231,6 +244,7 @@ android:layout_height="wrap_content" android:text="Developer" android:textStyle="bold" + android:textColor="@color/brand_primary" android:paddingBottom="8dp"/> @@ -256,6 +271,7 @@ android:layout_height="wrap_content" android:text="Controller" android:textStyle="bold" + android:textColor="@color/brand_primary" android:paddingBottom="8dp"/> - + \ No newline at end of file diff --git a/app/src/main/res/layout/drawer_header_settings.xml b/app/src/main/res/layout/drawer_header_settings.xml index a6a2904..cc5fa61 100644 --- a/app/src/main/res/layout/drawer_header_settings.xml +++ b/app/src/main/res/layout/drawer_header_settings.xml @@ -21,6 +21,7 @@ android:text="Settings" android:textStyle="bold" android:textSize="20sp" + android:textColor="@color/brand_primary" android:gravity="center" android:paddingBottom="12dp" /> @@ -56,6 +57,7 @@ android:layout_height="wrap_content" android:text="Renderer" android:textStyle="bold" + android:textColor="@color/brand_primary" android:paddingTop="8dp" android:paddingBottom="4dp" /> @@ -69,45 +71,65 @@ + + + + android:layout_marginTop="4dp" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_letter.xml b/app/src/main/res/layout/item_letter.xml deleted file mode 100644 index 2f7f7d2..0000000 --- a/app/src/main/res/layout/item_letter.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 1b1f7f0..f3d7c7f 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -141,4 +141,8 @@ #D1D4DC #C3C6CE #FFEB3B + + + #40FFFFFF + @android:color/transparent diff --git a/app/src/main/res/values/styles_buttons.xml b/app/src/main/res/values/styles_buttons.xml index 859c94e..9b5c84d 100644 --- a/app/src/main/res/values/styles_buttons.xml +++ b/app/src/main/res/values/styles_buttons.xml @@ -16,4 +16,10 @@ @android:color/transparent 0dp + + +