peer count for all games

This commit is contained in:
2025-11-13 19:38:21 +01:00
parent d96d191c13
commit 2d7f7513ad
5 changed files with 145 additions and 21 deletions
+1
View File
@@ -64,6 +64,7 @@ impl From<EtiGame> for Game {
installed: false, installed: false,
eti_game_version: None, eti_game_version: None,
local_version: None, local_version: None,
peer_count: 0, // ETI games start with 0 peers until peer system discovers them
} }
} }
} }
+2
View File
@@ -60,6 +60,8 @@ pub struct Game {
pub eti_game_version: Option<String>, pub eti_game_version: Option<String>,
/// Local game version from version.ini (YYYYMMDD format) /// Local game version from version.ini (YYYYMMDD format)
pub local_version: Option<String>, pub local_version: Option<String>,
/// Number of peers that have this game available
pub peer_count: u32,
} }
impl fmt::Debug for Game { impl fmt::Debug for Game {
+18 -1
View File
@@ -204,6 +204,16 @@ impl PeerGameDB {
#[must_use] #[must_use]
pub fn get_all_games(&self) -> Vec<Game> { pub fn get_all_games(&self) -> Vec<Game> {
let mut aggregated: HashMap<String, Game> = HashMap::new(); 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 peer in self.peers.values() {
for game in peer.games.values() { for game in peer.games.values() {
aggregated aggregated
@@ -218,8 +228,14 @@ impl PeerGameDB {
} else if existing.eti_game_version.is_none() { } else if existing.eti_game_version.is_none() {
existing.eti_game_version.clone_from(&game.eti_game_version); 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, installed: true,
eti_game_version: version.clone(), eti_game_version: version.clone(),
local_version: version, local_version: version,
peer_count: 0, // Local games start with 0 peers
}; };
games.push(game); games.push(game);
} }
@@ -228,3 +228,59 @@ h1.align-center {
.item-info.error { .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;
}
+54 -6
View File
@@ -21,6 +21,8 @@ enum InstallStatus {
type StatusLevel = 'info' | 'error'; type StatusLevel = 'info' | 'error';
type GameFilter = 'all' | 'available' | 'installed';
interface Game { interface Game {
id: string; id: string;
name: string; name: string;
@@ -33,15 +35,29 @@ interface Game {
local_version?: string; local_version?: string;
status_message?: string; status_message?: string;
status_level?: StatusLevel; status_level?: StatusLevel;
peer_count: number;
} }
const App = () => { const App = () => {
const [gameItems, setGameItems] = useState<Game[]>([]); const [gameItems, setGameItems] = useState<Game[]>([]);
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [gameDir, setGameDir] = useState(''); const [gameDir, setGameDir] = useState('');
const [currentFilter, setCurrentFilter] = useState<GameFilter>('all');
const checkingPeersTimeouts = useRef<Record<string, ReturnType<typeof setTimeout>>>({}); 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()) item.name.toLowerCase().includes(searchTerm.toLowerCase())
); );
@@ -211,6 +227,7 @@ const App = () => {
install_status: installStatus, install_status: installStatus,
status_message: previous?.status_message, status_message: previous?.status_message,
status_level: previous?.status_level, status_level: previous?.status_level,
peer_count: game.peer_count ?? 0, // Ensure peer_count is always set
}; };
}); });
}); });
@@ -381,6 +398,27 @@ const App = () => {
<h1 className="align-center">SoftLAN Launcher</h1> <h1 className="align-center">SoftLAN Launcher</h1>
<div className="main-header"> <div className="main-header">
{gameDir ? ( {gameDir ? (
<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="search-settings-wrapper"> <div className="search-settings-wrapper">
<div></div> <div></div>
<div className="search-container"> <div className="search-container">
@@ -397,6 +435,7 @@ const App = () => {
<span className="settings-text">{gameDir}</span> <span className="settings-text">{gameDir}</span>
</div> </div>
</div> </div>
</div>
) : ( ) : (
<div className="no-directory-container"> <div className="no-directory-container">
<div className="no-directory-message"> <div className="no-directory-message">
@@ -410,16 +449,16 @@ const App = () => {
</div> </div>
</div> </div>
<div className="grid-container"> <div className="grid-container">
{gameDir && filteredGames.length === 0 && gameItems.length === 0 ? ( {gameDir && filteredAndSearchedGames.length === 0 && gameItems.length === 0 ? (
<div className="no-games-message"> <div className="no-games-message">
Scanning for games in your directory... Scanning for games in your directory...
</div> </div>
) : gameDir && filteredGames.length === 0 && gameItems.length > 0 ? ( ) : gameDir && filteredAndSearchedGames.length === 0 && gameItems.length > 0 ? (
<div className="no-games-message"> <div className="no-games-message">
No games found matching your search. No games found matching your search and filters.
</div> </div>
) : null} ) : null}
{filteredGames.map((item) => { {filteredAndSearchedGames.map((item) => {
const uint8Array = new Uint8Array(item.thumbnail); const uint8Array = new Uint8Array(item.thumbnail);
const binaryString = uint8Array.reduce((acc, byte) => acc + String.fromCharCode(byte), ''); const binaryString = uint8Array.reduce((acc, byte) => acc + String.fromCharCode(byte), '');
const thumbnailUrl = `data:image/jpeg;base64,${btoa(binaryString)}`; const thumbnailUrl = `data:image/jpeg;base64,${btoa(binaryString)}`;
@@ -454,7 +493,16 @@ const App = () => {
: 'Play'} : 'Play'}
</div> </div>
<div className={`item-info${item.status_level ? ` ${item.status_level}` : ''}`}> <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>
</div> </div>
); );