mirror of
https://github.com/izzy2lost/PSX2.git
synced 2026-07-05 15:18:36 -07:00
added sort game options
This commit is contained in:
-422
@@ -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 A–Z sorting for predictable letter navigation
|
||||
if (sortMode != SORT_ALPHA) {
|
||||
android.util.Log.d("LetterTap", "Switching to ALPHA sort mode");
|
||||
sortMode = SORT_ALPHA;
|
||||
requireContext().getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
|
||||
.edit().putInt("covers_sort_mode", sortMode).apply();
|
||||
pendingLetterJump = letter;
|
||||
applyFilterAndSort();
|
||||
} else {
|
||||
android.util.Log.d("LetterTap", "Already in ALPHA mode, jumping to letter");
|
||||
// Jump to first index for that letter in the current (alphabetical) list
|
||||
jumpToLetter(letter);
|
||||
}
|
||||
// Center letters row on selected
|
||||
if (rvLetters != null && letters != null && !letters.isEmpty()) {
|
||||
int idx = letters.indexOf(letter);
|
||||
if (idx >= 0) {
|
||||
centerLettersOnIndex(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void centerLettersOnIndex(int idx) {
|
||||
if (rvLetters == null || llmLetters == null) return;
|
||||
View snapView = lettersSnapHelper != null ? lettersSnapHelper.findSnapView(llmLetters) : null;
|
||||
int centerPos = (snapView != null) ? rvLetters.getChildAdapterPosition(snapView) : llmLetters.findFirstVisibleItemPosition();
|
||||
if (centerPos == RecyclerView.NO_POSITION) centerPos = 0;
|
||||
int n = letters != null ? letters.size() : 0;
|
||||
if (n <= 0) return;
|
||||
int centerIdx = centerPos % n;
|
||||
int forward = (idx - centerIdx + n) % n;
|
||||
int backward = (centerIdx - idx + n) % n;
|
||||
int delta = (forward <= backward) ? forward : -backward;
|
||||
llmLetters.scrollToPosition(centerPos + delta);
|
||||
rvLetters.post(() -> { resnapLetters(); applyLetterTransforms(rvLetters); });
|
||||
}
|
||||
|
||||
private void resnapLetters() {
|
||||
if (rvLetters == null || llmLetters == null || lettersSnapHelper == null) return;
|
||||
View snap = lettersSnapHelper.findSnapView(llmLetters);
|
||||
if (snap == null) return;
|
||||
int[] dist = lettersSnapHelper.calculateDistanceToFinalSnap(llmLetters, snap);
|
||||
if (dist != null && (dist[0] != 0 || dist[1] != 0)) rvLetters.scrollBy(dist[0], dist[1]);
|
||||
}
|
||||
|
||||
private void updateSortButtonUi(@NonNull com.google.android.material.button.MaterialButton btn) {
|
||||
btn.setIconResource(R.drawable.sort_24px);
|
||||
boolean alpha = (sortMode == SORT_ALPHA);
|
||||
btn.setText(alpha ? "A–Z" : "RECENT");
|
||||
btn.setContentDescription(alpha ? "Sort A–Z" : "Sort Recent");
|
||||
}
|
||||
|
||||
private void showSearchDialog() {
|
||||
final EditText input = new EditText(requireContext());
|
||||
input.setHint("Search games");
|
||||
int pad = (int) (16 * getResources().getDisplayMetrics().density);
|
||||
input.setPadding(pad, pad, pad, pad);
|
||||
new com.google.android.material.dialog.MaterialAlertDialogBuilder(requireContext(),
|
||||
com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog)
|
||||
.setTitle("Search")
|
||||
.setView(input)
|
||||
.setNegativeButton("Clear", (d, w) -> {
|
||||
query = null;
|
||||
applyFilterAndSort();
|
||||
})
|
||||
.setPositiveButton("Apply", (d, w) -> {
|
||||
query = input.getText() != null ? input.getText().toString().trim() : null;
|
||||
if (query != null && query.isEmpty()) query = null;
|
||||
applyFilterAndSort();
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
private void jumpToLetter(char letterRaw) {
|
||||
android.util.Log.d("LetterJump", "jumpToLetter called with: " + letterRaw);
|
||||
if (titles == null || titles.length == 0) {
|
||||
android.util.Log.d("LetterJump", "No titles available");
|
||||
return;
|
||||
}
|
||||
char letter = Character.toUpperCase(letterRaw);
|
||||
int target = -1;
|
||||
for (int i = 0; i < titles.length; i++) {
|
||||
char c = firstLetter(titles[i]);
|
||||
if ((letter == '#') ? (c == '#') : (c == letter)) {
|
||||
target = i;
|
||||
android.util.Log.d("LetterJump", "Found target at index " + i + " for letter " + letter + ", title: " + titles[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (target >= 0) {
|
||||
android.util.Log.d("LetterJump", "Scrolling to index: " + target);
|
||||
scrollToIndex(target);
|
||||
} else {
|
||||
android.util.Log.d("LetterJump", "No target found for letter: " + letter);
|
||||
}
|
||||
}
|
||||
|
||||
private void scrollToIndex(int idx) {
|
||||
if (llm == null || rv == null || titles == null || titles.length == 0) return;
|
||||
View snap = snapHelper != null ? snapHelper.findSnapView(llm) : null;
|
||||
int centerPos = (snap != null) ? rv.getChildAdapterPosition(snap) : llm.findFirstVisibleItemPosition();
|
||||
if (centerPos == RecyclerView.NO_POSITION) centerPos = 0;
|
||||
int n = titles.length;
|
||||
int centerIdx = centerPos % n;
|
||||
int forward = (idx - centerIdx + n) % n;
|
||||
int backward = (centerIdx - idx + n) % n;
|
||||
int delta = (forward <= backward) ? forward : -backward;
|
||||
llm.scrollToPosition(centerPos + delta);
|
||||
rv.post(() -> { resnapToCenter(rv); applyCoverflowTransforms(rv); });
|
||||
}
|
||||
|
||||
private void applyFilterAndSort() {
|
||||
int n = origTitles != null ? origTitles.length : 0;
|
||||
ArrayList<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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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 |
Reference in New Issue
Block a user