add: group movies by release date
parent
497c2e6d2e
commit
698c785896
|
@ -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)
|
||||||
|
|
|
@ -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)),
|
||||||
),
|
),
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
|
),
|
||||||
))
|
))
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
16
pubspec.lock
16
pubspec.lock
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue