-
-
Notifications
You must be signed in to change notification settings - Fork 14
commit dcf36f5
abduznik edited this page May 23, 2026
·
1 revision
Commit: dcf36f5abcfb52e059095d485dedbdce066ec341
Author: Yan
Date: 2026-04-04
Why: Adds a new feature or capability to the application.
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.dartlib/ui/widgets/multi_disc_picker.dart
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),
+ ],
+ ),
+ );
+ }
+}