add: group movies by release date

main
daniel-michel 2024-01-09 14:48:36 +01:00
parent 497c2e6d2e
commit 698c785896
10 changed files with 180 additions and 39 deletions

View File

@ -168,8 +168,8 @@ class WikidataMovieData extends MovieData {
_precisionFromWikidata(value["precision"]), country); _precisionFromWikidata(value["precision"]), country);
}).toList(); }).toList();
// Sort release dates with higher precision to the beginning // Sort release dates with higher precision to the beginning
releaseDates releaseDates.sort((a, b) => -a.dateWithPrecision.precision.index
.sort((a, b) => -a.precision.index.compareTo(b.precision.index)); .compareTo(b.dateWithPrecision.precision.index));
List<String>? genres = selectInJson<String>( List<String>? genres = selectInJson<String>(
claims, "${WikidataProperties.genre}.*.mainsnak.datavalue.value.id") claims, "${WikidataProperties.genre}.*.mainsnak.datavalue.value.id")
.map(_getCachedLabelForEntity) .map(_getCachedLabelForEntity)

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:release_schedule/model/live_search.dart'; import 'package:release_schedule/model/live_search.dart';
import 'package:release_schedule/model/movie.dart'; import 'package:release_schedule/model/movie.dart';
import 'package:release_schedule/model/movie_manager.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/movie_manager_list.dart';
import 'package:release_schedule/view/swipe_transition.dart'; import 'package:release_schedule/view/swipe_transition.dart';
@ -126,10 +126,20 @@ class SearchResultPage extends StatelessWidget {
return AnimatedBuilder( return AnimatedBuilder(
animation: liveSearch, animation: liveSearch,
builder: (context, child) { builder: (context, child) {
return Column(children: [ return Column(
children: [
liveSearch.loading ? const LinearProgressIndicator() : Container(), liveSearch.loading ? const LinearProgressIndicator() : Container(),
Expanded(child: MovieList(liveSearch.searchResults)), 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 // 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.precision >= DatePrecision.month && (movie.releaseDate.dateWithPrecision.precision >=
DatePrecision.month &&
(movie.titles?.length ?? 0) >= 1), (movie.titles?.length ?? 0) >= 1),
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
@ -209,8 +220,9 @@ class HamburgerMenu extends StatelessWidget {
onTap: () => manager.removeMoviesWhere((movie) => onTap: () => manager.removeMoviesWhere((movie) =>
!movie.bookmarked && !movie.bookmarked &&
!(movie.releaseDates?.any((date) => !(movie.releaseDates?.any((date) =>
date.precision >= DatePrecision.month && date.dateWithPrecision.precision >=
date.date.isAfter(DateTime.now() DatePrecision.month &&
date.dateWithPrecision.date.isAfter(DateTime.now()
.subtract(const Duration(days: 30)))) ?? .subtract(const Duration(days: 30)))) ??
false)), false)),
), ),

View File

@ -97,7 +97,8 @@ class MovieData extends ChangeNotifier {
} }
bool same(MovieData other) { bool same(MovieData other) {
return title == other.title && releaseDate.date == other.releaseDate.date; return title == other.title &&
releaseDate.dateWithPrecision == other.releaseDate.dateWithPrecision;
} }
Map toJsonEncodable() { Map toJsonEncodable() {
@ -166,29 +167,23 @@ extension DatePrecisionComparison on DatePrecision {
typedef TitleInLanguage = ({String title, String language}); typedef TitleInLanguage = ({String title, String language});
class DateWithPrecisionAndCountry { class DateWithPrecision implements Comparable<DateWithPrecision> {
DateTime date; DateTime date;
DatePrecision precision; DatePrecision precision;
String country;
DateWithPrecisionAndCountry(this.date, this.precision, this.country); DateWithPrecision(this.date, this.precision);
DateWithPrecisionAndCountry.fromJsonEncodable(List<dynamic> json) DateWithPrecision.fromJsonEncodable(List<dynamic> json)
: date = DateTime.parse(json[0]), : date = DateTime.parse(json[0]),
precision = DatePrecision.values precision = DatePrecision.values
.firstWhere((element) => element.name == json[1]), .firstWhere((element) => element.name == json[1]);
country = json[2];
toJsonEncodable() { List<dynamic> toJsonEncodable() {
return [date.toIso8601String(), precision.name, country]; return [date.toIso8601String(), precision.name];
} }
@override @override
String toString() { String toString() {
return "${toDateString()} ($country)";
}
String toDateString() {
return switch (precision) { return switch (precision) {
DatePrecision.decade => DatePrecision.decade =>
"${DateFormat("yyyy").format(date).substring(0, 3)}0s", "${DateFormat("yyyy").format(date).substring(0, 3)}0s",
@ -199,6 +194,77 @@ class DateWithPrecisionAndCountry {
DatePrecision.minute => DateFormat("MMMM d, yyyy, HH:mm").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;
DateWithPrecisionAndCountry(
DateTime date, DatePrecision precision, this.country)
: dateWithPrecision = DateWithPrecision(date, precision);
DateWithPrecisionAndCountry.fromJsonEncodable(List<dynamic> json)
: dateWithPrecision = DateWithPrecision.fromJsonEncodable(json),
country = json[2];
toJsonEncodable() {
return dateWithPrecision.toJsonEncodable() + [country];
}
@override
String toString() {
return "${dateWithPrecision.toString()} ($country)";
}
} }
class Review { class Review {

View File

@ -70,8 +70,8 @@ class MovieManager extends ChangeNotifier {
int max = movies.length - 1; int max = movies.length - 1;
while (min <= max) { while (min <= max) {
int center = (min + max) ~/ 2; int center = (min + max) ~/ 2;
int diff = int diff = movie.releaseDate.dateWithPrecision
movie.releaseDate.date.compareTo(movies[center].releaseDate.date); .compareTo(movies[center].releaseDate.dateWithPrecision);
if (diff < 0) { if (diff < 0) {
max = center - 1; max = center - 1;
} else { } else {
@ -86,7 +86,12 @@ class MovieManager extends ChangeNotifier {
var temp = movies[i]; var temp = movies[i];
int j = i - 1; int j = i - 1;
for (; for (;
j >= 0 && movies[j].releaseDate.date.isAfter(temp.releaseDate.date); j >= 0 &&
movies[j]
.releaseDate
.dateWithPrecision
.compareTo(temp.releaseDate.dateWithPrecision) >
0;
j--) { j--) {
movies[j + 1] = movies[j]; movies[j + 1] = movies[j];
} }

View File

@ -4,7 +4,8 @@ import 'package:release_schedule/view/movie_page.dart';
class MovieItem extends StatelessWidget { class MovieItem extends StatelessWidget {
final MovieData movie; final MovieData movie;
const MovieItem(this.movie, {super.key}); final bool showReleaseDate;
const MovieItem(this.movie, {this.showReleaseDate = false, super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -13,7 +14,10 @@ class MovieItem extends StatelessWidget {
builder: (context, widget) { builder: (context, widget) {
return ListTile( return ListTile(
title: Text(movie.title), title: Text(movie.title),
subtitle: Text(movie.genres?.join(", ") ?? ""), subtitle: Text(
(showReleaseDate ? "${movie.releaseDate} " : "") +
(movie.genres?.join(", ") ?? ""),
),
trailing: IconButton( trailing: IconButton(
icon: Icon(movie.bookmarked icon: Icon(movie.bookmarked
? Icons.bookmark_added ? Icons.bookmark_added

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:release_schedule/model/movie.dart'; import 'package:release_schedule/model/movie.dart';
import 'package:release_schedule/view/movie_item.dart'; import 'package:release_schedule/view/movie_item.dart';
import 'package:sticky_grouped_list/sticky_grouped_list.dart';
class MovieList extends StatelessWidget { class MovieList extends StatelessWidget {
final List<MovieData> movies; final List<MovieData> movies;
@ -8,7 +9,32 @@ class MovieList extends StatelessWidget {
const MovieList(this.movies, {this.filter, super.key}); const MovieList(this.movies, {this.filter, super.key});
@override @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; final localFilter = filter;
if (localFilter != null) { if (localFilter != null) {
List<int> indexMap = []; List<int> indexMap = [];
@ -19,18 +45,26 @@ class MovieList extends StatelessWidget {
} }
index++; index++;
} }
return ListView.builder( return StickyGroupedListView<int, DateWithPrecision>(
itemCount: indexMap.length, elements: indexMap,
itemBuilder: (context, index) { floatingHeader: true,
return MovieItem(movies[indexMap[index]]); groupBy: (index) => movies[index].releaseDate.dateWithPrecision,
}, groupSeparatorBuilder: (index) => buildGroupSeparator(
); context, movies[index].releaseDate.dateWithPrecision),
}
return ListView.builder(
itemCount: movies.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return MovieItem(movies[index]); return MovieItem(movies[index]);
}, },
); );
} }
return StickyGroupedListView<MovieData, DateWithPrecision>(
elements: movies,
floatingHeader: true,
groupBy: (movie) => movie.releaseDate.dateWithPrecision,
groupSeparatorBuilder: (movie) =>
buildGroupSeparator(context, movie.releaseDate.dateWithPrecision),
itemBuilder: (context, movie) {
return MovieItem(movie);
},
);
}
} }

View File

@ -95,7 +95,9 @@ class MoviePage extends StatelessWidget {
TableCell( TableCell(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Text(releaseDate.toDateString()), child: Text(
releaseDate.dateWithPrecision.toString(),
),
)) ))
], ],
); );

View File

@ -219,6 +219,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.6" 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: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -240,6 +248,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.11.0" 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: stream_channel:
dependency: transitive dependency: transitive
description: description:

View File

@ -14,6 +14,7 @@ dependencies:
http: ^1.1.0 http: ^1.1.0
intl: ^0.18.1 intl: ^0.18.1
get_storage: ^2.1.1 get_storage: ^2.1.1
sticky_grouped_list: ^3.1.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -136,8 +136,9 @@ void main() {
DateTime(2023, 1, 1), DatePrecision.day, 'US'); DateTime(2023, 1, 1), DatePrecision.day, 'US');
final json = date.toJsonEncodable(); final json = date.toJsonEncodable();
final date2 = DateWithPrecisionAndCountry.fromJsonEncodable(json); final date2 = DateWithPrecisionAndCountry.fromJsonEncodable(json);
expect(date2.date, equals(date.date)); expect(date2.dateWithPrecision, equals(date.dateWithPrecision));
expect(date2.precision, equals(date.precision)); expect(date2.dateWithPrecision.precision,
equals(date.dateWithPrecision.precision));
expect(date2.country, equals(date.country)); expect(date2.country, equals(date.country));
}); });