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);
}).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<String>? genres = selectInJson<String>(
claims, "${WikidataProperties.genre}.*.mainsnak.datavalue.value.id")
.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/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: [
return Column(
children: [
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
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)),
),

View File

@ -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<DateWithPrecision> {
DateTime date;
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]),
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<dynamic> 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<dynamic> json)
: dateWithPrecision = DateWithPrecision.fromJsonEncodable(json),
country = json[2];
toJsonEncodable() {
return dateWithPrecision.toJsonEncodable() + [country];
}
@override
String toString() {
return "${dateWithPrecision.toString()} ($country)";
}
}
class Review {

View File

@ -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];
}

View File

@ -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

View File

@ -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<MovieData> 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<int> indexMap = [];
@ -19,18 +45,26 @@ class MovieList extends StatelessWidget {
}
index++;
}
return ListView.builder(
itemCount: indexMap.length,
itemBuilder: (context, index) {
return MovieItem(movies[indexMap[index]]);
},
);
}
return ListView.builder(
itemCount: movies.length,
return StickyGroupedListView<int, DateWithPrecision>(
elements: indexMap,
floatingHeader: true,
groupBy: (index) => movies[index].releaseDate.dateWithPrecision,
groupSeparatorBuilder: (index) => buildGroupSeparator(
context, movies[index].releaseDate.dateWithPrecision),
itemBuilder: (context, 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(
child: Padding(
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"
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:

View File

@ -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:

View File

@ -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));
});