feature: start movie list at current date

refactor: move DatePrecision and related logic into separate file
main
daniel-michel 2024-01-09 16:32:53 +01:00
parent 698c785896
commit dfde4d0aea
10 changed files with 193 additions and 104 deletions

View File

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

View File

@ -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<HomePage>
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")),
]),
],
),
);

View File

@ -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<DateWithPrecision> {
DateTime date;
DatePrecision precision;
DateWithPrecision(DateTime date, this.precision)
: date = simplifyDateToPrecision(date, precision);
DateWithPrecision.fromJsonEncodable(List<dynamic> json)
: date = DateTime.parse(json[0]),
precision = DatePrecision.values
.firstWhere((element) => element.name == json[1]);
DateWithPrecision.today() : this(DateTime.now().toUtc(), DatePrecision.day);
List<dynamic> 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;
}
}
}

View File

@ -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<DateWithPrecision> {
DateTime date;
DatePrecision precision;
DateWithPrecision(this.date, this.precision);
DateWithPrecision.fromJsonEncodable(List<dynamic> json)
: date = DateTime.parse(json[0]),
precision = DatePrecision.values
.firstWhere((element) => element.name == json[1]);
List<dynamic> 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;

View File

@ -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<int, DateWithPrecision>(
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<MovieData, DateWithPrecision>(
elements: movies,
floatingHeader: true,
@ -65,6 +116,7 @@ class MovieList extends StatelessWidget {
itemBuilder: (context, movie) {
return MovieItem(movie);
},
initialScrollIndex: firstMovieTodayOrAfterIndex,
);
}
}

View File

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

View File

@ -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() {

View File

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

View File

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

View File

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