Skip to content

commit 44e0991

abduznik edited this page May 23, 2026 · 1 revision

feat: overhaul Home UI with larger shelves and integrated Installed Games

Commit: 44e0991438483f44f49fa3386c443e49255d3951

Author: abduznik

Date: 2026-04-18

Why: Adds a new feature or capability to the application.

Files Changed

lib/ui/screens/library_screen.dart      | 447 ++++++++++++++++----------------
 lib/ui/widgets/platform_filter_bar.dart |  24 --
 2 files changed, 220 insertions(+), 251 deletions(-)
  • lib/ui/screens/library_screen.dart
  • lib/ui/widgets/platform_filter_bar.dart

Diff

diff --git a/lib/ui/screens/library_screen.dart b/lib/ui/screens/library_screen.dart
index 64b9712..f34e68f 100644
--- a/lib/ui/screens/library_screen.dart
+++ b/lib/ui/screens/library_screen.dart
@@ -139,6 +139,92 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen> with LibraryActio
     ref.read(downloadedGamesCacheProvider.notifier).refresh();
   }
 
+  Widget _buildHorizontalShelf({
+    required BuildContext context,
+    required String title,
+    required IconData icon,
+    required List<Game> games,
+    required WidgetRef ref,
+  }) {
+    if (games.isEmpty) return const SizedBox.shrink();
+
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Padding(
+          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+          child: Row(
+            children: [
+              Icon(icon, size: 20, color: Theme.of(context).colorScheme.primary),
+              const SizedBox(width: 8),
+              Text(title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
+            ],
+          ),
+        ),
+        SizedBox(
+          height: 220, // Increased height for larger cards
+          child: ListView.builder(
+            scrollDirection: Axis.horizontal,
+            padding: const EdgeInsets.symmetric(horizontal: 12),
+            itemCount: games.length,
+            itemBuilder: (context, index) {
+              final game = games[index];
+              final coverUrl = ref.read(rommServiceProvider)?.resolveCoverUrl(game);
+              return GestureDetector(
+                onTap: () => _handleGameTap(context, ref, game),
+                child: Container(
+                  width: 150, // Increased width for larger cards
+                  margin: const EdgeInsets.only(right: 12),
+                  child: Column(
+                    crossAxisAlignment: CrossAxisAlignment.start,
+                    children: [
+                      Expanded(
+                        child: ClipRRect(
+                          borderRadius: BorderRadius.circular(12),
+                          child: Stack(
+                            children: [
+                              coverUrl != null
+                                  ? CachedNetworkImage(
+                                      imageUrl: coverUrl,
+                                      fit: BoxFit.cover,
+                                      width: 150,
+                                      height: 200,
+                                      placeholder: (context, url) => Container(color: Colors.grey[900]),
+                                      errorWidget: (c, u, e) => Container(
+                                        color: Colors.grey[800],
+                                        child: const Icon(Icons.sports_esports, size: 40),
+                                      ),
+                                    )
+                                  : Container(
+                                      color: Colors.grey[800],
+                                      child: const Icon(Icons.sports_esports, size: 40),
+                                    ),
+                            ],
+                          ),
+                        ),
+                      ),
+                      const SizedBox(height: 8),
+                      Text(
+                        game.displayName,
+                        maxLines: 1,
+                        overflow: TextOverflow.ellipsis,
+                        style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
+                      ),
+                      Text(
+                        game.platformDisplayName ?? '',
+                        style: TextStyle(fontSize: 11, color: Colors.grey[500]),
+                      ),
+                    ],
+                  ),
+                ),
+              );
+            },
+          ),
+        ),
+      ],
+    );
+  }
+
   @override
   Widget build(BuildContext context) {
     final platformsAsync = ref.watch(platformsProvider);
@@ -153,7 +239,6 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen> with LibraryActio
     final directoryServiceAsync = ref.watch(directoryServiceProvider);
     final downloadedCache = ref.watch(downloadedGamesCacheProvider);
     final isSyncing = ref.watch(downloadedGamesCacheProvider.notifier).isSyncing;
-    final activeFilters = ref.watch(activeFiltersProvider);
     final isHomeSelected = ref.watch(isHomeSelectedProvider);
 
     // Trigger initial load once service becomes available
@@ -176,8 +261,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen> with LibraryActio
       data: (config) {
         final uri = Uri.tryParse(config.baseUrl);
         final host = uri?.host ?? config.baseUrl;
-        final gameCountStr = ' • ${paginatedState.total} games';
-        return 'Freegosy • $host$gameCountStr';
+        return 'Freegosy • $host';
       },
       loading: () => 'Freegosy',
       error: (e, s) => 'Freegosy',
@@ -224,20 +308,15 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen> with LibraryActio
               );
             },
           ),
