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/api_manager.dart';
import 'package:release_schedule/api/json_helper.dart'; import 'package:release_schedule/api/json_helper.dart';
import 'package:release_schedule/api/movie_api.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/dates.dart';
import 'package:release_schedule/model/movie.dart';
class WikidataProperties { class WikidataProperties {
static const String instanceOf = "P31"; static const String instanceOf = "P31";
@ -75,7 +75,7 @@ class WikidataMovieApi implements MovieApi {
final start = i * batchSize; final start = i * batchSize;
final end = min((i + 1) * batchSize, movieIds.length); final end = min((i + 1) * batchSize, movieIds.length);
var response = await _wikidataApi.get( 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> result = jsonDecode(response.body);
Map<String, dynamic> batchEntities = result["entities"]; Map<String, dynamic> batchEntities = result["entities"];
entities.addAll(batchEntities); entities.addAll(batchEntities);
@ -94,6 +94,12 @@ class WikidataMovieApi implements MovieApi {
// they will be retrieved from the cache in fromWikidataEntity // they will be retrieved from the cache in fromWikidataEntity
await _getLabelsForEntities(allCountryAndGenreIds); 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 return movieIds
.map((id) => WikidataMovieData.fromWikidataEntity(id, entities[id])) .map((id) => WikidataMovieData.fromWikidataEntity(id, entities[id]))
.toList(); .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( String _createUpcomingMovieQuery(
DateTime startDate, String instanceOf, int limit) { DateTime startDate, String instanceOf, int limit) {
String date = DateFormat("yyyy-MM-dd").format(startDate); String date = DateFormat("yyyy-MM-dd").format(startDate);
@ -213,7 +142,7 @@ ORDER BY ?minReleaseDate
LIMIT $limit"""; LIMIT $limit""";
} }
DatePrecision _precisionFromWikidata(int precision) { DatePrecision precisionFromWikidata(int precision) {
return switch (precision) { return switch (precision) {
>= 13 => DatePrecision.minute, >= 13 => DatePrecision.minute,
12 => DatePrecision.hour, 12 => DatePrecision.hour,
@ -264,6 +193,48 @@ Future<Map<String, String>> _getLabelsForEntities(
return labels; return labels;
} }
String _getCachedLabelForEntity(String entityId) { String getCachedLabelForEntity(String entityId) {
return _labelCache[entityId] ?? 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: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/dates.dart';
import 'package:release_schedule/model/live_search.dart'; import 'package:release_schedule/model/live_search.dart';
import 'package:release_schedule/model/local_movie_storage.dart'; import 'package:release_schedule/model/local_movie_storage.dart';
@ -17,6 +18,10 @@ class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final MovieManager manager = MovieManager(
WikidataMovieApi(),
LocalMovieStorageGetStorage(WikidataMovieData.fromEncodable),
);
return MaterialApp( return MaterialApp(
title: 'Movie Schedule', title: 'Movie Schedule',
themeMode: ThemeMode.dark, themeMode: ThemeMode.dark,
@ -32,10 +37,7 @@ class MyApp extends StatelessWidget {
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange), colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
useMaterial3: true, useMaterial3: true,
), ),
home: HomePage( home: HomePage(manager),
MovieManager(WikidataMovieApi(),
LocalMovieStorageGetStorage(WikidataMovieData.fromEncodable)),
),
); );
} }
} }
@ -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 // Only show movies that are bookmarked or have a release date with at least month precision and at least one title
filter: (movie) => filter: (movie) =>
movie.bookmarked || movie.bookmarked ||
(movie.releaseDate.dateWithPrecision.precision >= (movie.releaseDate != null &&
movie.releaseDate!.dateWithPrecision.precision >=
DatePrecision.month && DatePrecision.month &&
(movie.titles?.length ?? 0) >= 1), (movie.titles?.value?.length ?? 0) >= 1),
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
child: const Icon(Icons.refresh), child: const Icon(Icons.refresh),
@ -234,7 +237,7 @@ class HamburgerMenu extends StatelessWidget {
child: const Text("Remove irrelevant"), child: const Text("Remove irrelevant"),
onTap: () => manager.removeMoviesWhere((movie) => onTap: () => manager.removeMoviesWhere((movie) =>
!movie.bookmarked && !movie.bookmarked &&
!(movie.releaseDates?.any((date) => !(movie.releaseDates?.value?.any((date) =>
date.dateWithPrecision.precision >= date.dateWithPrecision.precision >=
DatePrecision.month && DatePrecision.month &&
date.dateWithPrecision.date.isAfter(DateTime.now() date.dateWithPrecision.date.isAfter(DateTime.now()

View File

@ -55,6 +55,7 @@ class DateWithPrecision implements Comparable<DateWithPrecision> {
.firstWhere((element) => element.name == json[1]); .firstWhere((element) => element.name == json[1]);
DateWithPrecision.today() : this(DateTime.now().toUtc(), DatePrecision.day); DateWithPrecision.today() : this(DateTime.now().toUtc(), DatePrecision.day);
DateWithPrecision.unspecified() : this(DateTime(0), DatePrecision.decade);
List<dynamic> toJsonEncodable() { List<dynamic> toJsonEncodable() {
return [date.toIso8601String(), precision.name]; 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'; import 'package:release_schedule/model/dates.dart';
class MovieData extends ChangeNotifier { class MovieData extends ChangeNotifier {
String _title;
DateWithPrecisionAndCountry _releaseDate;
bool _bookmarked = false; bool _bookmarked = false;
bool _hasDetails = false; String? _title;
List<DateWithPrecisionAndCountry>? _releaseDates; DateWithPrecisionAndCountry? _releaseDate;
List<String>? _genres;
List<TitleInLanguage>? _titles;
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; return _title;
} }
DateWithPrecisionAndCountry get releaseDate { DateWithPrecisionAndCountry? get releaseDate {
return _releaseDate; return _releaseDate;
} }
@ -25,111 +29,188 @@ class MovieData extends ChangeNotifier {
return _bookmarked; return _bookmarked;
} }
List<DateWithPrecisionAndCountry>? get releaseDates { Dated<String?>? get description {
return _description;
}
Dated<List<DateWithPrecisionAndCountry>?>? get releaseDates {
return _releaseDates; return _releaseDates;
} }
List<String>? get genres { Dated<List<String>?>? get genres {
return _genres; return _genres;
} }
List<TitleInLanguage>? get titles { Dated<List<TextInLanguage>?>? get titles {
return _titles; return _titles;
} }
bool get hasDetails { Dated<List<TextInLanguage>?>? get labels {
return _hasDetails; return _labels;
} }
/// Updates the information with that of a new version of the movie /// 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. /// but ignores fields that are user controlled, like whether the movie was bookmarked.
void updateWithNewIgnoringUserControlled(MovieData movie) { void updateWithNewIgnoringUserControlled(MovieData movie) {
setDetails( setDetails(
title: movie.title, titles: movie.titles,
releaseDate: movie.releaseDate, labels: movie.labels,
releaseDates: movie.releaseDates, releaseDates: movie.releaseDates,
genres: movie.genres, genres: movie.genres,
titles: movie.titles); description: movie.description,
);
} }
void setDetails( void setNewDetails({
{String? title, bool? bookmarked,
DateWithPrecisionAndCountry? releaseDate, List<TextInLanguage>? titles,
bool? bookmarked, List<TextInLanguage>? labels,
List<DateWithPrecisionAndCountry>? releaseDates, List<DateWithPrecisionAndCountry>? releaseDates,
List<String>? genres, List<String>? genres,
List<TitleInLanguage>? titles}) { String? description,
if (title != null) { }) {
_title = title; setDetails(
} bookmarked: bookmarked,
if (releaseDate != null) { titles: titles != null ? Dated.now(titles) : null,
_releaseDate = releaseDate; 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) { if (bookmarked != null) {
_bookmarked = bookmarked; _bookmarked = bookmarked;
} }
if (releaseDates != null) {
_releaseDates = releaseDates;
}
if (genres != null) {
_genres = genres;
}
if (titles != null) { if (titles != null) {
_titles = titles; _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(); notifyListeners();
} }
@override @override
String toString() { 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) { bool same(MovieData other) {
return title == other.title && return title != null &&
releaseDate.dateWithPrecision == other.releaseDate.dateWithPrecision; title == other.title &&
(releaseDate == null ||
other.releaseDate == null ||
releaseDate!.dateWithPrecision.date.year ==
other.releaseDate!.dateWithPrecision.date.year);
} }
Map toJsonEncodable() { Map toJsonEncodable() {
List? releaseDatesByCountry = dynamic releaseDatesByCountry = _releaseDates?.toJsonEncodable(
_releaseDates?.map((e) => e.toJsonEncodable()).toList(); (releaseDates) => releaseDates
List? titlesByCountry = _titles?.map((e) => [e.title, e.language]).toList(); ?.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 { return {
"title": title,
"releaseDate": _releaseDate.toJsonEncodable(),
"bookmarked": _bookmarked, "bookmarked": _bookmarked,
"titles": titlesByCountry,
"labels": labels,
"releaseDates": releaseDatesByCountry, "releaseDates": releaseDatesByCountry,
"genres": genres, "genres": genres,
"titles": titlesByCountry, "description":
_description?.toJsonEncodable((description) => description),
}; };
} }
MovieData.fromJsonEncodable(Map json) MovieData.fromJsonEncodable(Map json) {
: _title = json["title"],
_releaseDate =
DateWithPrecisionAndCountry.fromJsonEncodable(json["releaseDate"]) {
setDetails( setDetails(
bookmarked: json["bookmarked"] as bool, bookmarked: json["bookmarked"] as bool? ?? false,
genres: (json["genres"] as List<dynamic>?) titles: decodeOptionalJson<Dated<List<TextInLanguage>?>>(
?.map((genre) => genre as String) json["titles"],
.toList(), (json) => Dated.fromJsonEncodable(
releaseDates: json["releaseDates"] != null json,
? (json["releaseDates"] as List<dynamic>) (value) => (value as List<dynamic>)
.map((release) => .map((title) =>
DateWithPrecisionAndCountry.fromJsonEncodable(release)) (text: title[0], language: title[1]) as TextInLanguage)
.toList() .toList())),
: null, labels: decodeOptionalJson<Dated<List<TextInLanguage>?>>(
titles: json["titles"] != null json["labels"],
? (json["titles"] as List<dynamic>) (json) => Dated.fromJsonEncodable(
.map((title) => json,
(title: title[0], language: title[1]) as TitleInLanguage) (value) => (value as List<dynamic>)
.toList() .map((label) =>
: null); (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 { class DateWithPrecisionAndCountry {
final DateWithPrecision dateWithPrecision; final DateWithPrecision dateWithPrecision;

View File

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

View File

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

View File

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

View File

@ -29,7 +29,7 @@ class MoviePage extends StatelessWidget {
animation: movie, animation: movie,
builder: (context, child) { builder: (context, child) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text(movie.title), actions: [ appBar: AppBar(title: Text(movie.title ?? "-"), actions: [
IconButton( IconButton(
icon: Icon(movie.bookmarked icon: Icon(movie.bookmarked
? Icons.bookmark_added ? Icons.bookmark_added
@ -39,19 +39,27 @@ class MoviePage extends StatelessWidget {
]), ]),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Padding( child: Padding(
padding: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(18.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Wrap( Wrap(
spacing: 10, spacing: 10,
runSpacing: 10, runSpacing: 10,
children: movie.genres children: movie.genres?.value
?.map((genre) => Chip(label: Text(genre))) ?.map((genre) => Chip(label: Text(genre)))
.toList() ?? .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"), const Heading("Titles"),
Table( Table(
border: TableBorder.symmetric( border: TableBorder.symmetric(
@ -59,7 +67,7 @@ class MoviePage extends StatelessWidget {
color: Theme.of(context).dividerColor, color: Theme.of(context).dividerColor,
), ),
), ),
children: movie.titles?.map((title) { children: movie.titles?.value?.map((title) {
return TableRow( return TableRow(
children: [ children: [
TableCell( TableCell(
@ -70,7 +78,7 @@ class MoviePage extends StatelessWidget {
TableCell( TableCell(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), 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, color: Theme.of(context).dividerColor,
), ),
), ),
children: movie.releaseDates?.map((releaseDate) { children: movie.releaseDates?.value?.map((releaseDate) {
return TableRow( return TableRow(
children: [ children: [
TableCell( TableCell(

View File

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

View File

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

View File

@ -15,21 +15,30 @@ void main() {
setUp(() { setUp(() {
storage = InMemoryMovieStorage(); storage = InMemoryMovieStorage();
storage.update([ storage.update([
MovieData( MovieData()
'The Shawshank Redemption', ..setNewDetails(
DateWithPrecisionAndCountry( labels: [(text: 'The Shawshank Redemption', language: 'en')],
DateTime(1994, 9, 22), DatePrecision.day, 'US'), releaseDates: [
), DateWithPrecisionAndCountry(
MovieData( DateTime(1994, 9, 22), DatePrecision.day, 'US')
'The Godfather', ],
DateWithPrecisionAndCountry( ),
DateTime(1972, 3, 24), DatePrecision.day, 'US'), MovieData()
), ..setNewDetails(
MovieData( labels: [(text: 'The Godfather', language: 'en')],
'The Dark Knight', releaseDates: [
DateWithPrecisionAndCountry( DateWithPrecisionAndCountry(
DateTime(2008, 7, 18), DatePrecision.day, 'US'), 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'; import 'package:release_schedule/view/movie_page.dart';
void main() { 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 { testWidgets('MovieItem displays movie data', (WidgetTester tester) async {
final movie = MovieData( testMovie.setNewDetails(
'Test Movie',
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US'),
);
movie.setDetails(
genres: ['Action', 'Adventure'], genres: ['Action', 'Adventure'],
); );
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: Scaffold( home: Scaffold(
body: MovieItem(movie), body: MovieItem(testMovie),
), ),
), ),
); );
@ -30,26 +36,17 @@ void main() {
}); });
testWidgets('should update when the movie is modified', (tester) async { 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( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: Scaffold( home: Scaffold(
body: MovieItem(movie), body: MovieItem(testMovie),
), ),
), ),
); );
expect(find.text('Test Movie'), findsOneWidget); expect(find.text('Test Movie'), findsOneWidget);
movie.setDetails( testMovie.setNewDetails(
genres: ['Action', 'Adventure', 'Comedy'], genres: ['Action', 'Adventure', 'Comedy'],
); );
@ -59,26 +56,17 @@ void main() {
}); });
testWidgets('should update when the movie is bookmarked', (tester) async { 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( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: Scaffold( home: Scaffold(
body: MovieItem(movie), body: MovieItem(testMovie),
), ),
), ),
); );
expect(find.byIcon(Icons.bookmark_border), findsOneWidget); expect(find.byIcon(Icons.bookmark_border), findsOneWidget);
movie.setDetails( testMovie.setDetails(
bookmarked: true, bookmarked: true,
); );
@ -89,19 +77,10 @@ void main() {
testWidgets("should update the bookmark state when the icon is tapped", testWidgets("should update the bookmark state when the icon is tapped",
(tester) async { (tester) async {
final movie = MovieData(
'Test Movie',
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US'),
);
movie.setDetails(
genres: ['Action', 'Adventure'],
);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: Scaffold( home: Scaffold(
body: MovieItem(movie), body: MovieItem(testMovie),
), ),
), ),
); );
@ -116,19 +95,10 @@ void main() {
}); });
testWidgets("should navigate to MoviePage when tapped", (tester) async { 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( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: Scaffold( home: Scaffold(
body: MovieItem(movie), body: MovieItem(testMovie),
), ),
), ),
); );

View File

@ -7,20 +7,30 @@ import 'package:release_schedule/view/movie_list.dart';
void main() { void main() {
group('MovieList', () { group('MovieList', () {
testWidgets('should render a list of movies', (WidgetTester tester) async { late List<MovieData> movies;
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'),
),
];
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( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: Scaffold( home: Scaffold(
@ -36,25 +46,12 @@ void main() {
testWidgets("should filter the list of movies", testWidgets("should filter the list of movies",
(WidgetTester tester) async { (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( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: Scaffold( home: Scaffold(
body: MovieList( body: MovieList(
movies, 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() { void main() {
group('MovieManagerList', () { 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 { testWidgets('displays movie list', (tester) async {
final manager = MovieManager(MovieApi(), InMemoryMovieStorage()); final manager = MovieManager(MovieApi(), InMemoryMovieStorage());
manager.addMovies([ manager.addMovies(movies);
MovieData(
'Movie 1',
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US')),
MovieData(
'Movie 2',
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 // 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)); await tester.pump(const Duration(seconds: 5));
@ -35,25 +48,20 @@ void main() {
testWidgets('updates when new movies are added', (tester) async { testWidgets('updates when new movies are added', (tester) async {
final manager = MovieManager(MovieApi(), InMemoryMovieStorage()); final manager = MovieManager(MovieApi(), InMemoryMovieStorage());
manager.addMovies([ manager.addMovies(movies);
MovieData(
'Movie 1',
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US')),
MovieData(
'Movie 2',
DateWithPrecisionAndCountry(
DateTime(2023, 1, 1), DatePrecision.day, 'US')),
]);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp(home: Scaffold(body: MovieManagerList(manager)))); MaterialApp(home: Scaffold(body: MovieManagerList(manager))));
manager.addMovies([ manager.addMovies([
MovieData( MovieData()
'Movie 3', ..setNewDetails(
DateWithPrecisionAndCountry( labels: [(text: 'Movie 3', language: 'en')],
DateTime(2023, 1, 1), DatePrecision.day, 'US')), 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 // 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)); await tester.pump(const Duration(seconds: 5));

View File

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