diff options
| -rw-r--r-- | player-android/lib/api/player_api_client.dart | 4 | ||||
| -rw-r--r-- | player-android/lib/models/media.dart | 5 | ||||
| -rw-r--r-- | player-android/test/models_test.dart | 9 |
3 files changed, 16 insertions, 2 deletions
diff --git a/player-android/lib/api/player_api_client.dart b/player-android/lib/api/player_api_client.dart index 3031c73..eb438f4 100644 --- a/player-android/lib/api/player_api_client.dart +++ b/player-android/lib/api/player_api_client.dart @@ -6,7 +6,9 @@ class PlayerApiClient { final Uri baseUrl; final String bearerToken; - const PlayerApiClient({required this.baseUrl, required this.bearerToken}); + // Normal (non-const) constructor: Uri is not const-constructable, so the + // constructor must not be declared const. + PlayerApiClient({required this.baseUrl, required this.bearerToken}); Future<User> bootstrap({required String username, required String password}) => throw UnimplementedError(); Future<User> login({required String username, required String password}) => throw UnimplementedError(); diff --git a/player-android/lib/models/media.dart b/player-android/lib/models/media.dart index 7330bca..19c57b8 100644 --- a/player-android/lib/models/media.dart +++ b/player-android/lib/models/media.dart @@ -10,7 +10,10 @@ class Media { const Media({required this.id, required this.setId, required this.relPath, required this.fileName, required this.absPath, required this.type, required this.duration, required this.codec, required this.resolution, required this.bitrate, required this.fileSizeBytes, required this.width, required this.height, required this.thumbnailPath, required this.playCount, this.favorite = false, this.tags = const [], this.deletedAt, this.createdAt}); - factory Media.fromJson(Map<String, dynamic> json) => Media(id: json['id'] as int? ?? 0, setId: json['set_id'] as int? ?? 0, relPath: json['rel_path'] as String? ?? '', fileName: json['file_name'] as String? ?? '', absPath: json['abs_path'] as String? ?? '', type: json['type'] as String? ?? '', duration: (json['duration'] as num?)?.toDouble() ?? 0, codec: json['codec'] as String? ?? '', resolution: json['resolution'] as String? ?? '', bitrate: json['bitrate'] as int? ?? 0, fileSizeBytes: json['file_size_bytes'] as int? ?? 0, width: json['width'] as int? ?? 0, height: json['height'] as int? ?? 0, thumbnailPath: json['thumbnail_path'] as String? ?? '', playCount: json['play_count'] as int? ?? 0, favorite: json['favorite'] as bool? ?? false, tags: (json['tags'] as List<dynamic>? ?? const []).cast<String>(), deletedAt: dateTimeFromJson(json['deleted_at']), createdAt: dateTimeFromJson(json['created_at'])); + factory Media.fromJson(Map<String, dynamic> json) => Media(id: json['id'] as int? ?? 0, setId: json['set_id'] as int? ?? 0, relPath: json['rel_path'] as String? ?? '', fileName: json['file_name'] as String? ?? '', absPath: json['abs_path'] as String? ?? '', type: json['type'] as String? ?? '', duration: (json['duration'] as num?)?.toDouble() ?? 0, codec: json['codec'] as String? ?? '', resolution: json['resolution'] as String? ?? '', bitrate: json['bitrate'] as int? ?? 0, fileSizeBytes: json['file_size_bytes'] as int? ?? 0, width: json['width'] as int? ?? 0, height: json['height'] as int? ?? 0, thumbnailPath: json['thumbnail_path'] as String? ?? '', playCount: json['play_count'] as int? ?? 0, favorite: json['favorite'] as bool? ?? false, // Safe tag deserialization: keep only elements that are already Strings, +// silently dropping ints, nulls, or other unexpected types. This tolerates +// malformed server responses without throwing a TypeError at runtime. +tags: (json['tags'] as List<dynamic>? ?? []).whereType<String>().toList(), deletedAt: dateTimeFromJson(json['deleted_at']), createdAt: dateTimeFromJson(json['created_at'])); Map<String, dynamic> toJson() => {'id': id, 'set_id': setId, 'rel_path': relPath, 'file_name': fileName, 'abs_path': absPath, 'type': type, 'duration': duration, 'codec': codec, 'resolution': resolution, 'bitrate': bitrate, 'file_size_bytes': fileSizeBytes, 'width': width, 'height': height, 'thumbnail_path': thumbnailPath, 'play_count': playCount, 'favorite': favorite, 'tags': tags, 'deleted_at': dateTimeToJson(deletedAt), 'created_at': dateTimeToJson(createdAt)}; } diff --git a/player-android/test/models_test.dart b/player-android/test/models_test.dart index 22dc190..84fbf62 100644 --- a/player-android/test/models_test.dart +++ b/player-android/test/models_test.dart @@ -126,6 +126,15 @@ void main() { expect(media.deletedAt, isNull); expect(media.createdAt, isNull); }); + + // Regression test for e9: non-string and null elements in the tags list + // must be silently dropped rather than causing a TypeError at runtime. + test('fromJson drops non-string and null tag elements', () { + final media = Media.fromJson({'tags': [1, 'valid', null]}); + + // Only 'valid' survives: the integer 1 and null are filtered out. + expect(media.tags, ['valid']); + }); }); // --------------------------------------------------------------------------- |
