Cleaned up code

This commit is contained in:
Alexander Doerflinger
2026-01-28 15:28:39 +01:00
parent dde11329bf
commit 2953a1bf67
13 changed files with 396 additions and 234 deletions

View File

@@ -15,6 +15,7 @@ import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import com.aldo.apps.ochecompanion.database.AppDatabase;
import com.aldo.apps.ochecompanion.database.objects.Player;
@@ -77,6 +78,11 @@ public class AddPlayerActivity extends AppCompatActivity {
*/
private MaterialButton mSaveButton;
/**
* Button to delete a players profile from the database.
*/
private ImageView mBtnDelete;
// ========== UI - Cropper Views ==========
/**
@@ -183,6 +189,7 @@ public class AddPlayerActivity extends AppCompatActivity {
mUserNameInput = findViewById(R.id.etUsername);
mTitleView = findViewById(R.id.tvTitle);
mSaveButton = findViewById(R.id.btnSavePlayer);
mBtnDelete = findViewById(R.id.btnDeletePlayer);
// Get references to cropper UI elements
mIvCropPreview = findViewById(R.id.ivCropPreview);
@@ -191,6 +198,9 @@ public class AddPlayerActivity extends AppCompatActivity {
// Set up click listeners
mProfilePictureView.setOnClickListener(v -> mGetContent.launch("image/*"));
mSaveButton.setOnClickListener(v -> savePlayer());
if (mBtnDelete != null) {
mBtnDelete.setOnClickListener(v -> deletePlayer());
}
findViewById(R.id.btnConfirmCrop).setOnClickListener(v -> performCrop());
findViewById(R.id.btnCancelCrop).setOnClickListener(v -> exitCropMode());
}
@@ -202,7 +212,7 @@ public class AddPlayerActivity extends AppCompatActivity {
// Initialize scale detector for pinch-to-zoom functionality
mScaleDetector = new ScaleGestureDetector(this, new ScaleGestureDetector.SimpleOnScaleGestureListener() {
@Override
public boolean onScale(final ScaleGestureDetector detector) {
public boolean onScale(@NonNull final ScaleGestureDetector detector) {
// Apply the scale factor from the gesture
mScaleFactor *= detector.getScaleFactor();
@@ -401,6 +411,7 @@ public class AddPlayerActivity extends AppCompatActivity {
// Update UI labels for "edit mode"
mTitleView.setText(R.string.txt_update_profile_header);
mSaveButton.setText(R.string.txt_update_profile_username_save);
if (mBtnDelete != null) mBtnDelete.setVisibility(View.VISIBLE);
// Load existing profile picture if available
if (mExistingPlayer.profilePictureUri != null) {
@@ -413,6 +424,20 @@ public class AddPlayerActivity extends AppCompatActivity {
}).start();
}
/**
* Deletes the selected player from the database.
*/
private void deletePlayer() {
if (mExistingPlayer == null) return;
new Thread(() -> {
AppDatabase.getDatabase(this).playerDao().delete(mExistingPlayer);
runOnUiThread(() -> {
Toast.makeText(this, "Player removed from squad", Toast.LENGTH_SHORT).show();
finish();
});
}).start();
}
/**
* Validates and persists the player data to the database.
* Inserts new player or updates existing based on mExistingPlayer.
@@ -439,7 +464,7 @@ public class AddPlayerActivity extends AppCompatActivity {
}
// Close activity on main thread after save completes
runOnUiThread(() -> finish());
runOnUiThread(this::finish);
}).start();
}
}

View File

@@ -20,9 +20,8 @@ import com.aldo.apps.ochecompanion.utils.DartsConstants;
import com.aldo.apps.ochecompanion.utils.UIConstants;
import com.google.android.material.button.MaterialButton;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Main game activity for playing X01 darts games (501, 301, etc.).
@@ -41,10 +40,21 @@ public class GameActivity extends AppCompatActivity {
*/
private static final String EXTRA_START_SCORE = "extra_start_score";
/**
* Intent extra for a match ID. Making it possible to load a match from the database.
*/
private static final String EXTRA_MATCH_UUID = "extra_match_uuid";
// ========================================================================================
// Game Logic State
// ========================================================================================
/**
* The matches UUID from the database.
*/
private String mMatchUuid;
/**
* Index of the current active player (0 to playerCount-1).
* Cycles through players as turns complete.
@@ -151,6 +161,7 @@ public class GameActivity extends AppCompatActivity {
Intent intent = new Intent(context, GameActivity.class);
intent.putParcelableArrayListExtra(EXTRA_PLAYERS, players);
intent.putExtra(EXTRA_START_SCORE, startScore);
intent.putExtra(EXTRA_MATCH_UUID, UUID.randomUUID().toString());
context.startActivity(intent);
}
@@ -166,6 +177,7 @@ public class GameActivity extends AppCompatActivity {
// Extract game parameters from intent
mStartingScore = getIntent().getIntExtra(EXTRA_START_SCORE, DartsConstants.DEFAULT_GAME_SCORE);
mMatchUuid = getIntent().getStringExtra(EXTRA_MATCH_UUID);
ArrayList<Player> participants = getIntent().getParcelableArrayListExtra(EXTRA_PLAYERS);
// Initialize activity components in order
@@ -377,6 +389,15 @@ public class GameActivity extends AppCompatActivity {
// Update UI for next player
updateUI();
updateTurnIndicators();
saveMatchProgress();
}
/**
* Saves the current game state to the database.
*/
private void saveMatchProgress() {
// TODO: Persist current state to Room using mMatchUuid
// This allows the "Continue Game" feature on the Main Menu
}
/**

View File

@@ -4,7 +4,6 @@ import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
@@ -16,7 +15,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.aldo.apps.ochecompanion.database.AppDatabase;
import com.aldo.apps.ochecompanion.database.objects.Player;
import com.aldo.apps.ochecompanion.models.Match;
import com.aldo.apps.ochecompanion.database.objects.Match;
import com.aldo.apps.ochecompanion.ui.MatchRecapView;
import com.aldo.apps.ochecompanion.ui.adapter.MainMenuPlayerAdapter;
import com.aldo.apps.ochecompanion.utils.DartsConstants;
@@ -156,13 +155,28 @@ public class MainMenuActivity extends AppCompatActivity {
private void applyTestData(final int counter) {
// Create test player objects
final Player playerOne = new Player(DartsConstants.TEST_PLAYER_1, null);
playerOne.id = 1;
final Player playerTwo = new Player(DartsConstants.TEST_PLAYER_2, null);
playerTwo.id = 2;
final Player playerThree = new Player(DartsConstants.TEST_PLAYER_3, null);
playerThree.id = 3;
final Player playerFour = new Player(DartsConstants.TEST_PLAYER_4, null);
playerFour.id = 4;
// Create test match objects with different player configurations
final Match match1on1 = new Match(playerOne, playerTwo);
final Match matchGroup = new Match(playerOne, playerTwo, playerThree, playerFour);
// Create score maps for test matches
final java.util.Map<Integer, Integer> scores1v1 = new java.util.HashMap<>();
scores1v1.put(1, 0); // Player 1 won (reached 0 from 501)
scores1v1.put(2, 157); // Player 2 remaining score
final java.util.Map<Integer, Integer> scoresGroup = new java.util.HashMap<>();
scoresGroup.put(1, 0); // Player 1 won (reached 0 from 501)
scoresGroup.put(2, 89); // Player 2 remaining score
scoresGroup.put(3, 234); // Player 3 remaining score
scoresGroup.put(4, 312); // Player 4 remaining score
// Create test match objects with different player configurations and scores
final Match match1on1 = new Match("501", java.util.Arrays.asList(playerOne, playerTwo), scores1v1);
final Match matchGroup = new Match("501", java.util.Arrays.asList(playerOne, playerTwo, playerThree, playerFour), scoresGroup);
// Cycle through different test scenarios based on counter value
if (counter % UIConstants.TEST_CYCLE_MODULO == 0) {

View File

@@ -21,7 +21,7 @@ import com.aldo.apps.ochecompanion.database.objects.Player;
* @see Player
* @see Match
*/
@Database(entities = {Player.class, Match.class}, version = 2, exportSchema = false)
@Database(entities = {Player.class, Match.class}, version = 3, exportSchema = false)
public abstract class AppDatabase extends RoomDatabase {
/**

View File

@@ -1,6 +1,7 @@
package com.aldo.apps.ochecompanion.database.dao;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
@@ -34,6 +35,16 @@ public interface PlayerDao {
@Update
void update(final Player player);
/**
* Deletes an existing player from the database.
* Player is identified by its primary key ID.
* Must be called on a background thread.
*
* @param player The player to be deleted.
*/
@Delete
void delete(final Player player);
/**
* Retrieves a player by their unique ID.
* Must be called on a background thread.

View File

@@ -1,14 +1,23 @@
package com.aldo.apps.ochecompanion.database.objects;
import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.PrimaryKey;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Represents a completed darts match in the Oche Companion application.
* Room entity storing match information including game mode, timestamp, player count,
* and detailed performance data for all participants. Implements Serializable for
* passing between Android components.
* passing between Android components. Provides helper methods to parse participant
* JSON data and reconstruct Player objects.
*
* @see com.aldo.apps.ochecompanion.database.dao.MatchDao
* @see com.aldo.apps.ochecompanion.database.objects.Player
@@ -25,7 +34,7 @@ public class Match implements Serializable {
* @see PrimaryKey
*/
@PrimaryKey(autoGenerate = true)
public int mId;
public int id;
/**
* Unix epoch timestamp (milliseconds) when match was completed.
@@ -33,20 +42,20 @@ public class Match implements Serializable {
*
* @see System#currentTimeMillis()
*/
public long mTimestamp;
public long timestamp;
/**
* Identifier for the darts game variant played (e.g., "501", "301", "Cricket").
* Determines scoring rules and UI display for this match.
*/
public String mGameMode;
public String gameMode;
/**
* Total number of players who participated in this match.
* Determines match type (1=solo, 2=1v1, 3+=group) and affects UI display.
* Must match the number of entries in participantData JSON.
*/
public int mPlayerCount;
public int playerCount;
/**
* JSON string containing detailed performance data for all match participants.
@@ -56,7 +65,7 @@ public class Match implements Serializable {
* @see org.json.JSONArray
* @see org.json.JSONObject
*/
public String mParticipantData;
public String participantData;
/**
* Constructs a new Match entity ready for database insertion.
@@ -69,9 +78,219 @@ public class Match implements Serializable {
* @see com.aldo.apps.ochecompanion.database.dao.MatchDao#insert(Match)
*/
public Match(final long timestamp, final String gameMode, final int playerCount, final String participantData) {
this.mTimestamp = timestamp;
this.mGameMode = gameMode;
this.mPlayerCount = playerCount;
this.mParticipantData = participantData;
this.timestamp = timestamp;
this.gameMode = gameMode;
this.playerCount = playerCount;
this.participantData = participantData;
}
/**
* Convenience constructor for creating a Match from a list of Player objects.
* Automatically generates JSON participant data and sets timestamp to current time.
* All players will have a score of 0.
*
* @param gameMode Identifier for the darts game variant (e.g., "501", "Cricket")
* @param players List of Player objects to include in this match
*/
@Ignore
public Match(final String gameMode, final List<Player> players) {
this.timestamp = System.currentTimeMillis();
this.gameMode = gameMode;
this.playerCount = players.size();
this.participantData = generateParticipantJson(players, null);
}
/**
* Convenience constructor for creating a Match from players with their scores.
* Automatically generates JSON participant data and sets timestamp to current time.
*
* @param gameMode Identifier for the darts game variant (e.g., "501", "Cricket")
* @param players List of Player objects to include in this match
* @param scores Map of player IDs to their final scores
*/
@Ignore
public Match(final String gameMode, final List<Player> players, final Map<Integer, Integer> scores) {
this.timestamp = System.currentTimeMillis();
this.gameMode = gameMode;
this.playerCount = players.size();
this.participantData = generateParticipantJson(players, scores);
}
/**
* Returns the number of players participating in this match.
*
* @return The player count
*/
public int getParticipantCount() {
return playerCount;
}
/**
* Retrieves the username of the player at the specified position.
*
* @param position The zero-based index of the player in the match
* @return The username of the player, or "Unknown" if unavailable
*/
public String getPlayerNameByPosition(final int position) {
try {
final JSONArray participants = new JSONArray(participantData);
if (position >= 0 && position < participants.length()) {
final JSONObject participant = participants.getJSONObject(position);
return participant.optString("username", "Unknown");
}
} catch (JSONException e) {
// Return default if JSON parsing fails
}
return "Unknown";
}
/**
* Retrieves the career average of the player at the specified position.
*
* @param position The zero-based index of the player in the match
* @return The career average, or 0.0 if unavailable
*/
public double getPlayerAverageByPosition(final int position) {
try {
final JSONArray participants = new JSONArray(participantData);
if (position >= 0 && position < participants.length()) {
final JSONObject participant = participants.getJSONObject(position);
return participant.optDouble("careerAverage", 0.0);
}
} catch (JSONException e) {
// Return default if JSON parsing fails
}
return 0.0;
}
/**
* Retrieves the match score of the player at the specified position.
*
* @param position The zero-based index of the player in the match
* @return The match score, or 0 if unavailable
*/
public int getPlayerScoreByPosition(final int position) {
try {
final JSONArray participants = new JSONArray(participantData);
if (position >= 0 && position < participants.length()) {
final JSONObject participant = participants.getJSONObject(position);
return participant.optInt("score", 0);
}
} catch (JSONException e) {
// Return default if JSON parsing fails
}
return 0;
}
/**
* Returns a map of player IDs to their match scores.
*
* @return Map of player IDs to scores, or empty map if parsing fails
*/
public Map<Integer, Integer> getPlayerScores() {
final Map<Integer, Integer> scores = new HashMap<>();
try {
final JSONArray participants = new JSONArray(participantData);
for (int i = 0; i < participants.length(); i++) {
final JSONObject participant = participants.getJSONObject(i);
final int playerId = participant.optInt("id", 0);
final int score = participant.optInt("score", 0);
scores.put(playerId, score);
}
} catch (JSONException e) {
// Return empty map if JSON parsing fails
}
return scores;
}
/**
* Returns a list of all Player objects reconstructed from participant data.
*
* @return List of Player objects, or empty list if parsing fails
*/
public List<Player> getAllPlayers() {
final List<Player> players = new ArrayList<>();
try {
final JSONArray participants = new JSONArray(participantData);
for (int i = 0; i < participants.length(); i++) {
final JSONObject participant = participants.getJSONObject(i);
final String username = participant.optString("username", "Unknown");
final String photoUri = participant.optString("photoUri", null);
final Player player = new Player(username, photoUri);
player.id = participant.optInt("id", 0);
player.careerAverage = participant.optDouble("careerAverage", 0.0);
players.add(player);
}
} catch (JSONException e) {
// Return empty list if JSON parsing fails
}
return players;
}
/**
* Generates JSON string from a list of Player objects with optional scores.
*
* @param players List of Player objects to convert
* @param scores Map of player IDs to their match scores (null for all zeros)
* @return JSON string representation of player data
*/
private String generateParticipantJson(final List<Player> players, final Map<Integer, Integer> scores) {
final JSONArray participants = new JSONArray();
try {
for (final Player player : players) {
final JSONObject participant = new JSONObject();
participant.put("id", player.id);
participant.put("username", player.username);
participant.put("photoUri", player.profilePictureUri);
participant.put("careerAverage", player.careerAverage);
final int score = (scores != null && scores.containsKey(player.id)) ? scores.get(player.id) : 0;
participant.put("score", score);
participants.put(participant);
}
} catch (JSONException e) {
// Return empty array if JSON generation fails
}
return participants.toString();
}
/**
* Data class representing a participant in a match with their score.
*/
public static class ParticipantData {
/** The Player object */
public final Player player;
/** The player's score in this match */
public final int score;
public ParticipantData(final Player player, final int score) {
this.player = player;
this.score = score;
}
}
/**
* Returns all participants with their match scores.
*
* @return List of ParticipantData objects containing player info and scores
*/
public List<ParticipantData> getAllParticipants() {
final List<ParticipantData> participants = new ArrayList<>();
try {
final JSONArray participantArray = new JSONArray(participantData);
for (int i = 0; i < participantArray.length(); i++) {
final JSONObject participant = participantArray.getJSONObject(i);
final String username = participant.optString("username", "Unknown");
final String photoUri = participant.optString("photoUri", null);
final Player player = new Player(username, photoUri);
player.id = participant.optInt("id", 0);
player.careerAverage = participant.optDouble("careerAverage", 0.0);
final int score = participant.optInt("score", 0);
participants.add(new ParticipantData(player, score));
}
} catch (JSONException e) {
// Return empty list if JSON parsing fails
}
return participants;
}
}

View File

@@ -3,6 +3,7 @@ package com.aldo.apps.ochecompanion.database.objects;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
@@ -18,32 +19,32 @@ public class Player implements Parcelable {
* Room auto-populates on insert.
*/
@PrimaryKey(autoGenerate = true)
public int mId;
public int id;
/**
* Player's display name shown throughout the app. Should be non-null and
* ideally 1-30 characters. Supports Unicode.
*/
public String mUsername;
public String username;
/**
* File path to player's profile picture. Can be null (shows default avatar).
* Stored as path reference to avoid database bloat.
*/
public String mProfilePictureUri;
public String profilePictureUri;
/**
* Player's career three-dart average across all completed matches.
* Represents overall skill level. Typical ranges: 0-40 (beginner),
* 40-60 (casual), 60-80 (experienced), 80-100 (advanced), 100+ (pro).
*/
public double mCareerAverage = 0.0;
public double careerAverage = 0.0;
/**
* Total number of completed matches for this player. Provides context for
* statistical significance and enables experience-based features.
*/
public int mMatchesPlayed = 0;
public int matchesPlayed = 0;
/**
* Constructs a new Player ready for database insertion. ID is auto-generated
@@ -53,8 +54,8 @@ public class Player implements Parcelable {
* @param profilePictureUri Path to profile image (null for default avatar)
*/
public Player(final String username, final String profilePictureUri) {
this.mUsername = username;
this.mProfilePictureUri = profilePictureUri;
this.username = username;
this.profilePictureUri = profilePictureUri;
}
@@ -65,17 +66,17 @@ public class Player implements Parcelable {
* @param in Parcel containing serialized Player data
*/
protected Player(final Parcel in) {
mId = in.readInt();
mUsername = in.readString();
mProfilePictureUri = in.readString();
mCareerAverage = in.readDouble();
mMatchesPlayed = in.readInt();
id = in.readInt();
username = in.readString();
profilePictureUri = in.readString();
careerAverage = in.readDouble();
matchesPlayed = in.readInt();
}
/**
* Required Parcelable CREATOR field for Player reconstruction.
*/
public static final Creator<Player> sCREATOR = new Creator<Player>() {
public static final Creator<Player> CREATOR = new Creator<>() {
@Override
public Player createFromParcel(final Parcel in) {
return new Player(in);
@@ -100,11 +101,11 @@ public class Player implements Parcelable {
*/
@Override
public void writeToParcel(final Parcel dest, final int flags) {
dest.writeInt(mId);
dest.writeString(mUsername);
dest.writeString(mProfilePictureUri);
dest.writeDouble(mCareerAverage);
dest.writeInt(mMatchesPlayed);
dest.writeInt(id);
dest.writeString(username);
dest.writeString(profilePictureUri);
dest.writeDouble(careerAverage);
dest.writeInt(matchesPlayed);
}
/**
@@ -114,13 +115,14 @@ public class Player implements Parcelable {
* @return String in format "Player{mId=X, mUsername='...', ...}"
*/
@Override
@NonNull
public String toString() {
return "Player{" +
"mId=" + mId +
", mUsername='" + mUsername + '\'' +
", mProfilePictureUri='" + mProfilePictureUri + '\'' +
", mCareerAverage=" + mCareerAverage +
", mMatchesPlayed=" + mMatchesPlayed +
"mId=" + id +
", mUsername='" + username + '\'' +
", mProfilePictureUri='" + profilePictureUri + '\'' +
", mCareerAverage=" + careerAverage +
", mMatchesPlayed=" + matchesPlayed +
'}';
}
}

View File

@@ -1,131 +0,0 @@
package com.aldo.apps.ochecompanion.models;
import android.util.Log;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.List;
import com.aldo.apps.ochecompanion.database.objects.Player;
/**
* Model class representing a darts match with multiple participants.
*/
public class Match {
/**
* Tag for logging.
*/
private static final String TAG = "Match";
/**
* List of players participating in this match.
*/
private final List<Player> mPlayers;
/**
* Constructs an empty Match with no participants.
*/
public Match() {
// Initialize empty player list
mPlayers = new ArrayList<>();
// Log creation for debugging purposes
Log.d(TAG, "Match: Creating new empty match.");
}
/**
* Constructs a Match with the specified players.
*
* @param players Variable number of Player objects to participate in the match.
*/
public Match(final Player... players) {
// Initialize empty player list
mPlayers = new ArrayList<>();
// Add each player to the match in order
for (final Player player : players) {
// Log the addition for debugging
Log.d(TAG, "Match: Adding [" + player + "]");
// Add player to the internal list
mPlayers.add(player);
}
}
/**
* Returns the number of players participating in this match.
*
* @return The number of players in this match.
*/
public int getParticipantCount() {
return mPlayers.size();
}
/**
* Retrieves the username of the player at the specified position.
*
* @param position The zero-based index of the player in the match.
* @return The username of the player at the specified position, or "INVALID" if the position is out of bounds.
*/
public String getPlayerNameByPosition(final int position) {
// Validate position is within bounds
// Note: Consider changing <= to < to prevent IndexOutOfBoundsException
if (position >= 0 && position <= mPlayers.size()) {
// Return the username of the player at this position
return mPlayers.get(position).username;
}
// Return sentinel value for invalid position
return "INVALID";
}
/**
* Retrieves the career average of the player at the specified position.
*
* @param position The zero-based index of the player in the match.
* @return The career average of the player at the specified position, or -1 if the position is out of bounds.
*/
public double getPlayerAverageByPosition(final int position) {
// Validate position is within bounds
// Note: Consider changing <= to < to prevent IndexOutOfBoundsException
if (position >= 0 && position <= mPlayers.size()) {
// Return the career average of the player at this position
return mPlayers.get(position).careerAverage;
}
// Return sentinel value for invalid position
return -1;
}
/**
* Returns the list of all players in this match.
*
* @return The list of all Player objects in this match.
*/
public List<Player> getAllPlayers() {
return mPlayers;
}
/**
* Returns a string representation of this Match.
*
* @return A string representation of this match including all participating players.
*/
@NonNull
@Override
public String toString() {
// Use StringBuilder for efficient concatenation
final StringBuilder sb = new StringBuilder();
// Start the match representation
sb.append("Match {");
// Append each player's string representation
for (final Player player : mPlayers) {
sb.append("[").append(player).append("]");
}
// Close the match representation
// Note: This adds "]]" instead of "}". Consider fixing to sb.append("}");
sb.append("]");
return sb.toString();
}
}

View File

@@ -8,6 +8,8 @@ import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.aldo.apps.ochecompanion.utils.UIConstants;
@@ -26,9 +28,6 @@ public class CropOverlayView extends View {
/** Path with CW outer rect and CCW inner rect creating transparent hole. */
private final Path mPath = new Path();
/** Calculated side length of square crop box (80% of width). */
private float mBoxSize;
/** Constructor for programmatic instantiation. */
public CropOverlayView(final Context context) {
super(context);
@@ -58,10 +57,11 @@ public class CropOverlayView extends View {
protected void onLayout(final boolean changed, final int left, final int top, final int right, final int bottom) {
super.onLayout(changed, left, top, right, bottom);
mBoxSize = getWidth() * UIConstants.CROP_BOX_SIZE_RATIO;
final float l = (getWidth() - mBoxSize) / 2;
final float t = (getHeight() - mBoxSize) / 2;
mCropRect.set(l, t, l + mBoxSize, t + mBoxSize);
/* Calculated side length of square crop box (80% of width). */
final float boxSize = getWidth() * UIConstants.CROP_BOX_SIZE_RATIO;
final float l = (getWidth() - boxSize) / 2;
final float t = (getHeight() - boxSize) / 2;
mCropRect.set(l, t, l + boxSize, t + boxSize);
mPath.reset();
mPath.addRect(0, 0, getWidth(), getHeight(), Path.Direction.CW);
@@ -70,7 +70,7 @@ public class CropOverlayView extends View {
/** Renders the overlay mask with transparent center cutout. */
@Override
protected void onDraw(final Canvas canvas) {
protected void onDraw(@NonNull final Canvas canvas) {
super.onDraw(canvas);
canvas.drawPath(mPath, mMaskPaint);
}

View File

@@ -10,7 +10,7 @@ import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.aldo.apps.ochecompanion.R;
import com.aldo.apps.ochecompanion.models.Match;
import com.aldo.apps.ochecompanion.database.objects.Match;
import com.aldo.apps.ochecompanion.ui.adapter.MainMenuGroupMatchAdapter;
/**
@@ -92,10 +92,10 @@ public class MatchRecapView extends FrameLayout {
updateVisibility(mState1v1);
mTvP1Name.setText(match.getPlayerNameByPosition(0));
mTvP1Score.setText(String.valueOf(match.getPlayerAverageByPosition(0)));
mTvP1Score.setText(String.valueOf(match.getPlayerScoreByPosition(0)));
mTvP2Name.setText(match.getPlayerNameByPosition(1));
mTvP2Score.setText(String.valueOf(match.getPlayerAverageByPosition(1)));
mTvP2Score.setText(String.valueOf(match.getPlayerScoreByPosition(1)));
}
/** Configures group state with leaderboard RecyclerView. */

View File

@@ -11,18 +11,17 @@ import androidx.recyclerview.widget.RecyclerView;
import com.aldo.apps.ochecompanion.R;
import com.aldo.apps.ochecompanion.database.objects.Player;
import com.aldo.apps.ochecompanion.models.Match;
import com.aldo.apps.ochecompanion.database.objects.Match;
import com.aldo.apps.ochecompanion.ui.PlayerItemView;
import com.bumptech.glide.Glide;
import com.google.android.material.imageview.ShapeableImageView;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/**
* RecyclerView adapter for displaying group match results in Main Menu.
* Displays players sorted by career average with their names, scores, and profile pictures.
* Displays participants sorted by match score (descending) with their names, scores, and profile pictures.
*/
public class MainMenuGroupMatchAdapter extends RecyclerView.Adapter<MainMenuGroupMatchAdapter.GroupMatchHolder> {
@@ -30,7 +29,7 @@ public class MainMenuGroupMatchAdapter extends RecyclerView.Adapter<MainMenuGrou
private static final String TAG = "MainMenuGroupMatchAdapt";
/** List of players sorted by career average. */
private final List<Player> mPlayersList = new ArrayList<>();
private final List<Match.ParticipantData> mParticipantsList = new ArrayList<>();
/**
* Creates a new ViewHolder with a PlayerItemView.
@@ -62,24 +61,24 @@ public class MainMenuGroupMatchAdapter extends RecyclerView.Adapter<MainMenuGrou
*/
@Override
public void onBindViewHolder(@NonNull final GroupMatchHolder holder, final int position) {
// Retrieve the player at this position and bind it to the holder
holder.setPlayer(mPlayersList.get(position));
// Retrieve the participant at this position and bind it to the holder
holder.setParticipant(mParticipantsList.get(position));
}
/**
* Returns the number of players.
* Returns the number of participants.
*
* @return Player count.
* @return Participant count.
*/
@Override
public int getItemCount() {
return mPlayersList.size();
return mParticipantsList.size();
}
/**
* Updates the adapter with match data, sorting players by career average.
* Updates the adapter with match data, sorting participants by match score (descending).
*
* @param match The match containing players to display.
* @param match The match containing participants to display.
*/
@SuppressLint("NotifyDataSetChanged")
public void updateMatch(final Match match) {
@@ -87,23 +86,21 @@ public class MainMenuGroupMatchAdapter extends RecyclerView.Adapter<MainMenuGrou
Log.d(TAG, "updateMatch: match is null, aborting update.");
return;
}
// Clear any existing player data
mPlayersList.clear();
// Clear any existing participant data
mParticipantsList.clear();
if (match.getAllPlayers() == null || match.getAllPlayers().isEmpty()) {
Log.d(TAG, "updateMatch: No players found in the match, just clearing.");
final List<Match.ParticipantData> participants = match.getAllParticipants();
if (participants == null || participants.isEmpty()) {
Log.d(TAG, "updateMatch: No participants found in the match, just clearing.");
notifyDataSetChanged();
return;
}
// Get all players from the match
final List<Player> allPlayers = match.getAllPlayers();
// Sort participants by match score (highest to lowest)
participants.sort((p1, p2) -> Integer.compare(p2.score, p1.score));
// Sort players by career average (lowest to highest)
allPlayers.sort(new PlayerScoreComparator());
// Add sorted players to the display list
mPlayersList.addAll(allPlayers);
// Add sorted participants to the display list
mParticipantsList.addAll(participants);
// Notify RecyclerView to refresh the display
notifyDataSetChanged();
@@ -143,18 +140,19 @@ public class MainMenuGroupMatchAdapter extends RecyclerView.Adapter<MainMenuGrou
}
/**
* Binds player data to this ViewHolder.
* Binds participant data (player + match score) to this ViewHolder.
*
* @param player The player to display.
* @param participantData The participant data to display.
*/
public void setPlayer(final Player player) {
public void setParticipant(final Match.ParticipantData participantData) {
final Player player = participantData.player;
final int score = participantData.score;
// Set player name
mPlayerNameView.setText(player.username);
// Format and set career average score
mPlayerScoreView.setText(String.format(
itemView.getContext().getString(R.string.txt_player_average_base),
player.careerAverage));
// Display match score instead of career average
mPlayerScoreView.setText(String.valueOf(score));
// Load profile picture or show default icon
if (player.profilePictureUri != null) {
@@ -168,23 +166,4 @@ public class MainMenuGroupMatchAdapter extends RecyclerView.Adapter<MainMenuGrou
}
}
}
/**
* Comparator for sorting players by career average in ascending order.
*/
public static class PlayerScoreComparator implements Comparator<Player> {
/**
* Compares two players by career average.
*
* @param p1 First player.
* @param p2 Second player.
* @return Comparison result.
*/
@Override
public int compare(final Player p1, final Player p2) {
// Compare career averages in ascending order
return Double.compare(p1.careerAverage, p2.careerAverage);
}
}
}

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/double_red"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6V19zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" />
</vector>

View File

@@ -5,6 +5,19 @@
android:layout_height="match_parent"
android:background="@color/midnight_black">
<ImageView
android:id="@+id/btnDeletePlayer"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="top|end"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:padding="8dp"
android:src="@drawable/ic_delete"
android:background="?attr/selectableItemBackgroundBorderless"
android:visibility="gone"
app:tint="@color/double_red" />
<!-- FORM VIEW -->
<LinearLayout
android:id="@+id/layoutForm"