Compare commits

..

2 Commits

Author SHA1 Message Date
daniel-michel 92b4a302dc refactor: add dates for when information was retrieved and adjust everything 2024-01-10 23:50:43 +01:00
daniel-michel 0520120ccf feature: movie description 2024-01-10 14:46:03 +01:00
16 changed files with 680 additions and 542 deletions

View File

@ -0,0 +1,85 @@
import 'package:release_schedule/api/json_helper.dart';
import 'package:release_schedule/api/wikidata/wikidata_movie_api.dart';
import 'package:release_schedule/model/dates.dart';
import 'package:release_schedule/model/movie.dart';
class WikidataMovieData extends MovieData {
String entityId;
WikidataMovieData(this.entityId);
WikidataMovieData.fromEncodable(Map encodable)
: entityId = encodable["entityId"],
super.fromJsonEncodable(encodable);
@override
bool same(MovieData other) {
return other is WikidataMovieData && entityId == other.entityId;
}
@override
Map toJsonEncodable() {
return super.toJsonEncodable()..addAll({"entityId": entityId});
}
static WikidataMovieData fromWikidataEntity(
String entityId, Map<String, dynamic> entity) {
Map<String, dynamic> claims = entity["claims"];
List<TextInLanguage>? titles = selectInJson(
claims, "${WikidataProperties.title}.*.mainsnak.datavalue.value")
.map((value) => (
text: value["text"],
language: value["language"],
) as TextInLanguage)
.toList();
List<TextInLanguage>? labels = selectInJson(entity, "labels.*")
.map((value) => (
text: value["value"],
language: value["language"],
) as TextInLanguage)
.toList();
String? wikipediaTitle = selectInJson(entity, "sitelinks.enwiki.url")
.firstOrNull
?.split("/")
.last;
Dated<String?>? description = wikipediaTitle != null
? getCachedWikipediaExplainTextFotTitle(wikipediaTitle)
: null;
List<DateWithPrecisionAndCountry> releaseDates =
_getReleaseDates(claims).toList();
// Sort release dates with higher precision to the beginning
releaseDates.sort((a, b) => -a.dateWithPrecision.precision.index
.compareTo(b.dateWithPrecision.precision.index));
List<String>? genres = selectInJson<String>(
claims, "${WikidataProperties.genre}.*.mainsnak.datavalue.value.id")
.map(getCachedLabelForEntity)
.toList();
WikidataMovieData movie = WikidataMovieData(entityId);
movie.setDetails(
titles: Dated.now(titles),
labels: Dated.now(labels),
releaseDates: Dated.now(releaseDates),
genres: Dated.now(genres),
description: description,
);
return movie;
}
static Iterable<DateWithPrecisionAndCountry> _getReleaseDates(
Map<String, dynamic> claims) {
return selectInJson(claims, "${WikidataProperties.publicationDate}.*")
.where((dateClaim) => dateClaim["rank"] != "deprecated")
.expand<DateWithPrecisionAndCountry>((dateClaim) {
var value = selectInJson(dateClaim, "mainsnak.datavalue.value").first;
Iterable<String> countries = (selectInJson<String>(dateClaim,
"qualifiers.${WikidataProperties.placeOfPublication}.*.datavalue.value.id"))
.map(getCachedLabelForEntity);
if (countries.isEmpty) {
countries = ["unknown location"];
}
return countries.map((country) => DateWithPrecisionAndCountry(
DateTime.parse(value["time"]),
precisionFromWikidata(value["precision"]),
country));
});
}
}

View File

