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

View File

@@ -20,9 +20,8 @@ import com.aldo.apps.ochecompanion.utils.DartsConstants;
import com.aldo.apps.ochecompanion.utils.UIConstants; import com.aldo.apps.ochecompanion.utils.UIConstants;
import com.google.android.material.button.MaterialButton; import com.google.android.material.button.MaterialButton;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.UUID;
/** /**
* Main game activity for playing X01 darts games (501, 301, etc.). * 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"; 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 // Game Logic State
// ======================================================================================== // ========================================================================================
/**
* The matches UUID from the database.
*/
private String mMatchUuid;
/** /**
* Index of the current active player (0 to playerCount-1). * Index of the current active player (0 to playerCount-1).
* Cycles through players as turns complete. * Cycles through players as turns complete.
@@ -151,6 +161,7 @@ public class GameActivity extends AppCompatActivity {
Intent intent = new Intent(context, GameActivity.class); Intent intent = new Intent(context, GameActivity.class);
intent.putParcelableArrayListExtra(EXTRA_PLAYERS, players); intent.putParcelableArrayListExtra(EXTRA_PLAYERS, players);
intent.putExtra(EXTRA_START_SCORE, startScore); intent.putExtra(EXTRA_START_SCORE, startScore);
intent.putExtra(EXTRA_MATCH_UUID, UUID.randomUUID().toString());
context.startActivity(intent); context.startActivity(intent);
} }
@@ -166,6 +177,7 @@ public class GameActivity extends AppCompatActivity {
// Extract game parameters from intent // Extract game parameters from intent
mStartingScore = getIntent().getIntExtra(EXTRA_START_SCORE, DartsConstants.DEFAULT_GAME_SCORE); mStartingScore = getIntent().getIntExtra(EXTRA_START_SCORE, DartsConstants.DEFAULT_GAME_SCORE);
mMatchUuid = getIntent().getStringExtra(EXTRA_MATCH_UUID);
ArrayList<Player> participants = getIntent().getParcelableArrayListExtra(EXTRA_PLAYERS); ArrayList<Player> participants = getIntent().getParcelableArrayListExtra(EXTRA_PLAYERS);
// Initialize activity components in order // Initialize activity components in order
@@ -377,6 +389,15 @@ public class GameActivity extends AppCompatActivity {
// Update UI for next player // Update UI for next player
updateUI(); updateUI();
updateTurnIndicators(); 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.os.Bundle;
import android.view.View; import android.view.View;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.EdgeToEdge; import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity; 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.AppDatabase;
import com.aldo.apps.ochecompanion.database.objects.Player; 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.MatchRecapView;
import com.aldo.apps.ochecompanion.ui.adapter.MainMenuPlayerAdapter; import com.aldo.apps.ochecompanion.ui.adapter.MainMenuPlayerAdapter;
import com.aldo.apps.ochecompanion.utils.DartsConstants; import com.aldo.apps.ochecompanion.utils.DartsConstants;
@@ -156,13 +155,28 @@ public class MainMenuActivity extends AppCompatActivity {
private void applyTestData(final int counter) { private void applyTestData(final int counter) {
// Create test player objects // Create test player objects
final Player playerOne = new Player(DartsConstants.TEST_PLAYER_1, null); final Player playerOne = new Player(DartsConstants.TEST_PLAYER_1, null);
playerOne.id = 1;
final Player playerTwo = new Player(DartsConstants.TEST_PLAYER_2, null); final Player playerTwo = new Player(DartsConstants.TEST_PLAYER_2, null);
playerTwo.id = 2;
final Player playerThree = new Player(DartsConstants.TEST_PLAYER_3, null); final Player playerThree = new Player(DartsConstants.TEST_PLAYER_3, null);
playerThree.id = 3;
final Player playerFour = new Player(DartsConstants.TEST_PLAYER_4, null); final Player playerFour = new Player(DartsConstants.TEST_PLAYER_4, null);
playerFour.id = 4;
// Create test match objects with different player configurations // Create score maps for test matches
final Match match1on1 = new Match(playerOne, playerTwo); final java.util.Map<Integer, Integer> scores1v1 = new java.util.HashMap<>();
final Match matchGroup = new Match(playerOne, playerTwo, playerThree, playerFour); 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 // Cycle through different test scenarios based on counter value
if (counter % UIConstants.TEST_CYCLE_MODULO == 0) { if (counter % UIConstants.TEST_CYCLE_MODULO == 0) {

View File

@@ -21,7 +21,7 @@ import com.aldo.apps.ochecompanion.database.objects.Player;
* @see Player * @see Player
* @see Match * @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 { public abstract class AppDatabase extends RoomDatabase {
/** /**

View File

@@ -1,6 +1,7 @@
package com.aldo.apps.ochecompanion.database.dao; package com.aldo.apps.ochecompanion.database.dao;
import androidx.room.Dao; import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert; import androidx.room.Insert;
import androidx.room.Query; import androidx.room.Query;
import androidx.room.Update; import androidx.room.Update;
@@ -34,6 +35,16 @@ public interface PlayerDao {
@Update @Update
void update(final Player player); 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. * Retrieves a player by their unique ID.
* Must be called on a background thread. * Must be called on a background thread.

View File

@@ -1,14 +1,23 @@
package com.aldo.apps.ochecompanion.database.objects; package com.aldo.apps.ochecompanion.database.objects;
import androidx.room.Entity; import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.PrimaryKey; import androidx.room.PrimaryKey;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.Serializable; 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. * Represents a completed darts match in the Oche Companion application.
* Room entity storing match information including game mode, timestamp, player count, * Room entity storing match information including game mode, timestamp, player count,
* and detailed performance data for all participants. Implements Serializable for * 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.dao.MatchDao
* @see com.aldo.apps.ochecompanion.database.objects.Player * @see com.aldo.apps.ochecompanion.database.objects.Player
@@ -25,7 +34,7 @@ public class Match implements Serializable {
* @see PrimaryKey * @see PrimaryKey
*/ */
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
public int mId; public int id;
/** /**
* Unix epoch timestamp (milliseconds) when match was completed. * Unix epoch timestamp (milliseconds) when match was completed.
@@ -33,20 +42,20 @@ public class Match implements Serializable {
* *
* @see System#currentTimeMillis() * @see System#currentTimeMillis()
*/ */
public long mTimestamp; public long timestamp;
/** /**
* Identifier for the darts game variant played (e.g., "501", "301", "Cricket"). * Identifier for the darts game variant played (e.g., "501", "301", "Cricket").
* Determines scoring rules and UI display for this match. * Determines scoring rules and UI display for this match.
*/ */
public String mGameMode; public String gameMode;
/** /**
* Total number of players who participated in this match. * Total number of players who participated in this match.
* Determines match type (1=solo, 2=1v1, 3+=group) and affects UI display. * Determines match type (1=solo, 2=1v1, 3+=group) and affects UI display.
* Must match the number of entries in participantData JSON. * 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. * 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.JSONArray
* @see org.json.JSONObject * @see org.json.JSONObject
*/ */
public String mParticipantData; public String participantData;
/** /**
* Constructs a new Match entity ready for database insertion. * 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) * @see com.aldo.apps.ochecompanion.database.dao.MatchDao#insert(Match)
*/ */
public Match(final long timestamp, final String gameMode, final int playerCount, final String participantData) { public Match(final long timestamp, final String gameMode, final int playerCount, final String participantData) {
this.mTimestamp = timestamp; this.timestamp = timestamp;
this.mGameMode = gameMode; this.gameMode = gameMode;
this.mPlayerCount = playerCount; this.playerCount = playerCount;
this.mParticipantData = participantData; 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.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.room.Entity; import androidx.room.Entity;
import androidx.room.PrimaryKey; import androidx.room.PrimaryKey;
@@ -18,32 +19,32 @@ public class Player implements Parcelable {
* Room auto-populates on insert. * Room auto-populates on insert.
*/ */
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
public int mId; public int id;
/** /**
* Player's display name shown throughout the app. Should be non-null and * Player's display name shown throughout the app. Should be non-null and
* ideally 1-30 characters. Supports Unicode. * 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). * File path to player's profile picture. Can be null (shows default avatar).
* Stored as path reference to avoid database bloat. * Stored as path reference to avoid database bloat.
*/ */
public String mProfilePictureUri; public String profilePictureUri;
/** /**
* Player's career three-dart average across all completed matches. * Player's career three-dart average across all completed matches.
* Represents overall skill level. Typical ranges: 0-40 (beginner), * Represents overall skill level. Typical ranges: 0-40 (beginner),
* 40-60 (casual), 60-80 (experienced), 80-100 (advanced), 100+ (pro). * 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 * Total number of completed matches for this player. Provides context for
* statistical significance and enables experience-based features. * 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 * 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) * @param profilePictureUri Path to profile image (null for default avatar)
*/ */
public Player(final String username, final String profilePictureUri) { public Player(final String username, final String profilePictureUri) {
this.mUsername = username; this.username = username;
this.mProfilePictureUri = profilePictureUri; this.profilePictureUri = profilePictureUri;
} }
@@ -65,17 +66,17 @@ public class Player implements Parcelable {
* @param in Parcel containing serialized Player data * @param in Parcel containing serialized Player data
*/ */
protected Player(final Parcel in) { protected Player(final Parcel in) {
mId = in.readInt(); id = in.readInt();
mUsername = in.readString(); username = in.readString();
mProfilePictureUri = in.readString(); profilePictureUri = in.readString();
mCareerAverage = in.readDouble(); careerAverage = in.readDouble();
mMatchesPlayed = in.readInt(); matchesPlayed = in.readInt();
} }
/** /**
* Required Parcelable CREATOR field for Player reconstruction. * 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 @Override
public Player createFromParcel(final Parcel in) { public Player createFromParcel(final Parcel in) {
return new Player(in); return new Player(in);
@@ -100,11 +101,11 @@ public class Player implements Parcelable {
*/ */
@Override @Override
public void writeToParcel(final Parcel dest, final int flags) { public void writeToParcel(final Parcel dest, final int flags) {
dest.writeInt(mId); dest.writeInt(id);
dest.writeString(mUsername); dest.writeString(username);
dest.writeString(mProfilePictureUri); dest.writeString(profilePictureUri);
dest.writeDouble(mCareerAverage); dest.writeDouble(careerAverage);
dest.writeInt(mMatchesPlayed); dest.writeInt(matchesPlayed);
} }
/** /**
@@ -114,13 +115,14 @@ public class Player implements Parcelable {
* @return String in format "Player{mId=X, mUsername='...', ...}" * @return String in format "Player{mId=X, mUsername='...', ...}"
*/ */
@Override @Override
@NonNull
public String toString() { public String toString() {
return "Player{" + return "Player{" +
"mId=" + mId + "mId=" + id +
", mUsername='" + mUsername + '\'' + ", mUsername='" + username + '\'' +
", mProfilePictureUri='" + mProfilePictureUri + '\'' + ", mProfilePictureUri='" + profilePictureUri + '\'' +
", mCareerAverage=" + mCareerAverage + ", mCareerAverage=" + careerAverage +
", mMatchesPlayed=" + mMatchesPlayed + ", 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.graphics.RectF;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.View; import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.aldo.apps.ochecompanion.utils.UIConstants; 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. */ /** Path with CW outer rect and CCW inner rect creating transparent hole. */
private final Path mPath = new Path(); private final Path mPath = new Path();
/** Calculated side length of square crop box (80% of width). */
private float mBoxSize;
/** Constructor for programmatic instantiation. */ /** Constructor for programmatic instantiation. */
public CropOverlayView(final Context context) { public CropOverlayView(final Context context) {
super(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) { 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); super.onLayout(changed, left, top, right, bottom);
mBoxSize = getWidth() * UIConstants.CROP_BOX_SIZE_RATIO; /* Calculated side length of square crop box (80% of width). */
final float l = (getWidth() - mBoxSize) / 2; final float boxSize = getWidth() * UIConstants.CROP_BOX_SIZE_RATIO;
final float t = (getHeight() - mBoxSize) / 2; final float l = (getWidth() - boxSize) / 2;
mCropRect.set(l, t, l + mBoxSize, t + mBoxSize); final float t = (getHeight() - boxSize) / 2;
mCropRect.set(l, t, l + boxSize, t + boxSize);
mPath.reset(); mPath.reset();
mPath.addRect(0, 0, getWidth(), getHeight(), Path.Direction.CW); 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. */ /** Renders the overlay mask with transparent center cutout. */
@Override @Override
protected void onDraw(final Canvas canvas) { protected void onDraw(@NonNull final Canvas canvas) {
super.onDraw(canvas); super.onDraw(canvas);
canvas.drawPath(mPath, mMaskPaint); canvas.drawPath(mPath, mMaskPaint);
} }

View File

@@ -10,7 +10,7 @@ import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.aldo.apps.ochecompanion.R; 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; import com.aldo.apps.ochecompanion.ui.adapter.MainMenuGroupMatchAdapter;
/** /**
@@ -92,10 +92,10 @@ public class MatchRecapView extends FrameLayout {
updateVisibility(mState1v1); updateVisibility(mState1v1);
mTvP1Name.setText(match.getPlayerNameByPosition(0)); mTvP1Name.setText(match.getPlayerNameByPosition(0));
mTvP1Score.setText(String.valueOf(match.getPlayerAverageByPosition(0))); mTvP1Score.setText(String.valueOf(match.getPlayerScoreByPosition(0)));
mTvP2Name.setText(match.getPlayerNameByPosition(1)); 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. */ /** 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.R;
import com.aldo.apps.ochecompanion.database.objects.Player; 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.aldo.apps.ochecompanion.ui.PlayerItemView;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.google.android.material.imageview.ShapeableImageView; import com.google.android.material.imageview.ShapeableImageView;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator;
import java.util.List; import java.util.List;
/** /**
* RecyclerView adapter for displaying group match results in Main Menu. * 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> { 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"; private static final String TAG = "MainMenuGroupMatchAdapt";
/** List of players sorted by career average. */ /** 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. * Creates a new ViewHolder with a PlayerItemView.
@@ -62,24 +61,24 @@ public class MainMenuGroupMatchAdapter extends RecyclerView.Adapter<MainMenuGrou
*/ */
@Override @Override
public void onBindViewHolder(@NonNull final GroupMatchHolder holder, final int position) { public void onBindViewHolder(@NonNull final GroupMatchHolder holder, final int position) {
// Retrieve the player at this position and bind it to the holder // Retrieve the participant at this position and bind it to the holder
holder.setPlayer(mPlayersList.get(position)); holder.setParticipant(mParticipantsList.get(position));
} }
/** /**
* Returns the number of players. * Returns the number of participants.
* *
* @return Player count. * @return Participant count.
*/ */
@Override @Override
public int getItemCount() { 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") @SuppressLint("NotifyDataSetChanged")
public void updateMatch(final Match match) { 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."); Log.d(TAG, "updateMatch: match is null, aborting update.");
return; return;
} }
// Clear any existing player data // Clear any existing participant data
mPlayersList.clear(); mParticipantsList.clear();
if (match.getAllPlayers() == null || match.getAllPlayers().isEmpty()) { final List<Match.ParticipantData> participants = match.getAllParticipants();
Log.d(TAG, "updateMatch: No players found in the match, just clearing."); if (participants == null || participants.isEmpty()) {
Log.d(TAG, "updateMatch: No participants found in the match, just clearing.");
notifyDataSetChanged(); notifyDataSetChanged();
return; return;
} }
// Get all players from the match // Sort participants by match score (highest to lowest)
final List<Player> allPlayers = match.getAllPlayers(); participants.sort((p1, p2) -> Integer.compare(p2.score, p1.score));
// Sort players by career average (lowest to highest) // Add sorted participants to the display list
allPlayers.sort(new PlayerScoreComparator()); mParticipantsList.addAll(participants);
// Add sorted players to the display list
mPlayersList.addAll(allPlayers);
// Notify RecyclerView to refresh the display // Notify RecyclerView to refresh the display
notifyDataSetChanged(); 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 // Set player name
mPlayerNameView.setText(player.username); mPlayerNameView.setText(player.username);
// Format and set career average score // Display match score instead of career average
mPlayerScoreView.setText(String.format( mPlayerScoreView.setText(String.valueOf(score));
itemView.getContext().getString(R.string.txt_player_average_base),
player.careerAverage));
// Load profile picture or show default icon // Load profile picture or show default icon
if (player.profilePictureUri != null) { 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:layout_height="match_parent"
android:background="@color/midnight_black"> 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 --> <!-- FORM VIEW -->
<LinearLayout <LinearLayout
android:id="@+id/layoutForm" android:id="@+id/layoutForm"