-          // Random game button
-          Consumer(
-            builder: (context, ref, _) {
-              return IconButton(
-                icon: const Icon(Icons.shuffle),
-                tooltip: 'Random game',
-                onPressed: () async {
-                  final service = ref.read(rommServiceProvider);
-                  if (service == null) return;
-                  final game = await service.getRandomGame();
-                  if (game == null) return;
-                  if (context.mounted) _handleGameTap(context, ref, game);
-                },
-              );
+          IconButton(
+            icon: const Icon(Icons.shuffle),
+            tooltip: 'Random game',
+            onPressed: () async {
+              final service = ref.read(rommServiceProvider);
+              if (service == null) return;
+              final game = await service.getRandomGame();
+              if (game == null) return;
+              if (context.mounted) _handleGameTap(context, ref, game);
             },
           ),
         ],
@@ -280,7 +359,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen> with LibraryActio
                 data: (platforms) => PlatformFilterBar(
                   platforms: platforms,
                   selectedPlatformId: selectedPlatformId,
-                  downloadedOnly: activeFilters.downloadedOnly,
+                  downloadedOnly: false, // Legacy, unused
                   isHome: isHomeSelected,
                   onSelected: (platform) {
                     ref.read(isHomeSelectedProvider.notifier).state = false;
@@ -290,15 +369,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen> with LibraryActio
                     ref.read(isHomeSelectedProvider.notifier).state = true;
                     ref.read(selectedPlatformIdProvider.notifier).state = null;
                   },
-                  onDownloadedToggle: (selected) {
-                    final notifier = ref.read(activeFiltersProvider.notifier);
-                    notifier.state = notifier.state.copyWith(downloadedOnly: selected);
-                    // Refresh games list with new filter
-                    ref.read(paginatedGamesProvider.notifier).loadInitial(
-                      platformId: selectedPlatformId?.toString(),
-                      search: _searchController.text.isEmpty ? null : _searchController.text,
-                    );
-                  },
+                  onDownloadedToggle: (_) {}, // Legacy, unused
                 ),
                 loading: () => const LinearProgressIndicator(),
                 error: (e, s) => Text('Error loading platforms: $e'),
@@ -338,212 +409,134 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen> with LibraryActio
               Expanded(
                 child: paginatedState.isLoading
                     ? buildSkeletonGrid(cardAspectRatio, columnCount, cardSpacing, context)
-                    : (paginatedState.error != null && !paginatedState.error!.contains('Offline Mode'))
-                        ? Center(
-                            child: Column(
-                              mainAxisAlignment: MainAxisAlignment.center,
-                              children: [
-                                const Icon(Icons.error_outline, color: Colors.red, size: 48),
-                                const SizedBox(height: 16),
-                                Text(
-                                  'Error: ${paginatedState.error}',
-                                  style: const TextStyle(color: Colors.red),
-                                  textAlign: TextAlign.center,
-                                ),
-                                const SizedBox(height: 16),
-                                ElevatedButton(
-                                  onPressed: () {
-                                    ref.invalidate(rommServiceProvider);
-                                    ref.read(paginatedGamesProvider.notifier).loadInitial(
-                                      platformId: ref.read(selectedPlatformIdProvider)?.toString(),
-                                      search: ref.read(searchQueryProvider),
-                                    );
-                                  },
-                                  child: const Text('Retry'),
+                    : RefreshIndicator(
+                        key: _refreshIndicatorKey,
+                        onRefresh: _refreshLibrary,
+                        child: Consumer(
+                          builder: (context, ref, _) {
+                            final isHome = ref.watch(isHomeSelectedProvider);
+                            final recentAsync = ref.watch(recentlyPlayedProvider);
+                            final metadataCacheAsync = ref.watch(metadataCacheServiceProvider);
+                            final displayGames = paginatedState.games;
+
+                            if (isHome) {
+                              return SingleChildScrollView(
+                                physics: const AlwaysScrollableScrollPhysics(),
+                                child: Column(
+                                  children: [
+                                    // 1. Continue Playing
+                                    recentAsync.when(
+                                      loading: () => const SizedBox.shrink(),
+                                      error: (e, s) => const SizedBox.shrink(),
+                                      data: (games) => _buildHorizontalShelf(
+                                        context: context,
+                                        title: 'Continue Playing',
+                                        icon: Icons.play_circle_outline,
+                                        games: games,
+                                        ref: ref,
+                                      ),
+                                    ),
+                                    // 2. Recently Added
+                                    _buildHorizontalShelf(
+                                      context: context,
+                                      title: 'Recently Added',
+                                      icon: Icons.new_releases_outlined,
+                                      games: displayGames.take(15).toList(),
+                                      ref: ref,
+                                    ),
+                                    // 3. Installed Games
+                                    metadataCacheAsync.when(
+                                      loading: () => const SizedBox.shrink(),
+                                      error: (e, s) => const SizedBox.shrink(),
+                                      data: (cache) {
+                                        final installed = cache.cachedGames
+                                            .where((g) => downloadedCache[g.id] == true)
+                                            .toList();
+                                        return _buildHorizontalShelf(
+                                          context: context,
+                                          title: 'Installed Games',
+                                          icon: Icons.download_done,
+                                          games: installed,
+                                          ref: ref,
+                                        );
+                                      },
+                                    ),
+                                    const SizedBox(height: 32),
+                                  ],
                                 ),
-                              ],
-                            ),
-                          )
-                        : RefreshIndicator(
-                            key: _refreshIndicatorKey,
-                            onRefresh: _refreshLibrary,
-                            child: Consumer(
-                              builder: (context, ref, _) {
-                                final isHome = ref.watch(isHomeSelectedProvider);
-                                
-                                final recentAsync = ref.watch(recentlyPlayedProvider);
-                                final displayGames = paginatedState.games;
+                              );
+                            }
 
-                                if (displayGames.isEmpty && !isHome) {
-                                  return const CustomScrollView(slivers: [
-                                    SliverFillRemaining(child: Center(child: Text('No games found'))),
-                                  ]);
-                                }
+                            if (displayGames.isEmpty) {
+                              return const Center(child: Text('No games found'));
+                            }
 
-                                return CustomScrollView(
-                                  controller: _scrollController,
-                                  physics: const AlwaysScrollableScrollPhysics(),
-                                  slivers: [
-                                    if (isHome) ...[
-                                      SliverToBoxAdapter(
-                                        child: recentAsync.when(
-                                          loading: () => const SizedBox.shrink(),
-                                          error: (e, s) => const SizedBox.shrink(),
-                                          data: (games) {
-                                            if (games.isEmpty) return const SizedBox.shrink();
-                                            return Column(
-                                              crossAxisAlignment: CrossAxisAlignment.start,
-                                              children: [
-                                                Padding(
-                                                  padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
-                                                  child: Row(
-                                                    children: [
-                                                      const Icon(Icons.play_circle_outline, size: 18),
-                                                      const SizedBox(width: 6),
-                                                      const Text('Continue Playing',
-                                                          style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
-                                                    ],
-                                                  ),
-                                                ),
-                                                SizedBox(
-                                                  height: 160,
-                                                  child: ListView.builder(
-                                                    scrollDirection: Axis.horizontal,
-                                                    padding: const EdgeInsets.symmetric(horizontal: 12),
-                                                    itemCount: games.length,
-                                                    itemBuilder: (context, index) {
-                                                      final game = games[index];
-                                                      final coverUrl = ref.read(rommServiceProvider)?.resolveCoverUrl(game);
-                                                      return GestureDetector(
-                                                        onTap: () => _handleGameTap(context, ref, game),
-                                                        child: Container(
-                                                          width: 100,
-                                                          margin: const EdgeInsets.only(right: 8),
-                                                          child: Column(
-                                                            crossAxisAlignment: CrossAxisAlignment.start,
-                                                            children: [
-                                                              Expanded(
-                                                                child: ClipRRect(
-                                                                  borderRadius: BorderRadius.circular(8),
-                                                                  child: coverUrl != null
-                                                                      ? CachedNetworkImage(
-                                                                          imageUrl: coverUrl,
-                                                                          fit: BoxFit.cover,
-                                                                          width: 100,
-                                                                          errorWidget: (c, u, e) => Container(
-                                                                            color: Colors.grey[800],
-                                                                            child: const Icon(Icons.sports_esports),
-                                                                          ),
-                                                                        )
-                                                                      : Container(
-                                                                          color: Colors.grey[800],
-                                                                          child: const Icon(Icons.sports_esports),
-                                                                        ),
-                                                                ),
-                                                              ),
-                                                              const SizedBox(height: 4),
-                                                              Text(
-                                                                game.displayName,
-                                                                maxLines: 1,
-                                                                overflow: TextOverflow.ellipsis,
-                                                                style: const TextStyle(fontSize: 11),
-                                                              ),
-                                                            ],
-                                                          ),
-                                                        ),
-                                                      );
-                                                    },
-                                                  ),
-                                                ),
-                                                const SizedBox(height: 8),
-                                              ],
-                                            );
-                                          },
-                                        ),
-                                      ),
-                                      SliverToBoxAdapter(
-                                        child: Padding(
-                                          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
-                                          child: Row(
-                                            children: [
-                                              const Icon(Icons.new_releases_outlined, size: 18),
-                                              const SizedBox(width: 6),
-                                              const Text('Recently Added',
-                                                  style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
-                                            ],
-                                          ),
-                                        ),
+                            return CustomScrollView(
+                              controller: _scrollController,
+                              physics: const AlwaysScrollableScrollPhysics(),
+                              slivers: [
+                                SliverToBoxAdapter(
+                                  child: Padding(
+                                    padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0),
+                                    child: Align(
+                                      alignment: Alignment.centerRight,
+                                      child: Text(
+                                        'Showing ${displayGames.length} of ${paginatedState.total} games',
+                                        style: const TextStyle(fontSize: 12, color: Colors.grey),
                                       ),
-                                    ],
-                                    if (!isHome)
-                                      SliverToBoxAdapter(
-                                        child: Padding(
-                                          padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 2.0),
-                                          child: Align(
-                                            alignment: Alignment.centerRight,
-                                            child: Text(
-                                              'Showing ${displayGames.length} of ${paginatedState.total} games',
-                                              style: const TextStyle(fontSize: 12, color: Colors.grey),
-                                            ),
+                                    ),
+                                  ),
+                                ),
+                                SliverPadding(
+                                  padding: const EdgeInsets.all(12),
+                                  sliver: SliverGrid(
+                                    gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
+                                      crossAxisCount: columnCount,
+                                      crossAxisSpacing: cardSpacing,
+                                      mainAxisSpacing: cardSpacing,
+                                      mainAxisExtent: calculateCardHeight(columnCount, cardSpacing, cardAspectRatio, context),
+                                    ),
+                                    delegate: SliverChildBuilderDelegate(
+                                      (context, index) {
+                                        if (index == displayGames.length) {
+                                          return const Center(
+                                              child: Padding(
+                                            padding: EdgeInsets.all(16),
+                                            child: CircularProgressIndicator(),
+                                          ));
+                                        }
+                                        final game = displayGames[index];
+                                        final isDownloaded = downloadedCache[game.id] ?? false;
+                                        final coverUrl = ref.read(rommServiceProvider)?.resolveCoverUrl(game);
+                                        
+                                        return GestureDetector(
+                                          onTap: () => _handleGameTap(context, ref, game),
+                                          child: GameCard(
+                                            game: game,
+                                            coverUrl: coverUrl,
+                                            isDownloaded: isDownloaded,
+                                            platformLogoUrl: game.platformSlug != null
+                                                ? '${ref.read(rommConfigProvider).value?.baseUrl ?? ''}/assets/platforms/${game.platformSlug}.svg'
+                                                : null,
+                                            showTitle: showTitle,
+                                            showButtonsOnHover: showButtonsOnHover,
+                                            onDownload: () => startDownload(context, ref, game),
+                                            onLaunch: () => handleLaunch(context, ref, game),
+                                            onDelete: () => handleDeleteRom(context, ref, game),
+                                            onPushSaves: () => handlePushSaves(context, ref, game),
+                                            onPullSaves: () => handlePullSaves(context, ref, game),
                                           ),
-                                        ),
-                                      ),
-                                    SliverPadding(
-                                      padding: const EdgeInsets.all(12),
-                                      sliver: SliverGrid(
-                                        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
-                                          crossAxisCount: columnCount,
-                                          crossAxisSpacing: cardSpacing,
-                                          mainAxisSpacing: cardSpacing,
-                                          mainAxisExtent: calculateCardHeight(columnCount, cardSpacing, cardAspectRatio, context),
-                                        ),
-                                        delegate: SliverChildBuilderDelegate(
-                                          (context, index) {
-                                            if (index == displayGames.length && !isHome) {
-                                              return const Center(
-                                                  child: Padding(
-                                                padding: EdgeInsets.all(16),
-                                                child: CircularProgressIndicator(),
-                                              ));
-                                            }
-                                            final game = displayGames[index];
-                                            final isDownloaded = downloadedCache[game.id] ?? false;
-                                            final isWindowsGame = ['windows', 'pc', 'win'].contains(game.platformSlug?.toLowerCase() ?? '');
-                                            final coverUrl = ref.read(rommServiceProvider)?.resolveCoverUrl(game);
-                                            
-                                            return GestureDetector(
-                                              onTap: () => _handleGameTap(context, ref, game),
-                                              child: GestureDetector(
-                                                onLongPress: isWindowsGame ? () => handleWindowsConfig(context, ref, game) : null,
-                                                child: GameCard(
-                                                  game: game,
-                                                  coverUrl: coverUrl,
-                                                  isDownloaded: isDownloaded,
-                                                  platformLogoUrl: game.platformSlug != null
-                                                      ? '${ref.read(rommConfigProvider).value?.baseUrl ?? ''}/assets/platforms/${game.platformSlug}.svg'
-                                                      : null,
-                                                  showTitle: showTitle,
-                                                  showButtonsOnHover: showButtonsOnHover,
-                                                  onDownload: () => startDownload(context, ref, game),
-                                                  onLaunch: () => handleLaunch(context, ref, game),
-                                                  onDelete: () => handleDeleteRom(context, ref, game),
-                                                  onPushSaves: () => handlePushSaves(context, ref, game),
-                                                  onPullSaves: () => handlePullSaves(context, ref, game),
-                                                ),
-                                              ),
-                                            );
-                                          },
-                                          childCount: isHome 
-                                              ? (displayGames.length > 10 ? 10 : displayGames.length)
-                                              : displayGames.length + (paginatedState.isLoadingMore ? 1 : 0),
-                                        ),
-                                      ),
+                                        );
+                                      },
+                                      childCount: displayGames.length + (paginatedState.isLoadingMore ? 1 : 0),
                                     ),
-                                  ],
-                                );
-                              },
-                            ),
-                          ),
+                                  ),
+                                ),
+                              ],
+                            );
+                          },
+                        ),
+                      ),
               ),
             ],
           ),
diff --git a/lib/ui/widgets/platform_filter_bar.dart b/lib/ui/widgets/platform_filter_bar.dart
index e288819..c01f367 100644
--- a/lib/ui/widgets/platform_filter_bar.dart
+++ b/lib/ui/widgets/platform_filter_bar.dart
@@ -5,20 +5,16 @@ import '../../core/romm/romm_models.dart';
 class PlatformFilterBar extends StatelessWidget {
   final List<Platform> platforms;
   final int? selectedPlatformId;
-  final bool downloadedOnly;
   final bool isHome;
   final Function(Platform?) onSelected;
-  final Function(bool) onDownloadedToggle;
   final VoidCallback onHomeSelected;
 
   const PlatformFilterBar({
     super.key,
     required this.platforms,
     required this.selectedPlatformId,
-    required this.downloadedOnly,
     required this.isHome,
     required this.onSelected,
-    required this.onDownloadedToggle,
     required this.onHomeSelected,
   });
 
@@ -74,26 +70,6 @@ class PlatformFilterBar extends StatelessWidget {
                 side: isAllSelected ? null : BorderSide(color: colorScheme.outline.withValues(alpha: 0.5)),
               ),
             ),
-            Padding(
-              padding: const EdgeInsets.only(right: 8.0),
-              child: FilterChip(
-                avatar: Icon(
-                  Icons.download_done,
-                  size: 16,
-                  color: downloadedOnly ? Colors.white : colorScheme.primary,
-                ),
-                label: const Text('Downloaded'),
-                selected: downloadedOnly,
-                onSelected: (selected) {
-                  onDownloadedToggle(selected);
-                },
-                backgroundColor: downloadedOnly ? colorScheme.secondary : null,
-                labelStyle: TextStyle(
-                  color: downloadedOnly ? Colors.white : colorScheme.onSurface,
-                ),
-                side: downloadedOnly ? null : BorderSide(color: colorScheme.outline.withValues(alpha: 0.5)),
-              ),
-            ),
             ...platforms.map((platform) {
               final isSelected = selectedPlatformId == platform.id && !isHome;
               return Padding(

Clone this wiki locally