diff --git a/lib/api/wikidata_movie_api.dart b/lib/api/wikidata_movie_api.dart index 695e7a3..4064a46 100644 --- a/lib/api/wikidata_movie_api.dart +++ b/lib/api/wikidata_movie_api.dart @@ -168,8 +168,8 @@ class WikidataMovieData extends MovieData { _precisionFromWikidata(value["precision"]), country); }).toList(); // Sort release dates with higher precision to the beginning - releaseDates - .sort((a, b) => -a.precision.index.compareTo(b.precision.index)); + releaseDates.sort((a, b) => -a.dateWithPrecision.precision.index + .compareTo(b.dateWithPrecision.precision.index)); List? genres = selectInJson( claims, "${WikidataProperties.genre}.*.mainsnak.datavalue.value.id") .map(_getCachedLabelForEntity) diff --git a/lib/main.dart b/lib/main.dart index 31b4648..5395eff 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.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_list.dart'; +import 'package:release_schedule/view/movie_item.dart'; import 'package:release_schedule/view/movie_manager_list.dart'; import 'package:release_schedule/view/swipe_transition.dart'; @@ -126,10 +126,20 @@ class SearchResultPage extends StatelessWidget { return AnimatedBuilder( animation: liveSearch, builder: (context, child) { - return Column(children: [ - liveSearch.loading ? const LinearProgressIndicator() : Container(), - Expanded(child: MovieList(liveSearch.searchResults)), - ]); + return Column( + children: [ + liveSearch.loading ? const LinearProgressIndicator() : Container(), + Expanded( + child: ListView.builder( + itemCount: liveSearch.searchResults.length, + itemBuilder: (context, index) => MovieItem( + liveSearch.searchResults[index], + showReleaseDate: true, + ), + ), + ), + ], + ); }, ); } @@ -158,7 +168,8 @@ 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.precision >= DatePrecision.month && + (movie.releaseDate.dateWithPrecision.precision >= + DatePrecision.month && (movie.titles?.length ?? 0) >= 1), ), floatingActionButton: FloatingActionButton( @@ -209,8 +220,9 @@ class HamburgerMenu extends StatelessWidget { onTap: () => manager.removeMoviesWhere((movie) => !movie.bookmarked && !(movie.releaseDates?.any((date) => - date.precision >= DatePrecision.month && - date.date.isAfter(DateTime.now() + date.dateWithPrecision.precision >= + DatePrecision.month && + date.dateWithPrecision.date.isAfter(DateTime.now() .subtract(const Duration(days: 30)))) ?? false)), ), diff --git a/lib/model/movie.dart b/lib/model/movie.dart index 198b220..361604f 100644 --- a/lib/model/movie.dart +++ b/lib/model/movie.dart @@ -97,7 +97,8 @@ class MovieData extends ChangeNotifier { } bool same(MovieData other) { - return title == other.title && releaseDate.date == other.releaseDate.date; + return title == other.title && + releaseDate.dateWithPrecision == other.releaseDate.dateWithPrecision; } Map toJsonEncodable() { @@ -166,29 +167,23 @@ extension DatePrecisionComparison on DatePrecision { typedef TitleInLanguage = ({String title, String language}); -class DateWithPrecisionAndCountry { +class DateWithPrecision implements Comparable { DateTime date; DatePrecision precision; - String country; - DateWithPrecisionAndCountry(this.date, this.precision, this.country); + DateWithPrecision(this.date, this.precision); - DateWithPrecisionAndCountry.fromJsonEncodable(List json) + DateWithPrecision.fromJsonEncodable(List json) : date = DateTime.parse(json[0]), precision = DatePrecision.values - .firstWhere((element) => element.name == json[1]), - country = json[2]; + .firstWhere((element) => element.name == json[1]); - toJsonEncodable() { - return [date.toIso8601String(), precision.name, country]; + List toJsonEncodable() { + return [date.toIso8601String(), precision.name]; } @override String toString() { - return "${toDateString()} ($country)"; - } - - String toDateString() { return switch (precision) { DatePrecision.decade => "${DateFormat("yyyy").format(date).substring(0, 3)}0s", @@ -199,6 +194,77 @@ class DateWithPrecisionAndCountry { 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; + + DateWithPrecisionAndCountry( + DateTime date, DatePrecision precision, this.country) + : dateWithPrecision = DateWithPrecision(date, precision); + + DateWithPrecisionAndCountry.fromJsonEncodable(List json) + : dateWithPrecision = DateWithPrecision.fromJsonEncodable(json), + country = json[2]; + + toJsonEncodable() { + return dateWithPrecision.toJsonEncodable() + [country]; + } + + @override + String toString() { + return "${dateWithPrecision.toString()} ($country)"; + } } class Review { diff --git a/lib/model/movie_manager.dart b/lib/model/movie_manager.dart index 6fb410f..e1da704 100644 --- a/lib/model/movie_manager.dart +++ b/lib/model/movie_manager.dart @@ -70,8 +70,8 @@ class MovieManager extends ChangeNotifier { int max = movies.length - 1; while (min <= max) { int center = (min + max) ~/ 2; - int diff = - movie.releaseDate.date.compareTo(movies[center].releaseDate.date); + int diff = movie.releaseDate.dateWithPrecision + .compareTo(movies[center].releaseDate.dateWithPrecision); if (diff < 0) { max = center - 1; } else { @@ -86,7 +86,12 @@ class MovieManager extends ChangeNotifier { var temp = movies[i]; int j = i - 1; for (; - j >= 0 && movies[j].releaseDate.date.isAfter(temp.releaseDate.date); + j >= 0 && + movies[j] + .releaseDate + .dateWithPrecision + .compareTo(temp.releaseDate.dateWithPrecision) > + 0; j--) { movies[j + 1] = movies[j]; } diff --git a/lib/view/movie_item.dart b/lib/view/movie_item.dart index 577d2b5..92f75ab 100644 --- a/lib/view/movie_item.dart +++ b/lib/view/movie_item.dart @@ -4,7 +4,8 @@ import 'package:release_schedule/view/movie_page.dart'; class MovieItem extends StatelessWidget { final MovieData movie; - const MovieItem(this.movie, {super.key}); + final bool showReleaseDate; + const MovieItem(this.movie, {this.showReleaseDate = false, super.key}); @override Widget build(BuildContext context) { @@ -13,7 +14,10 @@ class MovieItem extends StatelessWidget { builder: (context, widget) { return ListTile( title: Text(movie.title), - subtitle: Text(movie.genres?.join(", ") ?? ""), + subtitle: Text( + (showReleaseDate ? "${movie.releaseDate} " : "") + + (movie.genres?.join(", ") ?? ""), + ), trailing: IconButton( icon: Icon(movie.bookmarked ? Icons.bookmark_added diff --git a/lib/view/movie_list.dart b/lib/view/movie_list.dart index 0d43f9b..dd4ed01 100644 --- a/lib/view/movie_list.dart +++ b/lib/view/movie_list.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.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'; class MovieList extends StatelessWidget { final List movies; @@ -8,7 +9,32 @@ class MovieList extends StatelessWidget { const MovieList(this.movies, {this.filter, super.key}); @override - Widget build(Object context) { + Widget build(BuildContext context) { + Widget buildGroupSeparator(BuildContext context, DateWithPrecision date) { + bool highlight = date.includes(DateTime.now()); + return SizedBox( + height: 50, + child: Align( + alignment: Alignment.center, + child: Card( + elevation: 5, + color: highlight + ? Theme.of(context).colorScheme.primaryContainer + : null, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + child: Text( + date.toString(), + ), + ), + ), + ), + ); + } + final localFilter = filter; if (localFilter != null) { List indexMap = []; @@ -19,17 +45,25 @@ class MovieList extends StatelessWidget { } index++; } - return ListView.builder( - itemCount: indexMap.length, + return StickyGroupedListView( + elements: indexMap, + floatingHeader: true, + groupBy: (index) => movies[index].releaseDate.dateWithPrecision, + groupSeparatorBuilder: (index) => buildGroupSeparator( + context, movies[index].releaseDate.dateWithPrecision), itemBuilder: (context, index) { - return MovieItem(movies[indexMap[index]]); + return MovieItem(movies[index]); }, ); } - return ListView.builder( - itemCount: movies.length, - itemBuilder: (context, index) { - return MovieItem(movies[index]); + return StickyGroupedListView( + elements: movies, + floatingHeader: true, + groupBy: (movie) => movie.releaseDate.dateWithPrecision, + groupSeparatorBuilder: (movie) => + buildGroupSeparator(context, movie.releaseDate.dateWithPrecision), + itemBuilder: (context, movie) { + return MovieItem(movie); }, ); } diff --git a/lib/view/movie_page.dart b/lib/view/movie_page.dart index 43f4a4d..fe9d31e 100644 --- a/lib/view/movie_page.dart +++ b/lib/view/movie_page.dart @@ -95,7 +95,9 @@ class MoviePage extends StatelessWidget { TableCell( child: Padding( padding: const EdgeInsets.all(8.0), - child: Text(releaseDate.toDateString()), + child: Text( + releaseDate.dateWithPrecision.toString(), + ), )) ], ); diff --git a/pubspec.lock b/pubspec.lock index bedf7f7..dd26b1c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -219,6 +219,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.6" + scrollable_positioned_list: + dependency: transitive + description: + name: scrollable_positioned_list + sha256: "1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287" + url: "https://pub.dev" + source: hosted + version: "0.3.8" sky_engine: dependency: transitive description: flutter @@ -240,6 +248,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.11.0" + sticky_grouped_list: + dependency: "direct main" + description: + name: sticky_grouped_list + sha256: "40398cb90321f07cbdbdd3049c27a5f048bd75a13abb19453b07a956f53a0eda" + url: "https://pub.dev" + source: hosted + version: "3.1.0" stream_channel: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3407758..8a44edc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: http: ^1.1.0 intl: ^0.18.1 get_storage: ^2.1.1 + sticky_grouped_list: ^3.1.0 dev_dependencies: flutter_test: diff --git a/test/model/movie_test.dart b/test/model/movie_test.dart index 9e90147..4b1090d 100644 --- a/test/model/movie_test.dart +++ b/test/model/movie_test.dart @@ -136,8 +136,9 @@ void main() { DateTime(2023, 1, 1), DatePrecision.day, 'US'); final json = date.toJsonEncodable(); final date2 = DateWithPrecisionAndCountry.fromJsonEncodable(json); - expect(date2.date, equals(date.date)); - expect(date2.precision, equals(date.precision)); + expect(date2.dateWithPrecision, equals(date.dateWithPrecision)); + expect(date2.dateWithPrecision.precision, + equals(date.dateWithPrecision.precision)); expect(date2.country, equals(date.country)); });