peer count for all games
This commit is contained in:
@@ -64,6 +64,7 @@ impl From<EtiGame> for Game {
|
||||
installed: false,
|
||||
eti_game_version: None,
|
||||
local_version: None,
|
||||
peer_count: 0, // ETI games start with 0 peers until peer system discovers them
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,8 @@ pub struct Game {
|
||||
pub eti_game_version: Option<String>,
|
||||
/// Local game version from version.ini (YYYYMMDD format)
|
||||
pub local_version: Option<String>,
|
||||
/// Number of peers that have this game available
|
||||
pub peer_count: u32,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Game {
|
||||
|
||||
@@ -204,6 +204,16 @@ impl PeerGameDB {
|
||||
#[must_use]
|
||||
pub fn get_all_games(&self) -> Vec<Game> {
|
||||
let mut aggregated: HashMap<String, Game> = HashMap::new();
|
||||
let mut peer_counts: HashMap<String, u32> = HashMap::new();
|
||||
|
||||
// Count peers per game
|
||||
for peer in self.peers.values() {
|
||||
for game_id in peer.games.keys() {
|
||||
*peer_counts.entry(game_id.clone()).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Aggregate games with peer counts
|
||||
for peer in self.peers.values() {
|
||||
for game in peer.games.values() {
|
||||
aggregated
|
||||
@@ -218,8 +228,14 @@ impl PeerGameDB {
|
||||
} else if existing.eti_game_version.is_none() {
|
||||
existing.eti_game_version.clone_from(&game.eti_game_version);
|
||||
}
|
||||
// Update peer count
|
||||
existing.peer_count = peer_counts[&game.id];
|
||||
})
|
||||
.or_insert_with(|| game.clone());
|
||||
.or_insert_with(|| {
|
||||
let mut game_clone = game.clone();
|
||||
game_clone.peer_count = peer_counts[&game.id];
|
||||
game_clone
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1109,6 +1125,7 @@ async fn load_local_game_db(game_dir: &str) -> eyre::Result<GameDB> {
|
||||
installed: true,
|
||||
eti_game_version: version.clone(),
|
||||
local_version: version,
|
||||
peer_count: 0, // Local games start with 0 peers
|
||||
};
|
||||
games.push(game);
|
||||
}
|
||||
|
||||
@@ -226,5 +226,61 @@ h1.align-center {
|
||||
}
|
||||
|
||||
.item-info.error {
|
||||
color: #ff6666;
|
||||
color: #ff6666;
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.filter-button {
|
||||
padding: 8px 16px;
|
||||
background: linear-gradient(45deg, #09305a, #37529c);
|
||||
color: #D5DBFE;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 8px rgba(0, 191, 255, 0.2);
|
||||
}
|
||||
|
||||
.filter-button:hover {
|
||||
background: linear-gradient(45deg, #09305a, #4866b9);
|
||||
box-shadow: 0 8px 12px rgba(0, 191, 255, 0.4);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.filter-button.active {
|
||||
background: linear-gradient(45deg, #09305a, #4866b9);
|
||||
border: 1px solid rgba(0, 191, 255, 0.6);
|
||||
box-shadow: 0 8px 12px rgba(0, 191, 255, 0.4);
|
||||
}
|
||||
|
||||
.item-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
min-height: 18px;
|
||||
margin: 8px 10px 16px;
|
||||
font-size: 0.85em;
|
||||
color: #8892b0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.status-left {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.status-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.peer-count {
|
||||
font-weight: bold;
|
||||
color: #4866b9;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ enum InstallStatus {
|
||||
|
||||
type StatusLevel = 'info' | 'error';
|
||||
|
||||
type GameFilter = 'all' | 'available' | 'installed';
|
||||
|
||||
interface Game {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -33,15 +35,29 @@ interface Game {
|
||||
local_version?: string;
|
||||
status_message?: string;
|
||||
status_level?: StatusLevel;
|
||||
peer_count: number;
|
||||
}
|
||||
|
||||
const App = () => {
|
||||
const [gameItems, setGameItems] = useState<Game[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [gameDir, setGameDir] = useState('');
|
||||
const [currentFilter, setCurrentFilter] = useState<GameFilter>('all');
|
||||
const checkingPeersTimeouts = useRef<Record<string, ReturnType<typeof setTimeout>>>({});
|
||||
|
||||
const filteredGames = gameItems.filter(item =>
|
||||
const getFilteredGames = (games: Game[], filter: GameFilter): Game[] => {
|
||||
switch (filter) {
|
||||
case 'available':
|
||||
return games.filter(game => game.peer_count > 0);
|
||||
case 'installed':
|
||||
return games.filter(game => game.installed);
|
||||
case 'all':
|
||||
default:
|
||||
return games;
|
||||
}
|
||||
};
|
||||
|
||||
const filteredAndSearchedGames = getFilteredGames(gameItems, currentFilter).filter(item =>
|
||||
item.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
@@ -211,6 +227,7 @@ const App = () => {
|
||||
install_status: installStatus,
|
||||
status_message: previous?.status_message,
|
||||
status_level: previous?.status_level,
|
||||
peer_count: game.peer_count ?? 0, // Ensure peer_count is always set
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -381,20 +398,42 @@ const App = () => {
|
||||
<h1 className="align-center">SoftLAN Launcher</h1>
|
||||
<div className="main-header">
|
||||
{gameDir ? (
|
||||
<div className="search-settings-wrapper">
|
||||
<div></div>
|
||||
<div className="search-container">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search games..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="search-input"
|
||||
/>
|
||||
<div>
|
||||
<div className="filter-container">
|
||||
<button
|
||||
className={`filter-button ${currentFilter === 'all' ? 'active' : ''}`}
|
||||
onClick={() => setCurrentFilter('all')}
|
||||
>
|
||||
All Games
|
||||
</button>
|
||||
<button
|
||||
className={`filter-button ${currentFilter === 'available' ? 'active' : ''}`}
|
||||
onClick={() => setCurrentFilter('available')}
|
||||
>
|
||||
Available
|
||||
</button>
|
||||
<button
|
||||
className={`filter-button ${currentFilter === 'installed' ? 'active' : ''}`}
|
||||
onClick={() => setCurrentFilter('installed')}
|
||||
>
|
||||
Installed
|
||||
</button>
|
||||
</div>
|
||||
<div className="settings-container">
|
||||
<button onClick={dialogGameDir} className="settings-button">Set Game Directory</button>
|
||||
<span className="settings-text">{gameDir}</span>
|
||||
<div className="search-settings-wrapper">
|
||||
<div></div>
|
||||
<div className="search-container">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search games..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="search-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="settings-container">
|
||||
<button onClick={dialogGameDir} className="settings-button">Set Game Directory</button>
|
||||
<span className="settings-text">{gameDir}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -410,16 +449,16 @@ const App = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid-container">
|
||||
{gameDir && filteredGames.length === 0 && gameItems.length === 0 ? (
|
||||
{gameDir && filteredAndSearchedGames.length === 0 && gameItems.length === 0 ? (
|
||||
<div className="no-games-message">
|
||||
Scanning for games in your directory...
|
||||
</div>
|
||||
) : gameDir && filteredGames.length === 0 && gameItems.length > 0 ? (
|
||||
) : gameDir && filteredAndSearchedGames.length === 0 && gameItems.length > 0 ? (
|
||||
<div className="no-games-message">
|
||||
No games found matching your search.
|
||||
No games found matching your search and filters.
|
||||
</div>
|
||||
) : null}
|
||||
{filteredGames.map((item) => {
|
||||
{filteredAndSearchedGames.map((item) => {
|
||||
const uint8Array = new Uint8Array(item.thumbnail);
|
||||
const binaryString = uint8Array.reduce((acc, byte) => acc + String.fromCharCode(byte), '');
|
||||
const thumbnailUrl = `data:image/jpeg;base64,${btoa(binaryString)}`;
|
||||
@@ -454,7 +493,16 @@ const App = () => {
|
||||
: 'Play'}
|
||||
</div>
|
||||
<div className={`item-info${item.status_level ? ` ${item.status_level}` : ''}`}>
|
||||
{item.status_message ?? ''}
|
||||
<div className="status-left">
|
||||
{item.status_message && item.peer_count === 0 && !item.installed ? item.status_message : ''}
|
||||
</div>
|
||||
<div className="status-right">
|
||||
{item.peer_count > 0 && (
|
||||
<span className="peer-count">
|
||||
👥 {item.peer_count}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user