feature: start movie list at current date
refactor: move DatePrecision and related logic into separate filemain
parent
698c785896
commit
dfde4d0aea
|
@ -6,6 +6,7 @@ import 'package:intl/intl.dart';
|
||||||
import 'package:release_schedule/api/api_manager.dart';
|
import 'package:release_schedule/api/api_manager.dart';
|
||||||
import 'package:release_schedule/api/json_helper.dart';
|
import 'package:release_schedule/api/json_helper.dart';
|
||||||
import 'package:release_schedule/api/movie_api.dart';
|
import 'package:release_schedule/api/movie_api.dart';
|
||||||
|
import 'package:release_schedule/model/dates.dart';
|
||||||
import 'package:release_schedule/model/movie.dart';
|
import 'package:release_schedule/model/movie.dart';
|
||||||
|
|
||||||
class WikidataProperties {
|
class WikidataProperties {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
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/live_search.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_item.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';
|
||||||
|
@ -65,6 +65,7 @@ class _HomePageState extends State<HomePage>
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
|
elevation: 1,
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
|
@ -159,6 +160,12 @@ class OverviewPage extends StatelessWidget {
|
||||||
length: 2,
|
length: 2,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
const TabBar(
|
||||||
|
tabs: [
|
||||||
|
Tab(icon: Icon(Icons.list), child: Text("Upcoming")),
|
||||||
|
Tab(icon: Icon(Icons.bookmark), child: Text("Bookmarked")),
|
||||||
|
],
|
||||||
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TabBarView(
|
child: TabBarView(
|
||||||
children: [
|
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")),
|
|
||||||
]),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:release_schedule/model/dates.dart';
|
||||||
|
|
||||||
class MovieData extends ChangeNotifier {
|
class MovieData extends ChangeNotifier {
|
||||||
String _title;
|
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});
|
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 {
|
class DateWithPrecisionAndCountry {
|
||||||
final DateWithPrecision dateWithPrecision;
|
final DateWithPrecision dateWithPrecision;
|
||||||
final String country;
|
final String country;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:release_schedule/model/dates.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';
|
import 'package:sticky_grouped_list/sticky_grouped_list.dart';
|
||||||
|
@ -10,6 +11,24 @@ class MovieList extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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) {
|
Widget buildGroupSeparator(BuildContext context, DateWithPrecision date) {
|
||||||
bool highlight = date.includes(DateTime.now());
|
bool highlight = date.includes(DateTime.now());
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
|
@ -45,6 +64,22 @@ class MovieList extends StatelessWidget {
|
||||||
}
|
}
|
||||||
index++;
|
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>(
|
return StickyGroupedListView<int, DateWithPrecision>(
|
||||||
elements: indexMap,
|
elements: indexMap,
|
||||||
floatingHeader: true,
|
floatingHeader: true,
|
||||||
|
@ -54,8 +89,24 @@ class MovieList extends StatelessWidget {
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return MovieItem(movies[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>(
|
return StickyGroupedListView<MovieData, DateWithPrecision>(
|
||||||
elements: movies,
|
elements: movies,
|
||||||
floatingHeader: true,
|
floatingHeader: true,
|
||||||
|
@ -65,6 +116,7 @@ class MovieList extends StatelessWidget {
|
||||||
itemBuilder: (context, movie) {
|
itemBuilder: (context, movie) {
|
||||||
return MovieItem(movie);
|
return MovieItem(movie);
|
||||||
},
|
},
|
||||||
|
initialScrollIndex: firstMovieTodayOrAfterIndex,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:release_schedule/api/movie_api.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/local_movie_storage.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';
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flutter_test/flutter_test.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/model/movie.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.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/model/movie.dart';
|
||||||
import 'package:release_schedule/view/movie_item.dart';
|
import 'package:release_schedule/view/movie_item.dart';
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.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/model/movie.dart';
|
||||||
import 'package:release_schedule/view/movie_item.dart';
|
import 'package:release_schedule/view/movie_item.dart';
|
||||||
import 'package:release_schedule/view/movie_list.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));
|
expect(find.byType(MovieItem), findsNWidgets(movies.length));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:release_schedule/api/movie_api.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/local_movie_storage.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';
|
||||||
|
|
Loading…
Reference in New Issue