added sort game options

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

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB