Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions lib/models/media_item.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ class MediaItem {
final String? imdbId;
final List<dynamic>? seasons; // To store season details for TV shows
final String? ageRating;
final String? originalTitle;
final String? tagline;
final List<String>? directors;
final List<String>? countries;

MediaItem({
required this.id,
Expand All @@ -37,6 +41,10 @@ class MediaItem {
this.imdbId,
this.seasons,
this.ageRating,
this.originalTitle,
this.tagline,
this.directors,
this.countries,
});

String get storageKey => '${mediaType}_$id';
Expand Down Expand Up @@ -73,6 +81,35 @@ class MediaItem {
return voteAverage.toStringAsFixed(1);
}

/// Audience average on a five-star scale, written the Brazilian way (4,1).
String get starRating =>
(voteAverage / 2).toStringAsFixed(1).replaceAll('.', ',');

/// Original title, only when it differs from the localized one.
String? get distinctOriginalTitle {
final original = originalTitle?.trim();
if (original == null || original.isEmpty) return null;
return original.toLowerCase() == title.toLowerCase() ? null : original;
}

/// "Dirigido por …" / "Criado por …" line, when the credits are known.
String? get directorLine {
final names = directors;
if (names == null || names.isEmpty) return null;
final joined = names.length == 1
? names.first
: '${names.take(names.length - 1).join(', ')} e ${names.last}';
return '${mediaType == 'tv' ? 'Criado por' : 'Dirigido por'} $joined';
}

/// Film-catalogue credit line: "Brasil, França, 2024".
String get originLine {
return [
...?countries?.take(2),
if (releaseDate != null && releaseDate!.length >= 4) formattedYear,
].join(', ');
}

factory MediaItem.fromJson(Map<String, dynamic> json,
{String defaultMediaType = 'movie'}) {
final titleStr = json['title'] ??
Expand Down Expand Up @@ -114,6 +151,40 @@ class MediaItem {
if (gIds.isEmpty) gIds = idsFromGenres;
}

List<String>? directors;
final storedDirectors = json['directors'];
if (storedDirectors is List) {
directors = storedDirectors.map((d) => d.toString()).toList();
} else if (json['credits'] is Map && json['credits']['crew'] is List) {
directors = [
for (final c in json['credits']['crew'] as List)
if (c is Map && c['job'] == 'Director' && c['name'] != null)
c['name'].toString(),
];
} else if (json['created_by'] is List) {
directors = [
for (final c in json['created_by'] as List)
if (c is Map && c['name'] != null) c['name'].toString(),
];
}

List<String>? countries;
final storedCountries = json['countries'];
if (storedCountries is List) {
countries = storedCountries.map((c) => c.toString()).toList();
} else if (json['production_countries'] is List) {
countries = [
for (final c in json['production_countries'] as List)
if (c is Map)
countryName(c['iso_3166_1']?.toString(), c['name']?.toString()),
].whereType<String>().toList();
} else if (json['origin_country'] is List) {
countries = [
for (final c in json['origin_country'] as List)
countryName(c?.toString(), null),
].whereType<String>().toList();
}

String? parsedImdbId = json['imdb_id'];
if (parsedImdbId == null && json['external_ids'] != null) {
parsedImdbId = json['external_ids']['imdb_id'];
Expand All @@ -140,6 +211,12 @@ class MediaItem {
imdbId: parsedImdbId ?? json['imdbId'],
seasons: json['seasons'],
ageRating: json['ageRating'],
originalTitle: json['original_title'] ?? json['original_name'],
tagline: (json['tagline'] as String?)?.trim().isEmpty ?? true
? null
: json['tagline'],
directors: directors == null || directors.isEmpty ? null : directors,
countries: countries == null || countries.isEmpty ? null : countries,
);
}

Expand All @@ -163,6 +240,10 @@ class MediaItem {
'imdbId': imdbId,
'seasons': seasons,
'ageRating': ageRating,
'original_title': originalTitle,
'tagline': tagline,
'directors': directors,
'countries': countries,
};
}

Expand Down Expand Up @@ -191,6 +272,10 @@ class MediaItem {
trailerKey: trailerKey,
imdbId: imdbId,
ageRating: ageRating,
originalTitle: originalTitle,
tagline: tagline,
directors: directors,
countries: countries,
);
}

Expand Down Expand Up @@ -223,6 +308,66 @@ class MediaItem {
imdbId: imdbId ?? this.imdbId,
seasons: seasons ?? this.seasons,
ageRating: ageRating ?? this.ageRating,
originalTitle: originalTitle,
tagline: tagline,
directors: directors,
countries: countries,
);
}
}