@ -6,8 +6,8 @@ import 'package:intl/intl.dart';
import 'package:release_schedule/api/api_manager.dart';
import 'package:release_schedule/api/json_helper.dart';
import 'package:release_schedule/api/movie_api.dart';
import 'package:release_schedule/api/wikidata/wikidata_movie.dart';
import 'package:release_schedule/model/dates.dart';
import 'package:release_schedule/model/movie.dart';
class WikidataProperties {
static const String instanceOf = "P31";
@ -75,7 +75,7 @@ class WikidataMovieApi implements MovieApi {
final start = i * batchSize;
final end = min((i + 1) * batchSize, movieIds.length);
var response = await _wikidataApi.get(
"&action=wbgetentities&format=json&props=labels|claims&ids=${movieIds.sublist(start, end).join("|")}");
"&action=wbgetentities&format=json&props=labels|claims|sitelinks/urls&ids=${movieIds.sublist(start, end).join("|")}");
Map<String, dynamic> result = jsonDecode(response.body);
Map<String, dynamic> batchEntities = result["entities"];
entities.addAll(batchEntities);
@ -94,6 +94,12 @@ class WikidataMovieApi implements MovieApi {
// they will be retrieved from the cache in fromWikidataEntity
await _getLabelsForEntities(allCountryAndGenreIds);
// Get wikipedia explaintexts
Iterable<String> allWikipediaTitles =
selectInJson<String>(entities, "*.sitelinks.enwiki.url")
.map((url) => url.split("/").last);
await _getWikipediaExplainTextForTitles(allWikipediaTitles.toList());
return movieIds
.map((id) => WikidataMovieData.fromWikidataEntity(id, entities[id]))
.toList();
@ -117,83 +123,6 @@ class WikidataMovieApi implements MovieApi {
}
}
class WikidataMovieData extends MovieData {
String entityId;
WikidataMovieData(
String title, DateWithPrecisionAndCountry releaseDate, this.entityId)
: super(title, releaseDate);
WikidataMovieData.fromEncodable(Map encodable)
: entityId = encodable["entityId"],
super.fromJsonEncodable(encodable);
@override
bool same(MovieData other) {
return other is WikidataMovieData && entityId == other.entityId;
}
@override
Map toJsonEncodable() {
return super.toJsonEncodable()..addAll({"entityId": entityId});
}
static WikidataMovieData fromWikidataEntity(
String entityId, Map<String, dynamic> entity) {
String title =
selectInJson<String>(entity, "labels.en.value").firstOrNull ??
selectInJson<String>(entity, "labels.*.value").first;
Map<String, dynamic> claims = entity["claims"];
List<TitleInLanguage>? titles = selectInJson(
claims, "${WikidataProperties.title}.*.mainsnak.datavalue.value")
.map((value) => (
title: value["text"],
language: value["language"],
) as TitleInLanguage)
.toList();
List<DateWithPrecisionAndCountry> releaseDates =
_getReleaseDates(claims).toList();
// Sort release dates with higher precision to the beginning
releaseDates.sort((a, b) => -a.dateWithPrecision.precision.index
.compareTo(b.dateWithPrecision.precision.index));
List<String>? genres = selectInJson<String>(
claims, "${WikidataProperties.genre}.*.mainsnak.datavalue.value.id")
.map(_getCachedLabelForEntity)
.toList();
WikidataMovieData movie = WikidataMovieData(
title,
releaseDates.isNotEmpty
? releaseDates[0]
: DateWithPrecisionAndCountry(
DateTime.now(), DatePrecision.decade, "unknown location"),
entityId);
movie.setDetails(
titles: titles,
releaseDates: releaseDates,
genres: genres,
);
return movie;
}
static Iterable<DateWithPrecisionAndCountry> _getReleaseDates(
Map<String, dynamic> claims) {
return selectInJson(claims, "${WikidataProperties.publicationDate}.*")
.where((dateClaim) => dateClaim["rank"] != "deprecated")
.expand<DateWithPrecisionAndCountry>((dateClaim) {
var value = selectInJson(dateClaim, "mainsnak.datavalue.value").first;
Iterable<String> countries = (selectInJson<String>(dateClaim,
"qualifiers.${WikidataProperties.placeOfPublication}.*.datavalue.value.id"))
.map(_getCachedLabelForEntity);
if (countries.isEmpty) {
countries = ["unknown location"];
}
return countries.map((country) => DateWithPrecisionAndCountry(
DateTime.parse(value["time"]),
_precisionFromWikidata(value["precision"]),
country));
});
}
}
String _createUpcomingMovieQuery(
DateTime startDate, String instanceOf, int limit) {
String date = DateFormat("yyyy-MM-dd").format(startDate);
@ -213,7 +142,7 @@ ORDER BY ?minReleaseDate
LIMIT $limit""";
}
DatePrecision _precisionFromWikidata(int precision) {
DatePrecision precisionFromWikidata(int precision) {
return switch (precision) {
>= 13 => DatePrecision.minute,
12 => DatePrecision.hour,
@ -264,6 +193,48 @@ Future<Map<String, String>> _getLabelsForEntities(
return labels;
}
String _getCachedLabelForEntity(String entityId) {
String getCachedLabelForEntity(String entityId) {
return _labelCache[entityId] ?? entityId;
}
ApiManager _wikipediaApi =
ApiManager("https://en.wikipedia.org/w/api.php?format=json&origin=*");
Map<String, Dated<String?>> _wikipediaExplainTextCache = {};
Future<Map<String, Dated<String?>>> _getWikipediaExplainTextForTitles(
List<String> pageTitles) async {
const batchSize = 50;
Map<String, Dated<String?>> explainTexts = {};
for (int i = pageTitles.length - 1; i >= 0; i--) {
if (_wikipediaExplainTextCache.containsKey(pageTitles[i])) {
explainTexts[pageTitles[i]] = _wikipediaExplainTextCache[pageTitles[i]]!;
pageTitles.removeAt(i);
}
}
for (int i = 0; i < (pageTitles.length / batchSize).ceil(); i++) {
final start = i * batchSize;
final end = min((i + 1) * batchSize, pageTitles.length);
Response response = await _wikipediaApi.get(
"&action=query&prop=extracts&exintro&explaintext&redirects=1&titles=${pageTitles.sublist(start, end).join("|")}");
Map<String, dynamic> result = jsonDecode(response.body);
List<dynamic> normalize = result["query"]["normalized"];
Map<String, dynamic> batchPages = result["query"]["pages"];
for (String pageId in batchPages.keys) {
String pageTitle = batchPages[pageId]["title"];
String originalTitle = normalize
.where((element) => element["to"] == pageTitle)
.firstOrNull?["from"] ??
pageTitle;
String? explainText = batchPages[pageId]["extract"];
if (explainText != null) {
_wikipediaExplainTextCache[originalTitle] =
explainTexts[originalTitle] = Dated.now(explainText);
}
}
}
return explainTexts;
}
Dated<String?>? getCachedWikipediaExplainTextFotTitle(String title) {
return _wikipediaExplainTextCache[title];
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:release_schedule/api/wikidata_movie_api.dart';
import 'package:release_schedule/api/wikidata/wikidata_movie.dart';
import 'package:release_schedule/api/wikidata/wikidata_movie_api.dart';
import 'package:release_schedule/model/dates.dart';
import 'package:release_schedule/model/live_search.dart';
import 'package:release_schedule/model/local_movie_storage.dart';
@ -17,6 +18,10 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final MovieManager manager = MovieManager(
WikidataMovieApi(),
LocalMovieStorageGetStorage(WikidataMovieData.fromEncodable),
);
return MaterialApp(
title: 'Movie Schedule',
themeMode: ThemeMode.dark,
@ -32,10 +37,7 @@ class MyApp extends StatelessWidget {
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
useMaterial3: true,
),
home: HomePage(
MovieManager(WikidataMovieApi(),
LocalMovieStorageGetStorage(WikidataMovieData.fromEncodable)),
),
home: HomePage(manager),
);
}
}
@ -187,9 +189,10 @@ class OverviewPage extends StatelessWidget {
// Only show movies that are bookmarked or have a release date with at least month precision and at least one title
filter: (movie) =>
movie.bookmarked ||
(movie.releaseDate.dateWithPrecision.precision >=
(movie.releaseDate != null &&
movie.releaseDate!.dateWithPrecision.precision >=
DatePrecision.month &&
(movie.titles?.length ?? 0) >= 1),
(movie.titles?.value?.length ?? 0) >= 1),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.refresh),
@ -234,7 +237,7 @@ class HamburgerMenu extends StatelessWidget {
child: const Text("Remove irrelevant"),
onTap: () => manager.removeMoviesWhere((movie) =>
!movie.bookmarked &&
!(movie.releaseDates?.any((date) =>
!(movie.releaseDates?.value?.any((date) =>
date.dateWithPrecision.precision >=
DatePrecision.month &&
date.dateWithPrecision.date.isAfter(DateTime.now()

View File

@ -55,6 +55,7 @@ class DateWithPrecision implements Comparable<DateWithPrecision> {
.firstWhere((element) => element.name == json[1]);
DateWithPrecision.today() : this(DateTime.now().toUtc(), DatePrecision.day);
DateWithPrecision.unspecified() : this(DateTime(0), DatePrecision.decade);
List<dynamic> toJsonEncodable() {
return [date.toIso8601String(), precision.name];
@ -122,3 +123,28 @@ class DateWithPrecision implements Comparable<DateWithPrecision> {
}
}
}
class Dated<T> {
final T value;
final DateTime date;
Dated(this.value, this.date);
Dated.now(this.value) : date = DateTime.now().toUtc();
Dated.fromJsonEncodable(
dynamic json, T Function(dynamic) valueFromJsonEncodable)
: value = valueFromJsonEncodable(json["value"]),
date = DateTime.parse(json["date"]);
Map<String, dynamic> toJsonEncodable(
dynamic Function(T) valueToJsonEncodable) {
return {
"value": valueToJsonEncodable(value),
"date": date.toIso8601String()
};
}
@override
toString() => "$value as of $date";
}

View File

@ -2,22 +2,26 @@ import 'package:flutter/material.dart';
import 'package:release_schedule/model/dates.dart';
class MovieData extends ChangeNotifier {
String _title;
DateWithPrecisionAndCountry _releaseDate;
bool _bookmarked = false;
bool _hasDetails = false;
List<DateWithPrecisionAndCountry>? _releaseDates;
List<String>? _genres;
List<TitleInLanguage>? _titles;
String? _title;
DateWithPrecisionAndCountry? _releaseDate;
MovieData(this._title, this._releaseDate);
// if it is entirely null the information was never loaded
// if only the value is null it was loaded but nothing was found
Dated<List<TextInLanguage>?>? _titles;
Dated<List<TextInLanguage>?>? _labels;
Dated<List<DateWithPrecisionAndCountry>?>? _releaseDates;
Dated<String?>? _description;
Dated<List<String>?>? _genres;
String get title {
MovieData();
String? get title {
return _title;
}
DateWithPrecisionAndCountry get releaseDate {
DateWithPrecisionAndCountry? get releaseDate {
return _releaseDate;
}
@ -25,111 +29,188 @@ class MovieData extends ChangeNotifier {
return _bookmarked;
}
List<DateWithPrecisionAndCountry>? get releaseDates {
Dated<String?>? get description {
return _description;
}
Dated<List<DateWithPrecisionAndCountry>?>? get releaseDates {
return _releaseDates;
}
List<String>? get genres {
Dated<List<String>?>? get genres {
return _genres;
}
List<TitleInLanguage>? get titles {
Dated<List<TextInLanguage>?>? get titles {
return _titles;
}
bool get hasDetails {
return _hasDetails;
Dated<List<TextInLanguage>?>? get labels {
return _labels;
}
/// Updates the information with that of a new version of the movie
/// but ignores fields that are user controlled, like whether the movie was bookmarked.
void updateWithNewIgnoringUserControlled(MovieData movie) {
setDetails(
title: movie.title,
releaseDate: movie.releaseDate,
releaseDates: movie.releaseDates,
genres: movie.genres,
titles: movie.titles);
titles: movie.titles,
labels: movie.labels,
releaseDates: movie.releaseDates,
genres: movie.genres,
description: movie.description,
);
}
void setDetails(
{String? title,
DateWithPrecisionAndCountry? releaseDate,
bool? bookmarked,
List<DateWithPrecisionAndCountry>? releaseDates,
List<String>? genres,
List<TitleInLanguage>? titles}) {
if (title != null) {
_title = title;
}
if (releaseDate != null) {
_releaseDate = releaseDate;
}
void setNewDetails({
bool? bookmarked,
List<TextInLanguage>? titles,
List<TextInLanguage>? labels,
List<DateWithPrecisionAndCountry>? releaseDates,
List<String>? genres,
String? description,
}) {
setDetails(
bookmarked: bookmarked,
titles: titles != null ? Dated.now(titles) : null,
labels: labels != null ? Dated.now(labels) : null,
releaseDates: releaseDates != null ? Dated.now(releaseDates) : null,
genres: genres != null ? Dated.now(genres) : null,
description: description != null ? Dated.now(description) : null,
);
}
void setDetails({
bool? bookmarked,
Dated<List<TextInLanguage>?>? titles,
Dated<List<TextInLanguage>?>? labels,
Dated<List<DateWithPrecisionAndCountry>?>? releaseDates,
Dated<List<String>?>? genres,
Dated<String?>? description,
}) {
if (bookmarked != null) {
_bookmarked = bookmarked;
}
if (releaseDates != null) {
_releaseDates = releaseDates;
}
if (genres != null) {
_genres = genres;
}
if (titles != null) {
_titles = titles;
}
_hasDetails = true;
if (labels != null) {
_labels = labels;
}
if (titles != null || labels != null) {
_title = null;
_title ??= _titles?.value
?.where((title) => title.language == "en")
.firstOrNull
?.text;
_title ??= _labels?.value
?.where((label) => label.language == "en")
.firstOrNull
?.text;
_title ??= _labels?.value?.firstOrNull?.text;
_title ??= _titles?.value?.firstOrNull?.text;
}
if (description != null) {
_description = description;
}
if (releaseDates != null) {
_releaseDates = releaseDates;
DateWithPrecisionAndCountry? mostPrecise =
_releaseDates?.value?.isNotEmpty ?? false
? _releaseDates?.value?.reduce((a, b) =>
a.dateWithPrecision.precision > b.dateWithPrecision.precision
? a
: b)
: null;
_releaseDate = mostPrecise;
}
if (genres != null) {
_genres = genres;
}
notifyListeners();
}
@override
String toString() {
return "$title (${_releaseDate.toString()}${_genres?.isNotEmpty ?? false ? "; ${_genres?.join(", ")}" : ""})";
return "$title (${_releaseDate.toString()}${_genres?.value?.isNotEmpty ?? false ? "; ${_genres?.value?.join(", ")}" : ""})";
}
bool same(MovieData other) {
return title == other.title &&
releaseDate.dateWithPrecision == other.releaseDate.dateWithPrecision;
return title != null &&
title == other.title &&
(releaseDate == null ||
other.releaseDate == null ||
releaseDate!.dateWithPrecision.date.year ==
other.releaseDate!.dateWithPrecision.date.year);
}
Map toJsonEncodable() {
List? releaseDatesByCountry =
_releaseDates?.map((e) => e.toJsonEncodable()).toList();
List? titlesByCountry = _titles?.map((e) => [e.title, e.language]).toList();
dynamic releaseDatesByCountry = _releaseDates?.toJsonEncodable(
(releaseDates) => releaseDates
?.map((releaseDate) => releaseDate.toJsonEncodable())
.toList());
dynamic titlesByCountry = _titles?.toJsonEncodable(
(titles) => titles?.map((e) => [e.text, e.language]).toList());
dynamic labels = _labels?.toJsonEncodable(
(labels) => labels?.map((e) => [e.text, e.language]).toList());
dynamic genres = _genres?.toJsonEncodable((genres) => genres);
return {
"title": title,
"releaseDate": _releaseDate.toJsonEncodable(),
"bookmarked": _bookmarked,
"titles": titlesByCountry,
"labels": labels,
"releaseDates": releaseDatesByCountry,
"genres": genres,
"titles": titlesByCountry,
"description":
_description?.toJsonEncodable((description) => description),
};
}
MovieData.fromJsonEncodable(Map json)
: _title = json["title"],
_releaseDate =
DateWithPrecisionAndCountry.fromJsonEncodable(json["releaseDate"]) {
MovieData.fromJsonEncodable(Map json) {
setDetails(
bookmarked: json["bookmarked"] as bool,
genres: (json["genres"] as List<dynamic>?)
?.map((genre) => genre as String)
.toList(),
releaseDates: json["releaseDates"] != null
? (json["releaseDates"] as List<dynamic>)
.map((release) =>
DateWithPrecisionAndCountry.fromJsonEncodable(release))
.toList()
: null,
titles: json["titles"] != null
? (json["titles"] as List<dynamic>)
.map((title) =>
(title: title[0], language: title[1]) as TitleInLanguage)
.toList()
: null);
bookmarked: json["bookmarked"] as bool? ?? false,
titles: decodeOptionalJson<Dated<List<TextInLanguage>?>>(
json["titles"],
(json) => Dated.fromJsonEncodable(
json,
(value) => (value as List<dynamic>)
.map((title) =>
(text: title[0], language: title[1]) as TextInLanguage)
.toList())),
labels: decodeOptionalJson<Dated<List<TextInLanguage>?>>(
json["labels"],
(json) => Dated.fromJsonEncodable(
json,
(value) => (value as List<dynamic>)
.map((label) =>
(text: label[0], language: label[1]) as TextInLanguage)
.toList())),
genres: decodeOptionalJson<Dated<List<String>?>>(
json["genres"],
(json) =>
Dated.fromJsonEncodable(json, (value) => value.cast<String>())),
releaseDates:
decodeOptionalJson<Dated<List<DateWithPrecisionAndCountry>?>>(
json["releaseDates"],
(json) => Dated.fromJsonEncodable(
json,
(value) => (value as List<dynamic>)
.map((releaseDate) =>
DateWithPrecisionAndCountry.fromJsonEncodable(
releaseDate))
.toList())),
description: decodeOptionalJson<Dated<String?>>(json["description"],
(json) => Dated.fromJsonEncodable(json, (value) => value)),
);
}
}
typedef TitleInLanguage = ({String title, String language});
T? decodeOptionalJson<T>(dynamic json, T Function(dynamic) decode) {
if (json == null) {
return null;
}
return decode(json);
}
typedef TextInLanguage = ({String text, String language});
class DateWithPrecisionAndCountry {
final DateWithPrecision dateWithPrecision;

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:release_schedule/api/movie_api.dart';
import 'package:release_schedule/model/dates.dart';
import 'package:release_schedule/model/delayed_function_caller.dart';
import 'package:release_schedule/model/local_movie_storage.dart';
import 'package:release_schedule/model/movie.dart';
@ -64,11 +65,15 @@ class MovieManager extends ChangeNotifier {
void _insertMovie(MovieData movie) {
int min = 0;
int max = movies.length - 1;
DateWithPrecision? movieDate = movie.releaseDate?.dateWithPrecision;
while (min <= max) {
int center = (min + max) ~/ 2;
int diff = movie.releaseDate.dateWithPrecision
.compareTo(movies[center].releaseDate.dateWithPrecision);
if (diff < 0) {
DateWithPrecision? centerDate =
movies[center].releaseDate?.dateWithPrecision;
int diff = movieDate != null && centerDate != null
? movieDate.compareTo(centerDate)
: 0;
if (movieDate == null || centerDate != null && diff < 0) {
max = center - 1;
} else {
min = center + 1;
@ -80,15 +85,13 @@ class MovieManager extends ChangeNotifier {
void _resortMovies() {
for (int i = 0; i < movies.length; i++) {
var temp = movies[i];
DateWithPrecision? tempDate = temp.releaseDate?.dateWithPrecision;
int j = i - 1;
for (;
j >= 0 &&
movies[j]
.releaseDate
.dateWithPrecision
.compareTo(temp.releaseDate.dateWithPrecision) >
0;
j--) {
for (; j >= 0; j--) {
DateWithPrecision? date = movies[j].releaseDate?.dateWithPrecision;
if (date == null || tempDate != null && date.compareTo(tempDate) <= 0) {
break;
}
movies[j + 1] = movies[j];
}
movies[j + 1] = temp;
@ -115,8 +118,8 @@ class MovieManager extends ChangeNotifier {
movies,
search,
(movie) => [
movie.title,
...(movie.titles?.map((title) => title.title) ?? []),
movie.title ?? "",
...(movie.titles?.value?.map((title) => title.text) ?? []),
]);
return results;
}

View File

@ -13,10 +13,10 @@ class MovieItem extends StatelessWidget {
animation: movie,
builder: (context, widget) {
return ListTile(
title: Text(movie.title),
title: Text(movie.title ?? "-"),
subtitle: Text(
(showReleaseDate ? "${movie.releaseDate} " : "") +
(movie.genres?.join(", ") ?? ""),
(movie.genres?.value?.join(", ") ?? ""),
),
trailing: IconButton(
icon: Icon(movie.bookmarked

View File

@ -78,9 +78,9 @@ class MovieList extends StatelessWidget {
int max = indexMap.length;
while (min < max) {
int center = (min + max) ~/ 2;
DateWithPrecision date =
movies[indexMap[center]].releaseDate.dateWithPrecision;
if (date.compareTo(today) < 0) {
DateWithPrecision? date =
movies[indexMap[center]].releaseDate?.dateWithPrecision;
if (date != null && date.compareTo(today) < 0) {
min = center + 1;
} else {
max = center;
@ -91,7 +91,8 @@ class MovieList extends StatelessWidget {
return GroupedList<DateWithPrecision>(
itemCount: indexMap.length,
groupBy: (index) =>
movies[indexMap[index]].releaseDate.dateWithPrecision,
movies[indexMap[index]].releaseDate?.dateWithPrecision ??
DateWithPrecision.unspecified(),
groupSeparatorBuilder: (date) => buildGroupSeparator(context, date),
itemBuilder: (context, index) {
return MovieItem(movies[indexMap[index]]);
@ -106,8 +107,8 @@ class MovieList extends StatelessWidget {
int max = movies.length;
while (min < max) {
int center = (min + max) ~/ 2;
DateWithPrecision date = movies[center].releaseDate.dateWithPrecision;
if (date.compareTo(today) < 0) {
DateWithPrecision? date = movies[center].releaseDate?.dateWithPrecision;
if (date != null && date.compareTo(today) < 0) {
min = center + 1;
} else {
max = center;
@ -117,7 +118,9 @@ class MovieList extends StatelessWidget {
}();
return GroupedList<DateWithPrecision>(
itemCount: movies.length,
groupBy: (index) => movies[index].releaseDate.dateWithPrecision,
groupBy: (index) =>
movies[index].releaseDate?.dateWithPrecision ??
DateWithPrecision.unspecified(),
groupSeparatorBuilder: (date) => buildGroupSeparator(context, date),
itemBuilder: (context, index) {
return MovieItem(movies[index]);

View File

@ -29,7 +29,7 @@ class MoviePage extends StatelessWidget {
animation: movie,
builder: (context, child) {
return Scaffold(
appBar: AppBar(title: Text(movie.title), actions: [
appBar: AppBar(title: Text(movie.title ?? "-"), actions: [
IconButton(
icon: Icon(movie.bookmarked
? Icons.bookmark_added
@ -39,19 +39,27 @@ class MoviePage extends StatelessWidget {
]),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(12.0),
padding: const EdgeInsets.all(18.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 10,
runSpacing: 10,
children: movie.genres
children: movie.genres?.value
?.map((genre) => Chip(label: Text(genre)))
.toList() ??
[],
),
const SizedBox(height: 20),
const Heading("About"),
Text(
movie.description?.value?.trim().replaceAll("\n", "\n\n") ??
"-",
textAlign: TextAlign.justify,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
height: 1.6,
),
),
const Heading("Titles"),
Table(
border: TableBorder.symmetric(
@ -59,7 +67,7 @@ class MoviePage extends StatelessWidget {
color: Theme.of(context).dividerColor,
),
),
children: movie.titles?.map((title) {
children: movie.titles?.value?.map((title) {
return TableRow(
children: [
TableCell(
@ -70,7 +78,7 @@ class MoviePage extends StatelessWidget {
TableCell(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(title.title),
child: Text(title.text),
))
],
);
@ -84,7 +92,7 @@ class MoviePage extends StatelessWidget {
color: Theme.of(context).dividerColor,
),
),
children: movie.releaseDates?.map((releaseDate) {
children: movie.releaseDates?.value?.map((releaseDate) {
return TableRow(
children: [
TableCell(

View File

@ -9,6 +9,23 @@ void main() {
group('MovieManager', () {
late MovieManager movieManager;
final theMatrix = MovieData()
..setNewDetails(
labels: [(text: 'The Matrix', language: 'en')],
releaseDates: [
DateWithPrecisionAndCountry(
DateTime(1999, 3, 31), DatePrecision.day, 'USA')
],
);
final theMatrixReloaded = MovieData()
..setNewDetails(
labels: [(text: 'The Matrix Reloaded', language: 'en')],
releaseDates: [
DateWithPrecisionAndCountry(
DateTime(2003, 5, 7), DatePrecision.day, 'USA')
],
);
setUp(() {
movieManager = MovieManager(
MovieApi(),
@ -18,16 +35,8 @@ void main() {
test('addMovies should add movies to the list', () {
final movies = [
MovieData(
'The Matrix',
DateWithPrecisionAndCountry(DateTime(1999, 3, 31), DatePrecision.day,
'United States of America'),
),
MovieData(
'The Matrix Reloaded',
DateWithPrecisionAndCountry(DateTime(2003, 5, 7), DatePrecision.day,
'United States of America'),
),
MovieData()..updateWithNewIgnoringUserControlled(theMatrix),
MovieData()..updateWithNewIgnoringUserControlled(theMatrixReloaded),
];
movieManager.addMovies(movies);
@ -37,56 +46,45 @@ void main() {
test('addMovies should add new movies', () {
final movies = [
MovieData(
'The Matrix',
DateWithPrecisionAndCountry(DateTime(1999, 3, 31), DatePrecision.day,
'United States of America'),
),
MovieData(
'The Matrix Reloaded',
DateWithPrecisionAndCountry(DateTime(2003, 5, 7), DatePrecision.day,
'United States of America'),
),
MovieData()..updateWithNewIgnoringUserControlled(theMatrix),
MovieData()..updateWithNewIgnoringUserControlled(theMatrixReloaded),
];
movieManager.addMovies(movies);
final newMovies = [
MovieData(
'The Matrix Revolutions',
DateWithPrecisionAndCountry(DateTime(2003, 11, 5), DatePrecision.day,
'United States of America'),
),
MovieData()
..setNewDetails(
labels: [(text: 'The Matrix Revolutions', language: 'en')],
releaseDates: [
DateWithPrecisionAndCountry(
DateTime(2003, 11, 5), DatePrecision.day, 'USA')
],
),
];
movieManager.addMovies(newMovies);
expect(movieManager.movies, equals([...movies, ...newMovies]));
expect(movieManager.movies, equals(movies + newMovies));
});
test('addMovies should update existing movies', () {
final movies = [
MovieData(
'The Matrix',
DateWithPrecisionAndCountry(DateTime(1999, 3, 31), DatePrecision.day,
'United States of America'),
),
MovieData(
'The Matrix Reloaded',
DateWithPrecisionAndCountry(DateTime(2003, 5, 7), DatePrecision.day,
'United States of America'),
),
MovieData()..updateWithNewIgnoringUserControlled(theMatrix),
MovieData()..updateWithNewIgnoringUserControlled(theMatrixReloaded),
];
movieManager.addMovies(movies);
final updatedMovie = MovieData(
'The Matrix Reloaded',
DateWithPrecisionAndCountry(DateTime(2003, 5, 7), DatePrecision.day,
'United States of America'),
)..setDetails(
final updatedMovie = MovieData()
..setNewDetails(
bookmarked: true,
genres: ['Action', 'Adventure'],
labels: [(text: 'The Matrix Reloaded', language: 'en')],
releaseDates: [
DateWithPrecisionAndCountry(
DateTime(2003, 5, 7), DatePrecision.day, 'USA')
],
);
movieManager.addMovies([updatedMovie]);
@ -97,37 +95,35 @@ void main() {
test('addMovies should sort movies by their release dates', () {
final movies = [
MovieData(
'The Matrix Reloaded',
DateWithPrecisionAndCountry(DateTime(2003, 5, 7), DatePrecision.day,
'United States of America'),
),
MovieData(
'The Matrix',
DateWithPrecisionAndCountry(DateTime(1999, 3, 31), DatePrecision.day,
'United States of America'),
),
MovieData()..updateWithNewIgnoringUserControlled(theMatrixReloaded),
MovieData()..updateWithNewIgnoringUserControlled(theMatrix),
];
movieManager.addMovies(movies);
expect(movieManager.movies, equals([...movies.reversed]));
expect(movieManager.movies, equals(movies.reversed.toList()));
});
test(
'addMovies should sort movies that have a less precise release date before movies with more precise release dates',
() {
final movies = [
MovieData(
'The Matrix Reloaded',
DateWithPrecisionAndCountry(DateTime(2003, 5, 7), DatePrecision.day,
'United States of America'),
),
MovieData(
'The Matrix',
DateWithPrecisionAndCountry(DateTime(2003, 5, 7), DatePrecision.month,
'United States of America'),
),
MovieData()
..updateWithNewIgnoringUserControlled(theMatrixReloaded)
..setNewDetails(
releaseDates: [
DateWithPrecisionAndCountry(
DateTime(2003, 5, 7), DatePrecision.day, 'USA')
],
),
MovieData()
..updateWithNewIgnoringUserControlled(theMatrix)
..setNewDetails(
releaseDates: [
DateWithPrecisionAndCountry(
DateTime(2003, 5, 7), DatePrecision.month, 'USA')
],
),
];
movieManager.addMovies(movies);
@ -139,68 +135,59 @@ void main() {
'when a movie is modified and it\'s date is changed the movies should be resorted',
() async {
final movies = [
MovieData(
'The Matrix Reloaded',
DateWithPrecisionAndCountry(DateTime(1998, 5, 7), DatePrecision.day,
'United States of America'),
),
MovieData(
'The Matrix',
DateWithPrecisionAndCountry(DateTime(1999, 3, 31), DatePrecision.day,
'United States of America'),
),
MovieData()
..updateWithNewIgnoringUserControlled(theMatrixReloaded)
..setNewDetails(
releaseDates: [
DateWithPrecisionAndCountry(
DateTime(1998, 5, 7), DatePrecision.day, 'USA')
],
),
MovieData()..updateWithNewIgnoringUserControlled(theMatrix),
];
movieManager.addMovies(movies);
final movie = movieManager.movies.first;
movie.setDetails(
releaseDate: DateWithPrecisionAndCountry(DateTime(2003, 5, 7),
DatePrecision.day, 'United States of America'),
movie.setNewDetails(
releaseDates: [
DateWithPrecisionAndCountry(DateTime(2003, 5, 7), DatePrecision.day,
'United States of America')
],
);
await Future.delayed(const Duration(milliseconds: 100));
expect(movieManager.movies, equals([...movies.reversed]));
expect(movieManager.movies, equals(movies.reversed.toList()));
});
test('removeMoviesWhere should remove movies from the list', () {
final movies = [
MovieData(
'The Matrix',
DateWithPrecisionAndCountry(DateTime(1999, 3, 31), DatePrecision.day,
'United States of America'),
),
MovieData(
'The Matrix Reloaded',
DateWithPrecisionAndCountry(DateTime(2003, 5, 7), DatePrecision.day,
'United States of America'),
),
MovieData()..updateWithNewIgnoringUserControlled(theMatrix),
MovieData()..updateWithNewIgnoringUserControlled(theMatrixReloaded),
];
MovieData notRemoved = MovieData(
'Harry Potter and the Philosopher\'s Stone',
DateWithPrecisionAndCountry(
DateTime(2001, 11, 4), DatePrecision.day, 'United Kingdom'),
);
MovieData notRemoved = MovieData()
..setNewDetails(
labels: [
(text: 'Harry Potter and the Philosopher\'s Stone', language: 'en')
],
releaseDates: [
DateWithPrecisionAndCountry(
DateTime(2001, 11, 4), DatePrecision.day, 'UK')
],
);
movieManager.addMovies([...movies, notRemoved]);
movieManager.addMovies(movies + [notRemoved]);
movieManager.removeMoviesWhere((movie) => movie.title.contains('Matrix'));
movieManager.removeMoviesWhere(
(movie) => movie.title?.contains('Matrix') == true);
expect(movieManager.movies, equals([notRemoved]));
});
test("localSearch", () {
final movies = [
MovieData(
'The Matrix',
DateWithPrecisionAndCountry(DateTime(1999, 3, 31), DatePrecision.day,
'United States of America'),
),
MovieData(
'The Matrix Reloaded',
DateWithPrecisionAndCountry(DateTime(2003, 5, 7), DatePrecision.day,
'United States of America'),
),
MovieData()..updateWithNewIgnoringUserControlled(theMatrix),
MovieData()..updateWithNewIgnoringUserControlled(theMatrixReloaded),
];
movieManager.addMovies(movies);

View File

@ -4,114 +4,108 @@ import 'package:release_schedule/model/movie.dart';
void main() {
group('MovieData', () {
MovieData firstMovie = MovieData()
..setNewDetails(
labels: [(text: 'Title 1', language: 'en')],
releaseDates: [
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US')
],
);
MovieData secondMovie = MovieData()
..setNewDetails(
labels: [(text: 'Title 2', language: 'en')],
releaseDates: [
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US')
],
);
test('updateWithNew() updates all fields', () {
final movie1 = MovieData(
'Title 1',
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US'));
final movie2 = MovieData(
'Title 2',
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'UK'));
movie2.setDetails(releaseDates: [
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US')
], genres: [
'Action',
'Adventure'
], titles: [
(title: 'Title 2', language: 'en')
]);
final movie1 = MovieData()
..updateWithNewIgnoringUserControlled(firstMovie);
final movie2 = MovieData()
..updateWithNewIgnoringUserControlled(secondMovie)
..setNewDetails(
releaseDates: [
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'UK')
],
genres: ['Action', 'Adventure'],
titles: [(text: 'Titel 2', language: 'de')],
);
movie1.updateWithNewIgnoringUserControlled(movie2);
expect(movie1.title, equals('Title 2'));
expect(movie1.releaseDate.country, equals('UK'));
expect(movie1.releaseDates!.length, equals(1));
expect(movie1.releaseDates![0].country, equals('US'));
expect(movie1.genres!.length, equals(2));
expect(movie1.genres![0], equals('Action'));
expect(movie1.genres![1], equals('Adventure'));
expect(movie1.titles!.length, equals(1));
expect(movie1.titles![0].title, equals('Title 2'));
expect(movie1.titles![0].language, equals('en'));
expect(movie1.releaseDate?.country, equals('UK'));
expect(movie1.releaseDates?.value?.length, equals(1));
expect(movie1.releaseDates?.value?[0].country, equals('UK'));
expect(movie1.genres?.value?.length, equals(2));
expect(movie1.genres?.value?[0], equals('Action'));
expect(movie1.genres?.value?[1], equals('Adventure'));
expect(movie1.titles?.value?.length, equals(1));
expect(movie1.titles?.value?[0].text, equals('Titel 2'));
expect(movie1.titles?.value?[0].language, equals('de'));
});
test('same() returns true for same title and release date', () {
final movie1 = MovieData(
'Title 1',
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US'));
final movie2 = MovieData(
'Title 1',
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US'));
test('same() returns true for same title and release year', () {
final movie1 = MovieData()
..updateWithNewIgnoringUserControlled(firstMovie);
final movie2 = MovieData()
..updateWithNewIgnoringUserControlled(firstMovie)
..setNewDetails(
releaseDates: [
DateWithPrecisionAndCountry(
DateTime(2023, 4, 27), DatePrecision.day, 'US')
],
);
expect(movie1.same(movie2), isTrue);
});
test('same() returns false for different title', () {
final movie1 = MovieData(
'Title 1',
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US'));
final movie2 = MovieData(
'Title 2',
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US'));
final movie1 = MovieData()
..updateWithNewIgnoringUserControlled(firstMovie);
final movie2 = MovieData()
..updateWithNewIgnoringUserControlled(secondMovie);
expect(movie1.same(movie2), isFalse);
});
test('same() returns false for different release date', () {
final movie1 = MovieData(
'Title 1',
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US'));
final movie2 = MovieData(
'Title 1',
DateWithPrecisionAndCountry(
DateTime(2023, 1, 2), DatePrecision.day, 'US'));
test('same() returns false for different release years', () {
final movie1 = MovieData()
..updateWithNewIgnoringUserControlled(firstMovie);
final movie2 = MovieData()
..updateWithNewIgnoringUserControlled(firstMovie)
..setNewDetails(
releaseDates: [
DateWithPrecisionAndCountry(
DateTime(2022, 1, 1), DatePrecision.day, 'US')
],
);
expect(movie1.same(movie2), isFalse);
});
test('can be encoded to JSON and back', () {
final movie = MovieData(
'Title 1',
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US'));
movie.setDetails(releaseDates: [
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US')
], genres: [
'Action',
'Adventure'
], titles: [
(title: 'Title 2', language: 'en')
]);
final movie = MovieData()
..updateWithNewIgnoringUserControlled(firstMovie)
..setNewDetails(
genres: ['Action', 'Adventure'],
);
final json = movie.toJsonEncodable();
final movie2 = MovieData.fromJsonEncodable(json);
expect(movie2.title, equals('Title 1'));
expect(movie2.releaseDate.country, equals('US'));
expect(movie2.releaseDates!.length, equals(1));
expect(movie2.releaseDates![0].country, equals('US'));
expect(movie2.genres!.length, equals(2));
expect(movie2.genres![0], equals('Action'));
expect(movie2.genres![1], equals('Adventure'));
expect(movie2.titles!.length, equals(1));
expect(movie2.titles![0].title, equals('Title 2'));
expect(movie2.titles![0].language, equals('en'));
expect(movie2.releaseDate?.country, equals('US'));
expect(movie2.releaseDates?.value?.length, equals(1));
expect(movie2.releaseDates?.value?[0].country, equals('US'));
expect(movie2.genres?.value?.length, equals(2));
expect(movie2.genres?.value?[0], equals('Action'));
expect(movie2.genres?.value?[1], equals('Adventure'));
expect(movie2.titles, equals(null));
});
test('toString()', () {
final movie = MovieData(
'Title 1',
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US'));
movie.setDetails(releaseDates: [
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US')
], genres: [
'Action',
'Adventure'
], titles: [
(title: 'Title 2', language: 'en')
]);
final movie = MovieData()
..updateWithNewIgnoringUserControlled(firstMovie)
..setNewDetails(
genres: ['Action', 'Adventure'],
);
expect(movie.toString(),
equals('Title 1 (January 1, 2023 (US); Action, Adventure)'));
});

View File

@ -15,21 +15,30 @@ void main() {
setUp(() {
storage = InMemoryMovieStorage();
storage.update([
MovieData(
'The Shawshank Redemption',
DateWithPrecisionAndCountry(
DateTime(1994, 9, 22), DatePrecision.day, 'US'),
),
MovieData(
'The Godfather',
DateWithPrecisionAndCountry(
DateTime(1972, 3, 24), DatePrecision.day, 'US'),
),
MovieData(
'The Dark Knight',
DateWithPrecisionAndCountry(
DateTime(2008, 7, 18), DatePrecision.day, 'US'),
),
MovieData()
..setNewDetails(
labels: [(text: 'The Shawshank Redemption', language: 'en')],
releaseDates: [
DateWithPrecisionAndCountry(
DateTime(1994, 9, 22), DatePrecision.day, 'US')
],
),
MovieData()
..setNewDetails(
labels: [(text: 'The Godfather', language: 'en')],
releaseDates: [
DateWithPrecisionAndCountry(
DateTime(1972, 3, 24), DatePrecision.day, 'US')
],
),
MovieData()
..setNewDetails(
labels: [(text: 'The Dark Knight', language: 'en')],
releaseDates: [
DateWithPrecisionAndCountry(
DateTime(2008, 7, 18), DatePrecision.day, 'US')
],
),
]);
});

View File

@ -6,20 +6,26 @@ import 'package:release_schedule/view/movie_item.dart';
import 'package:release_schedule/view/movie_page.dart';
void main() {
late MovieData testMovie;
setUp(() {
testMovie = MovieData()
..setNewDetails(
labels: [(text: 'Test Movie', language: 'en')],
releaseDates: [
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US')
],
);
});
testWidgets('MovieItem displays movie data', (WidgetTester tester) async {
final movie = MovieData(
'Test Movie',
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US'),
);
movie.setDetails(
testMovie.setNewDetails(
genres: ['Action', 'Adventure'],
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: MovieItem(movie),
body: MovieItem(testMovie),
),
),
);
@ -30,26 +36,17 @@ void main() {
});
testWidgets('should update when the movie is modified', (tester) async {
final movie = MovieData(
'Test Movie',
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US'),
);
movie.setDetails(
genres: ['Action', 'Adventure'],
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: MovieItem(movie),
body: MovieItem(testMovie),
),
),
);
expect(find.text('Test Movie'), findsOneWidget);
movie.setDetails(
testMovie.setNewDetails(
genres: ['Action', 'Adventure', 'Comedy'],
);
@ -59,26 +56,17 @@ void main() {
});
testWidgets('should update when the movie is bookmarked', (tester) async {
final movie = MovieData(
'Test Movie',
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US'),
);
movie.setDetails(
genres: ['Action', 'Adventure'],
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: MovieItem(movie),
body: MovieItem(testMovie),
),
),
);
expect(find.byIcon(Icons.bookmark_border), findsOneWidget);
movie.setDetails(
testMovie.setDetails(
bookmarked: true,
);
@ -89,19 +77,10 @@ void main() {
testWidgets("should update the bookmark state when the icon is tapped",
(tester) async {
final movie = MovieData(
'Test Movie',
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US'),
);
movie.setDetails(
genres: ['Action', 'Adventure'],
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: MovieItem(movie),
body: MovieItem(testMovie),
),
),
);
@ -116,19 +95,10 @@ void main() {
});
testWidgets("should navigate to MoviePage when tapped", (tester) async {
final movie = MovieData(
'Test Movie',
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US'),
);
movie.setDetails(
genres: ['Action', 'Adventure'],
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: MovieItem(movie),
body: MovieItem(testMovie),
),
),
);

View File

@ -7,20 +7,30 @@ import 'package:release_schedule/view/movie_list.dart';
void main() {
group('MovieList', () {
testWidgets('should render a list of movies', (WidgetTester tester) async {
final movies = [
MovieData(
'The Shawshank Redemption',
DateWithPrecisionAndCountry(
DateTime(1994, 9, 22), DatePrecision.day, 'US'),
),
MovieData(
'The Godfather',
DateWithPrecisionAndCountry(
DateTime(1972, 3, 24), DatePrecision.day, 'US'),
),
];
late List<MovieData> movies;
setUp(() {
movies = [
MovieData()
..setNewDetails(
labels: [(text: 'The Shawshank Redemption', language: 'en')],
releaseDates: [
DateWithPrecisionAndCountry(
DateTime(1994, 9, 22), DatePrecision.day, 'US')
],
),
MovieData()
..setNewDetails(
labels: [(text: 'The Godfather', language: 'en')],
releaseDates: [
DateWithPrecisionAndCountry(
DateTime(1972, 3, 24), DatePrecision.day, 'US')
],
)
];
});
testWidgets('should render a list of movies', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
@ -36,25 +46,12 @@ void main() {
testWidgets("should filter the list of movies",
(WidgetTester tester) async {
final movies = [
MovieData(
'The Shawshank Redemption',
DateWithPrecisionAndCountry(
DateTime(1994, 9, 22), DatePrecision.day, 'US'),
),
MovieData(
'The Godfather',
DateWithPrecisionAndCountry(
DateTime(1972, 3, 24), DatePrecision.day, 'US'),
),
];
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: MovieList(
movies,
filter: (movie) => movie.title.contains('Godfather'),
filter: (movie) => movie.title?.contains('Godfather') == true,
),
),
),

View File

@ -11,18 +11,31 @@ import 'package:release_schedule/view/movie_manager_list.dart';
void main() {
group('MovieManagerList', () {
late List<MovieData> movies;
setUp(() {
movies = [
MovieData()
..setNewDetails(
labels: [(text: 'Movie 1', language: 'en')],
releaseDates: [
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US')
],
),
MovieData()
..setNewDetails(
labels: [(text: 'Movie 2', language: 'en')],
releaseDates: [
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US')
],
)
];
});
testWidgets('displays movie list', (tester) async {
final manager = MovieManager(MovieApi(), InMemoryMovieStorage());
manager.addMovies([
MovieData(
'Movie 1',
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US')),
MovieData(
'Movie 2',
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US')),
]);
manager.addMovies(movies);
// pump the delay until the change is written to the cache, so no timers run when the test finishes
await tester.pump(const Duration(seconds: 5));
@ -35,25 +48,20 @@ void main() {
testWidgets('updates when new movies are added', (tester) async {
final manager = MovieManager(MovieApi(), InMemoryMovieStorage());
manager.addMovies([
MovieData(
'Movie 1',
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US')),
MovieData(
'Movie 2',
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US')),
]);
manager.addMovies(movies);
await tester.pumpWidget(
MaterialApp(home: Scaffold(body: MovieManagerList(manager))));
manager.addMovies([
MovieData(
'Movie 3',
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US')),
MovieData()
..setNewDetails(
labels: [(text: 'Movie 3', language: 'en')],
releaseDates: [
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US')
],
)
]);
// pump the delay until the change is written to the cache, so no timers run when the test finishes
await tester.pump(const Duration(seconds: 5));

View File

@ -6,13 +6,20 @@ import 'package:release_schedule/view/movie_page.dart';
void main() {
group('MoviePage', () {
testWidgets('should render the movie details', (WidgetTester tester) async {
final movie = MovieData(
'The Shawshank Redemption',
DateWithPrecisionAndCountry(
DateTime(1994, 9, 22), DatePrecision.day, 'US'),
);
late MovieData movie;
setUp(() {
movie = MovieData()
..setNewDetails(
labels: [(text: 'The Shawshank Redemption', language: 'en')],
releaseDates: [
DateWithPrecisionAndCountry(
DateTime(1994, 9, 22), DatePrecision.day, 'US')
],
);
});
testWidgets('should render the movie details', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
@ -23,16 +30,10 @@ void main() {
await tester.pumpAndSettle();
expect(find.text(movie.title), findsAtLeastNWidgets(1));
expect(find.text(movie.title!), findsAtLeastNWidgets(1));
});
testWidgets('should bookmark the movie', (WidgetTester tester) async {
final movie = MovieData(
'The Shawshank Redemption',
DateWithPrecisionAndCountry(
DateTime(1994, 9, 22), DatePrecision.day, 'US'),
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
@ -50,56 +51,48 @@ void main() {
expect(movie.bookmarked, isTrue);
});
});
testWidgets("should display the movie's genres",
(WidgetTester tester) async {
movie.setNewDetails(genres: ['Drama']);
testWidgets("should display the movie's genres", (WidgetTester tester) async {
final movie = MovieData(
'The Shawshank Redemption',
DateWithPrecisionAndCountry(
DateTime(1994, 9, 22), DatePrecision.day, 'US'),
)..setDetails(genres: ['Drama']);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: MoviePage(movie),
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: MoviePage(movie),
),
),
),
);
);
await tester.pumpAndSettle();
await tester.pumpAndSettle();
expect(find.text('Drama'), findsOneWidget);
});
expect(find.text('Drama'), findsOneWidget);
});
testWidgets("should display the movie's titles and release dates",
(WidgetTester tester) async {
final movie = MovieData(
'The Shawshank Redemption',
DateWithPrecisionAndCountry(
DateTime(1994, 9, 22), DatePrecision.day, 'US'),
)..setDetails(
titles: [(title: 'The Shawshank Redemption', language: 'en')],
testWidgets("should display the movie's titles and release dates",
(WidgetTester tester) async {
movie.setNewDetails(
titles: [(text: 'The Shawshank Redemption', language: 'en')],
releaseDates: [
DateWithPrecisionAndCountry(
DateTime(1994, 9, 22), DatePrecision.day, 'US')
],
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: MoviePage(movie),
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: MoviePage(movie),
),
),
),
);
);
await tester.pumpAndSettle();
await tester.pumpAndSettle();
expect(find.text('en'), findsOneWidget);
expect(find.text('The Shawshank Redemption'), findsNWidgets(2));
expect(find.text('en'), findsOneWidget);
expect(find.text('The Shawshank Redemption'), findsNWidgets(2));
expect(find.text('US'), findsOneWidget);
expect(find.textContaining('1994'), findsOneWidget);
expect(find.text('US'), findsOneWidget);
expect(find.textContaining('1994'), findsOneWidget);
});
});
}