Skip to content

commit dcf36f5

abduznik edited this page May 23, 2026 · 1 revision

feat: add multi-disc picker support

Commit: dcf36f5abcfb52e059095d485dedbdce066ec341

Author: Yan

Date: 2026-04-04

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

Files Changed

lib/core/romm/romm_models.dart        |   9 ++
 lib/ui/widgets/multi_disc_picker.dart | 157 ++++++++++++++++++++++++++++++++++
 2 files changed, 166 insertions(+)
  • lib/core/romm/romm_models.dart
  • lib/ui/widgets/multi_disc_picker.dart

Diff

diff --git a/lib/core/romm/romm_models.dart b/lib/core/romm/romm_models.dart
index 6343af6..24bc27f 100644
--- a/lib/core/romm/romm_models.dart
+++ b/lib/core/romm/romm_models.dart
@@ -13,6 +13,7 @@ class Game {
   final int fileSize; // Kept
   final String? multiFilePath; // maps to 'multi_file_path' in JSON
   final bool hasMultipleFiles;
+  final List<Map<String, dynamic>> files; // Added: list of files for multi-disc games
 
   // New fields
   final String? summary;
@@ -72,6 +73,7 @@ class Game {
     required this.fileSize, // Kept
     this.multiFilePath,
     this.hasMultipleFiles = false,
+    this.files = const [], // Added
     this.summary,
     this.genres = const [],
     this.companies = const [],
@@ -108,6 +110,7 @@ class Game {
       fileSize: json['file_size_bytes'] is int ? json['file_size_bytes'] : 0,
       multiFilePath: json['multi_file_path']?.toString(),
       hasMultipleFiles: json['has_multiple_files'] as bool? ?? false,
+      files: (json['files'] as List<dynamic>?)?.map((e) => e as Map<String, dynamic>).toList() ?? [],
       summary: json['summary']?.toString(),
       genres: (json['metadatum']?['genres'] as List<dynamic>?)?.map((e) => e.toString()).toList() ?? [],
       companies: (json['metadatum']?['companies'] as List<dynamic>?)?.map((e) => e.toString()).toList() ?? [],
@@ -153,12 +156,16 @@ class Platform {
   final int id;
   final String name;
   final String slug;
+  final String fsSlug;
+  final String displayName;
   final int gamesCount;
 
   Platform({
     required this.id,
     required this.name,
     required this.slug,
+    this.fsSlug = '',
+    this.displayName = '',
     this.gamesCount = 0,
   });
 
@@ -167,6 +174,8 @@ class Platform {
       id: json['id'] as int? ?? 0,
       name: json['name']?.toString() ?? '',
       slug: json['slug']?.toString() ?? '',
+      fsSlug: json['fs_slug']?.toString() ?? '',
+      displayName: json['display_name']?.toString() ?? '',
       gamesCount: (json['rom_count'] as int?) ?? 
                   (json['roms_count'] as int?) ?? 
                   (json['games_count'] as int?) ?? 0,
diff --git a/lib/ui/widgets/multi_disc_picker.dart b/lib/ui/widgets/multi_disc_picker.dart
new file mode 100644
index 0000000..258fd3c
--- /dev/null
+++ b/lib/ui/widgets/multi_disc_picker.dart
@@ -0,0 +1,157 @@
+import 'package:flutter/material.dart';
+import '../../core/romm/romm_models.dart';
+
+class MultiDiscPicker extends StatelessWidget {
+  final Game game;
+  final List<Map<String, dynamic>> files; // list of file objects from game.files
+  final Function(Map<String, dynamic>) onSelect;
+
+  const MultiDiscPicker({
+    super.key,
+    required this.game,
+    required this.files,
+    required this.onSelect,
+  });
+
+  static Future<void> show(
+    BuildContext context, {
+    required Game game,
+    required List<Map<String, dynamic>> files,
+    required Function(Map<String, dynamic>) onSelect,
+  }) {
+    return showModalBottomSheet(
+      context: context,
+      backgroundColor: Colors.transparent,
+      builder: (context) => MultiDiscPicker(
+        game: game,
+        files: files,
+        onSelect: onSelect,
+      ),
+    );
+  }
+
+  String _formatSize(int? bytes) {
+    if (bytes == null) return '';
+    if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
+    if (bytes < 1024 * 1024 * 1024) return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
+    return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB';
+  }
+
+  String _discLabel(String filename, int index) {
+    // Try to detect disc number from filename
+    final lower = filename.toLowerCase();
+    final discMatch = RegExp(r'disc\s*(\d+)|disk\s*(\d+)|cd\s*(\d+)|part\s*(\d+)').firstMatch(lower);
+    if (discMatch != null) {
+      final num = discMatch.group(1) ?? discMatch.group(2) ?? discMatch.group(3) ?? discMatch.group(4);
+      return 'Disc $num';
+    }
+    return 'File ${index + 1}';
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      decoration: BoxDecoration(
+        color: Theme.of(context).colorScheme.surface,
+        borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
+      ),
+      child: Column(
+        mainAxisSize: MainAxisSize.min,
+        children: [
+          // Handle bar
+          Container(
+            margin: const EdgeInsets.only(top: 12),
+            width: 40,
+            height: 4,
+            decoration: BoxDecoration(
+              color: Colors.white24,
+              borderRadius: BorderRadius.circular(2),
+            ),
+          ),
+          // Header
+          Padding(
+            padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
+            child: Row(
+              children: [
+                const Icon(Icons.album_outlined, size: 20),
+                const SizedBox(width: 8),
+                Expanded(
+                  child: Column(
+                    crossAxisAlignment: CrossAxisAlignment.start,
+                    children: [
+                      const Text(
+                        'Select Disc',
+                        style: TextStyle(
+                          fontWeight: FontWeight.bold,
+                          fontSize: 16,
+                        ),
+                      ),
+                      Text(
+                        game.displayName,
+                        style: const TextStyle(
+                          color: Colors.white54,
+                          fontSize: 12,
+                        ),
+                        overflow: TextOverflow.ellipsis,
+                      ),
+                    ],
+                  ),
+                ),
+              ],
+            ),
+          ),
+          const Divider(color: Colors.white12),
+          // File list
+          ListView.builder(
+            shrinkWrap: true,
+            physics: const NeverScrollableScrollPhysics(),
+            itemCount: files.length,
+            itemBuilder: (context, index) {
+              final file = files[index];
+              final filename = file['file_name']?.toString() ?? file['name']?.toString() ?? 'File ${index + 1}';
+              final size = file['file_size_bytes'] as int?;
+              final label = _discLabel(filename, index);
+
+              return ListTile(
+                leading: Container(
+                  width: 40,
+                  height: 40,
+                  decoration: BoxDecoration(
+                    color: Theme.of(context).colorScheme.primaryContainer,
+                    borderRadius: BorderRadius.circular(8),
+                  ),
+                  child: Center(
+                    child: Text(
+                      '${index + 1}',
+                      style: TextStyle(
+                        color: Theme.of(context).colorScheme.onPrimaryContainer,
+                        fontWeight: FontWeight.bold,
+                      ),
+                    ),
+                  ),
+                ),
+                title: Text(label),
+                subtitle: Text(
+                  filename,
+                  style: const TextStyle(fontSize: 11, color: Colors.white54),
+                  overflow: TextOverflow.ellipsis,
+                ),
+                trailing: size != null
+                    ? Text(
+                        _formatSize(size),
+                        style: const TextStyle(fontSize: 11, color: Colors.white38),
+                      )
+                    : null,
+                onTap: () {
+                  Navigator.pop(context);
+                  onSelect(file);
+                },
+              );
+            },
+          ),
+          const SizedBox(height: 16),
+        ],
+      ),
+    );
+  }
+}

Clone this wiki locally