diff --git a/lib/api/wikidata_movie_api.dart b/lib/api/wikidata_movie_api.dart index 4064a46..9a2015b 100644 --- a/lib/api/wikidata_movie_api.dart +++ b/lib/api/wikidata_movie_api.dart @@ -6,6 +6,7 @@ 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/model/dates.dart'; import 'package:release_schedule/model/movie.dart'; class WikidataProperties { diff --git a/lib/main.dart b/lib/main.dart index 5395eff..93b59f8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:release_schedule/model/dates.dart'; import 'package:release_schedule/model/live_search.dart'; -import 'package:release_schedule/model/movie.dart'; import 'package:release_schedule/model/movie_manager.dart'; import 'package:release_schedule/view/movie_item.dart'; import 'package:release_schedule/view/movie_manager_list.dart'; @@ -65,6 +65,7 @@ class _HomePageState extends State Widget build(BuildContext context) { return Scaffold( appBar: AppBar( + elevation: 1, title: Row( children: [ Expanded( @@ -159,6 +160,12 @@ class OverviewPage extends StatelessWidget { length: 2, child: Column( children: [ + const TabBar( + tabs: [ + Tab(icon: Icon(Icons.list), child: Text("Upcoming")), + Tab(icon: Icon(Icons.bookmark), child: Text("Bookmarked")), + ], + ), Expanded( child: TabBarView( children: [ @@ -195,10 +202,6 @@ class OverviewPage extends StatelessWidget { ], ), ), - const TabBar(tabs: [ - Tab(icon: Icon(Icons.list), child: Text("Upcoming")), - Tab(icon: Icon(Icons.bookmark), child: Text("Bookmarked")), - ]), ], ), ); diff --git a/lib/model/dates.dart b/lib/model/dates.dart new file mode 100644 index 0000000..43c6a4a --- /dev/null +++ b/lib/model/dates.dart @@ -0,0 +1,124 @@ +import 'package:intl/intl.dart'; + +DateTime getToday() { + DateTime now = DateTime.now().toUtc(); + return DateTime.utc(now.year, now.month, now.day); +} + +enum DatePrecision { decade, year, month, day, hour, minute } + +extension DatePrecisionComparison on DatePrecision { + bool operator <(DatePrecision other) { + return index < other.index; + } + + bool operator <=(DatePrecision other) { + return index <= other.index; + } + + bool operator >(DatePrecision other) { + return index > other.index; + } + + bool operator >=(DatePrecision other) { + return index >= other.index; + } +} + +DateTime simplifyDateToPrecision(DateTime date, DatePrecision precision) { + switch (precision) { + case DatePrecision.decade: + return DateTime(date.year ~/ 10 * 10); + case DatePrecision.year: + return DateTime(date.year); + case DatePrecision.month: + return DateTime(date.year, date.month); + case DatePrecision.day: + return DateTime(date.year, date.month, date.day); + case DatePrecision.hour: + return DateTime(date.year, date.month, date.day, date.hour); + case DatePrecision.minute: + return DateTime(date.year, date.month, date.day, date.hour, date.minute); + } +} + +class DateWithPrecision implements Comparable { + DateTime date; + DatePrecision precision; + + DateWithPrecision(DateTime date, this.precision) + : date = simplifyDateToPrecision(date, precision); + + DateWithPrecision.fromJsonEncodable(List json) + : date = DateTime.parse(json[0]), + precision = DatePrecision.values + .firstWhere((element) => element.name == json[1]); + + DateWithPrecision.today() : this(DateTime.now().toUtc(), DatePrecision.day); + + List toJsonEncodable() { + return [date.toIso8601String(), precision.name]; + } + + @override + String toString() { + return switch (precision) { + DatePrecision.decade => + "${DateFormat("yyyy").format(date).substring(0, 3)}0s", + DatePrecision.year => DateFormat.y().format(date), + DatePrecision.month => DateFormat.yMMMM().format(date), + DatePrecision.day => DateFormat.yMMMMd().format(date), + DatePrecision.hour => DateFormat("MMMM d, yyyy, HH").format(date), + DatePrecision.minute => DateFormat("MMMM d, yyyy, HH:mm").format(date) + }; + } + + @override + int compareTo(DateWithPrecision other) { + if (date.isBefore(other.date)) { + return -1; + } else if (date.isAfter(other.date)) { + return 1; + } else { + return precision.index - other.precision.index; + } + } + + @override + bool operator ==(Object other) { + return other is DateWithPrecision && + date == other.date && + precision == other.precision; + } + + @override + int get hashCode { + return date.hashCode ^ precision.hashCode; + } + + bool includes(DateTime date) { + switch (precision) { + case DatePrecision.decade: + return this.date.year ~/ 10 == date.year ~/ 10; + case DatePrecision.year: + return this.date.year == date.year; + case DatePrecision.month: + return this.date.year == date.year && this.date.month == date.month; + case DatePrecision.day: + return this.date.year == date.year && + this.date.month == date.month && + this.date.day == date.day; + case DatePrecision.hour: + return this.date.year == date.year && + this.date.month == date.month && + this.date.day == date.day && + this.date.hour == date.hour; + case DatePrecision.minute: + return this.date.year == date.year && + this.date.month == date.month && + this.date.day == date.day && + this.date.hour == date.hour && + this.date.minute == date.minute; + } + } +} diff --git a/lib/model/movie.dart b/lib/model/movie.dart index 361604f..41c3789 100644 --- a/lib/model/movie.dart +++ b/lib/model/movie.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; +import 'package:release_schedule/model/dates.dart'; class MovieData extends ChangeNotifier { String _title; @@ -145,106 +145,8 @@ class MovieData extends ChangeNotifier { } } -enum DatePrecision { decade, year, month, day, hour, minute } - -extension DatePrecisionComparison on DatePrecision { - bool operator <(DatePrecision other) { - return index < other.index; - } - - bool operator <=(DatePrecision other) { - return index <= other.index; - } - - bool operator >(DatePrecision other) { - return index > other.index; - } - - bool operator >=(DatePrecision other) { - return index >= other.index; - } -} - typedef TitleInLanguage = ({String title, String language}); -class DateWithPrecision implements Comparable { - DateTime date; - DatePrecision precision; - - DateWithPrecision(this.date, this.precision); - - DateWithPrecision.fromJsonEncodable(List json) - : date = DateTime.parse(json[0]), - precision = DatePrecision.values - .firstWhere((element) => element.name == json[1]); - - List toJsonEncodable() { - return [date.toIso8601String(), precision.name]; - } - - @override - String toString() { - return switch (precision) { - DatePrecision.decade => - "${DateFormat("yyyy").format(date).substring(0, 3)}0s", - DatePrecision.year => DateFormat.y().format(date), - DatePrecision.month => DateFormat.yMMMM().format(date), - DatePrecision.day => DateFormat.yMMMMd().format(date), - DatePrecision.hour => DateFormat("MMMM d, yyyy, HH").format(date), - DatePrecision.minute => DateFormat("MMMM d, yyyy, HH:mm").format(date) - }; - } - - @override - int compareTo(DateWithPrecision other) { - if (date.isBefore(other.date)) { - return -1; - } else if (date.isAfter(other.date)) { - return 1; - } else { - return precision.index - other.precision.index; - } - } - - @override - bool operator ==(Object other) { - return other is DateWithPrecision && - date == other.date && - precision == other.precision; - } - - @override - int get hashCode { - return date.hashCode ^ precision.hashCode; - } - - bool includes(DateTime date) { - switch (precision) { - case DatePrecision.decade: - return this.date.year ~/ 10 == date.year ~/ 10; - case DatePrecision.year: - return this.date.year == date.year; - case DatePrecision.month: - return this.date.year == date.year && this.date.month == date.month; - case DatePrecision.day: - return this.date.year == date.year && - this.date.month == date.month && - this.date.day == date.day; - case DatePrecision.hour: - return this.date.year == date.year && - this.date.month == date.month && - this.date.day == date.day && - this.date.hour == date.hour; - case DatePrecision.minute: - return this.date.year == date.year && - this.date.month == date.month && - this.date.day == date.day && - this.date.hour == date.hour && - this.date.minute == date.minute; - } - } -} - class DateWithPrecisionAndCountry { final DateWithPrecision dateWithPrecision; final String country; diff --git a/lib/view/movie_list.dart b/lib/view/movie_list.dart index dd4ed01..0ee5155 100644 --- a/lib/view/movie_list.dart +++ b/lib/view/movie_list.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:release_schedule/model/dates.dart'; import 'package:release_schedule/model/movie.dart'; import 'package:release_schedule/view/movie_item.dart'; import 'package:sticky_grouped_list/sticky_grouped_list.dart'; @@ -10,6 +11,24 @@ class MovieList extends StatelessWidget { @override Widget build(BuildContext context) { + if (movies.isEmpty) { + return Center( + child: IntrinsicHeight( + child: Column( + children: [ + const Icon( + Icons.close, + size: 100, + ), + Text( + "No movies available", + style: Theme.of(context).textTheme.headlineMedium, + ), + ], + ), + ), + ); + } Widget buildGroupSeparator(BuildContext context, DateWithPrecision date) { bool highlight = date.includes(DateTime.now()); return SizedBox( @@ -45,6 +64,22 @@ class MovieList extends StatelessWidget { } index++; } + int firstMovieTodayOrAfterIndex = () { + DateWithPrecision today = DateWithPrecision.today(); + int min = 0; + int max = indexMap.length; + while (min < max) { + int center = (min + max) ~/ 2; + DateWithPrecision date = + movies[indexMap[center]].releaseDate.dateWithPrecision; + if (date.compareTo(today) < 0) { + min = center + 1; + } else { + max = center; + } + } + return max; + }(); return StickyGroupedListView( elements: indexMap, floatingHeader: true, @@ -54,8 +89,24 @@ class MovieList extends StatelessWidget { itemBuilder: (context, index) { return MovieItem(movies[index]); }, + initialScrollIndex: firstMovieTodayOrAfterIndex, ); } + int firstMovieTodayOrAfterIndex = () { + DateWithPrecision today = DateWithPrecision.today(); + int min = 0; + int max = movies.length; + while (min < max) { + int center = (min + max) ~/ 2; + DateWithPrecision date = movies[center].releaseDate.dateWithPrecision; + if (date.compareTo(today) < 0) { + min = center + 1; + } else { + max = center; + } + } + return max; + }(); return StickyGroupedListView( elements: movies, floatingHeader: true, @@ -65,6 +116,7 @@ class MovieList extends StatelessWidget { itemBuilder: (context, movie) { return MovieItem(movie); }, + initialScrollIndex: firstMovieTodayOrAfterIndex, ); } } diff --git a/test/model/movie_manager_test.dart b/test/model/movie_manager_test.dart index c949991..016c218 100644 --- a/test/model/movie_manager_test.dart +++ b/test/model/movie_manager_test.dart @@ -1,5 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:release_schedule/api/movie_api.dart'; +import 'package:release_schedule/model/dates.dart'; import 'package:release_schedule/model/local_movie_storage.dart'; import 'package:release_schedule/model/movie.dart'; import 'package:release_schedule/model/movie_manager.dart'; diff --git a/test/model/movie_test.dart b/test/model/movie_test.dart index 4b1090d..81548a3 100644 --- a/test/model/movie_test.dart +++ b/test/model/movie_test.dart @@ -1,4 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; +import 'package:release_schedule/model/dates.dart'; import 'package:release_schedule/model/movie.dart'; void main() { diff --git a/test/view/movie_item_test.dart b/test/view/movie_item_test.dart index f009683..a3773d8 100644 --- a/test/view/movie_item_test.dart +++ b/test/view/movie_item_test.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:release_schedule/model/dates.dart'; import 'package:release_schedule/model/movie.dart'; import 'package:release_schedule/view/movie_item.dart'; diff --git a/test/view/movie_list_test.dart b/test/view/movie_list_test.dart index 473b695..d259e1f 100644 --- a/test/view/movie_list_test.dart +++ b/test/view/movie_list_test.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:release_schedule/model/dates.dart'; import 'package:release_schedule/model/movie.dart'; import 'package:release_schedule/view/movie_item.dart'; import 'package:release_schedule/view/movie_list.dart'; @@ -28,6 +29,8 @@ void main() { ), ); + await tester.pumpAndSettle(); + expect(find.byType(MovieItem), findsNWidgets(movies.length)); }); }); diff --git a/test/view/movie_manager_list_test.dart b/test/view/movie_manager_list_test.dart index c17c41a..27f7b92 100644 --- a/test/view/movie_manager_list_test.dart +++ b/test/view/movie_manager_list_test.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:release_schedule/api/movie_api.dart'; +import 'package:release_schedule/model/dates.dart'; import 'package:release_schedule/model/local_movie_storage.dart'; import 'package:release_schedule/model/movie.dart'; import 'package:release_schedule/model/movie_manager.dart';