const _countryNames = {
'AR': 'Argentina',
'AU': 'Austrália',
'AT': 'Áustria',
'BE': 'Bélgica',
'BR': 'Brasil',
'CA': 'Canadá',
'CL': 'Chile',
'CN': 'China',
'CO': 'Colômbia',
'KR': 'Coreia do Sul',
'DK': 'Dinamarca',
'EG': 'Egito',
'ES': 'Espanha',
'US': 'Estados Unidos',
'FI': 'Finlândia',
'FR': 'França',
'GR': 'Grécia',
'HK': 'Hong Kong',
'HU': 'Hungria',
'IN': 'Índia',
'ID': 'Indonésia',
'IE': 'Irlanda',
'IR': 'Irã',
'IS': 'Islândia',
'IL': 'Israel',
'IT': 'Itália',
'JP': 'Japão',
'MX': 'México',
'NO': 'Noruega',
'NZ': 'Nova Zelândia',
'NL': 'Países Baixos',
'PE': 'Peru',
'PL': 'Polônia',
'PT': 'Portugal',
'GB': 'Reino Unido',
'CZ': 'República Tcheca',
'RO': 'Romênia',
'RU': 'Rússia',
'SE': 'Suécia',
'CH': 'Suíça',
'TH': 'Tailândia',
'TW': 'Taiwan',
'TR': 'Turquia',
'UA': 'Ucrânia',
'UY': 'Uruguai',
'DE': 'Alemanha',
'ZA': 'África do Sul',
};

/// Portuguese country name for an ISO 3166-1 code; TMDB only ships English.
String? countryName(String? code, String? fallback) {
final name = _countryNames[code?.toUpperCase()] ?? fallback ?? code;
return name == null || name.isEmpty ? null : name;
}
8 changes: 4 additions & 4 deletions lib/screens/categories_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ class CategoriesScreen extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('DESCUBRA',
Text('EXPLORAR',
style: colors.display(fontSize: width >= 800 ? 48 : 34)),
const SizedBox(height: 12),
Text('Outras histórias. Novos pontos de vista.',
Text('Cinema de todo o mundo, organizado por gênero.',
style: colors.body(fontSize: 17)),
]))),
SliverPadding(
Expand Down Expand Up @@ -121,13 +121,13 @@ class CategoriesScreen extends StatelessWidget {
style: colors.caption(
color: hasImage
? Colors.white
: colors.accent)),
: colors.textPrimary)),
const SizedBox(width: 8),
Icon(Icons.arrow_forward,
size: 18,
color: hasImage
? Colors.white
: colors.accent)
: colors.textPrimary)
]),
]))),
]));
Expand Down
33 changes: 25 additions & 8 deletions lib/screens/home_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import '../widgets/continue_watching_row.dart';
import '../widgets/hero_banner.dart';
import '../widgets/home_skeleton.dart';
import '../widgets/media_row.dart';
import '../widgets/now_showing_grid.dart';
import '../widgets/wordmark.dart';
import 'profile_selection_screen.dart';

Expand All @@ -25,6 +26,17 @@ class HomeScreen extends StatelessWidget {
if (catalog.heroItem != null) catalog.heroItem!,
...catalog.trending,
]);
// "Filme do dia": one pick from the head of the catalogue, rotating daily.
final now = DateTime.now();
final day = DateTime.utc(now.year, now.month, now.day)
.difference(DateTime.utc(2024))
.inDays;
final hero =
heroes.isEmpty ? null : heroes[day % heroes.length.clamp(1, 8)];
final showing = settings
.visibleItems(catalog.trending)
.where((m) => m.storageKey != hero?.storageKey)
.toList();
return Scaffold(
backgroundColor: SabuflixTheme.of(context).background,
body: catalog.isLoading
Expand All @@ -45,8 +57,8 @@ class HomeScreen extends StatelessWidget {
floating: true,
backgroundColor: SabuflixTheme.of(context).background,
elevation: 0,
centerTitle: false,
title: SabuflixWordmark(fontSize: 19),
centerTitle: true,
title: SabuflixWordmark(fontSize: 18),
actions: [
Padding(
padding: EdgeInsets.only(right: 16),
Expand All @@ -61,9 +73,14 @@ class HomeScreen extends StatelessWidget {
onRetry: catalog.loadCatalog,
),
),
if (heroes.isNotEmpty)
if (hero != null)
SliverToBoxAdapter(child: HeroBanner(media: hero)),
if (showing.isNotEmpty)
SliverToBoxAdapter(
child: HeroBanner(media: heroes.first)),
child: NowShowingGrid(
items: showing.take(isDesktop ? 6 : 4).toList(),
),
),
SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
Expand Down Expand Up @@ -92,12 +109,12 @@ class HomeScreen extends StatelessWidget {
settings.visibleItems(section.value),
),
MediaRow(
title: 'Em Alta Hoje',
title: 'Em Alta',
mediaItems:
settings.visibleItems(catalog.trending),
showing.skip(isDesktop ? 6 : 4).toList(),
),
MediaRow(
title: 'Filmes Populares',
title: 'Os Mais Vistos',
mediaItems: settings.visibleItems(
catalog.popularMovies,
),
Expand All @@ -108,7 +125,7 @@ class HomeScreen extends StatelessWidget {
settings.visibleItems(catalog.popularTV),
),
MediaRow(
title: 'Mais Bem Avaliados',
title: 'Aclamados pela Crítica',
mediaItems:
settings.visibleItems(catalog.topRated),
),
Expand Down
Loading
Loading