(add) history timeline ui

This commit is contained in:
Erfan Rahmati
2022-09-13 12:55:24 +04:30
parent 9f1ac25fbc
commit 7fa9d28003
10 changed files with 583 additions and 60 deletions

View File

@ -10,5 +10,6 @@ const List<String> months = [
'September',
'October',
'November',
'December'
'December',
''
];

View File

@ -23,4 +23,11 @@ enum ListName {
enum ItemType { MOVIE, TV, ARTIST, OTHER, UNKNOWN }
enum ShowType { SEARCH_PAGE, OFFICIAL_LIST, USER_LIST, EPISODE, BOX_OFFICE }
enum ShowType {
SEARCH_PAGE,
OFFICIAL_LIST,
USER_LIST,
USER_HISTORY,
EPISODE,
BOX_OFFICE
}

View File

@ -37,7 +37,11 @@ class PreferencesShareholder {
late HiveShowPreview hiveShow;
Box<HiveShowPreview> list = Hive.box<HiveShowPreview>(listName);
if (fullShow != null) {
hiveShow = await convertFullShowToHive(fullShow: fullShow);
hiveShow = await convertFullShowToHive(
fullShow: fullShow,
date: date,
time: time,
);
} else {
hiveShow = convertShowPreviewToHive(
showPreview: showPreview,

View File

@ -5,6 +5,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:hive_flutter/adapters.dart';
import 'package:movielab/constants/colors.dart';
import 'package:movielab/constants/types.dart';
import 'package:movielab/models/hive/convertor.dart';
import 'package:movielab/models/hive/models/show_preview.dart';
import 'package:movielab/models/show_models/show_preview_model.dart';
@ -17,6 +18,8 @@ import 'package:movielab/widgets/inefficacious_refresh_indicator.dart';
import 'package:movielab/widgets/toast.dart';
import 'package:ms_undraw/ms_undraw.dart';
import 'sections/history_timeline.dart';
class ListPage extends StatefulWidget {
final String listName;
const ListPage({Key? key, required this.listName}) : super(key: key);
@ -145,18 +148,49 @@ class _ListPageState extends State<ListPage> {
builder: (context, box, _) {
final list = box.values.toList().cast<HiveShowPreview>();
return list.isNotEmpty
? InefficaciousRefreshIndicator(
child: ListView.builder(
itemCount: list.length,
physics: const BouncingScrollPhysics(),
itemBuilder: (context, index) {
return ListShowBox(
listName: widget.listName,
showPreview: convertHiveToShowPreview(
list[list.length - index - 1]));
},
),
)
? widget.listName == "history"
? Column(
children: [
Expanded(
child: CustomScrollView(
slivers: [
TimelineSteps(
steps: [
for (HiveShowPreview show in list)
ListShowBox(
listName: widget.listName,
width: MediaQuery.of(context)
.size
.width -
76,
showPreview:
convertHiveToShowPreview(
show),
showType: ShowType.USER_HISTORY)
],
watchDates: [
for (HiveShowPreview show in list)
convertHiveToShowPreview(show)
.watchDate
],
)
],
),
),
],
)
: InefficaciousRefreshIndicator(
child: ListView.builder(
itemCount: list.length,
physics: const BouncingScrollPhysics(),
itemBuilder: (context, index) {
return ListShowBox(
listName: widget.listName,
showPreview: convertHiveToShowPreview(
list[list.length - index - 1]));
},
),
)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [

View File

@ -0,0 +1,131 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:movielab/constants/colors.dart';
import 'package:movielab/constants/general.dart';
import 'package:movielab/pages/show/show_box/lists_show_box.dart';
import 'package:timeline_tile/timeline_tile.dart';
class TimelineSteps extends StatelessWidget {
const TimelineSteps({Key? key, required this.steps, required this.watchDates})
: super(key: key);
final List<ListShowBox> steps;
final List<DateTime?> watchDates;
@override
Widget build(BuildContext context) {
return SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
if (index.isOdd) {
return const TimelineDivider(
color: kPrimaryColor,
thickness: 7.5,
begin: 0.1,
end: 0.9,
);
}
final int itemIndex = index ~/ 2;
final ListShowBox step = steps[itemIndex];
final bool isLeftAlign = itemIndex.isEven;
final child = _TimelineStepsChild(
step: step,
isLeftAlign: isLeftAlign,
);
final isFirst = itemIndex == 0;
final isLast = itemIndex == steps.length - 1;
double indicatorY;
if (isFirst) {
indicatorY = 0.2;
} else if (isLast) {
indicatorY = 0.8;
} else {
indicatorY = 0.5;
}
return TimelineTile(
alignment: TimelineAlign.manual,
endChild: isLeftAlign ? child : null,
startChild: isLeftAlign ? null : child,
lineXY: isLeftAlign ? 0.1 : 0.9,
isFirst: isFirst,
isLast: isLast,
indicatorStyle: IndicatorStyle(
width: 67.5,
height: 25,
indicatorXY: indicatorY,
indicator: _TimelineStepIndicator(
step:
'${months[(watchDates[itemIndex]?.month ?? 14) - 1].substring(0, 3)} ${watchDates[itemIndex]?.year}'),
),
beforeLineStyle: const LineStyle(
color: kPrimaryColor,
thickness: 7.5,
),
);
},
childCount: max(0, steps.length * 2 - 1),
),
);
}
}
class _TimelineStepIndicator extends StatelessWidget {
const _TimelineStepIndicator({Key? key, required this.step})
: super(key: key);
final String step;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: kPrimaryColor, borderRadius: BorderRadius.circular(8.5)),
child: Center(
child: Text(
step,
style: const TextStyle(
fontSize: 13.5,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
);
}
}
class _TimelineStepsChild extends StatelessWidget {
const _TimelineStepsChild({
Key? key,
required this.isLeftAlign,
required this.step,
}) : super(key: key);
final ListShowBox step;
final bool isLeftAlign;
@override
Widget build(BuildContext context) {
return Padding(
padding: isLeftAlign
? const EdgeInsets.only(
right: 20,
top: 16,
bottom: 16,
)
: const EdgeInsets.only(left: 20, top: 16, bottom: 16),
child: Column(
crossAxisAlignment:
isLeftAlign ? CrossAxisAlignment.end : CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [step],
),
);
}
}

View File

@ -15,11 +15,13 @@ class ExpandedItemBox extends StatefulWidget {
final String? iRank;
final String preTag;
final ShowType showType;
final double? width;
const ExpandedItemBox(
{Key? key,
required this.show,
this.iRank,
this.preTag = "",
this.width,
required this.showType})
: super(key: key);
@ -68,7 +70,7 @@ class _ExpandedItemBoxState extends State<ExpandedItemBox>
borderRadius: BorderRadius.circular(15),
child: Container(
margin: const EdgeInsets.only(left: 8.5, top: 10),
width: MediaQuery.of(context).size.width,
width: widget.width ?? MediaQuery.of(context).size.width,
height: widget.show.image != 'null' ? 160 : 205,
child: Row(
children: [
@ -170,8 +172,10 @@ class _ExpandedItemBoxState extends State<ExpandedItemBox>
Container(
alignment: Alignment.bottomLeft,
width: widget.show.image != 'null'
? MediaQuery.of(context).size.width - 160
: MediaQuery.of(context).size.width - 40,
? (widget.width ?? MediaQuery.of(context).size.width) -
160
: (widget.width ?? MediaQuery.of(context).size.width) -
40,
padding: const EdgeInsets.only(left: 10, top: 5),
child: Column(
children: [
@ -180,7 +184,8 @@ class _ExpandedItemBoxState extends State<ExpandedItemBox>
children: [
Flexible(
child: showBoxText(
text: widget.showType == ShowType.USER_LIST
text: widget.showType == ShowType.USER_LIST ||
widget.showType == ShowType.USER_HISTORY
? widget.show.title
: widget.show.rank != ""
? "${widget.show.rank}. ${widget.show.title}"
@ -235,8 +240,16 @@ class _ExpandedItemBoxState extends State<ExpandedItemBox>
children: [
SizedBox(
width: widget.show.image != 'null'
? MediaQuery.of(context).size.width - 180
: MediaQuery.of(context).size.width - 40,
? (widget.width ??
MediaQuery.of(context)
.size
.width) -
180
: (widget.width ??
MediaQuery.of(context)
.size
.width) -
40,
child: showBoxText(
text: widget.show.released != ""
? widget.show.released ??

View File

@ -11,13 +11,18 @@ import 'package:movielab/modules/tools/capitalizer.dart';
import 'package:movielab/pages/show/show_box/expanded_item_box/expanded_item_box.dart';
import 'package:movielab/widgets/toast.dart';
// ignore: must_be_immutable
class ListShowBox extends StatelessWidget {
final ShowPreview showPreview;
final String listName;
final double? width;
final ShowType? showType;
ListShowBox({
Key? key,
required this.showPreview,
required this.listName,
this.width,
this.showType,
}) : super(key: key);
late FToast fToast;
@ -26,48 +31,46 @@ class ListShowBox extends StatelessWidget {
fToast = FToast();
fToast.init(context);
String id = showPreview.id;
return Padding(
padding: const EdgeInsets.all(5),
child: Slidable(
key: UniqueKey(),
startActionPane: ActionPane(
motion: const ScrollMotion(),
dismissible: DismissiblePane(
onDismissed: () => delete(),
),
children: [
SlidableAction(
onPressed: (context) => delete(),
autoClose: true,
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
icon: Icons.delete,
label: 'Delete',
),
],
return Slidable(
key: UniqueKey(),
startActionPane: ActionPane(
motion: const ScrollMotion(),
dismissible: DismissiblePane(
onDismissed: () => delete(),
),
endActionPane: ActionPane(
motion: const ScrollMotion(),
dismissible: DismissiblePane(
onDismissed: () => delete(),
children: [
SlidableAction(
onPressed: (context) => delete(),
autoClose: true,
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
icon: Icons.delete,
label: 'Delete',
),
children: [
SlidableAction(
onPressed: (context) => delete(),
autoClose: true,
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
icon: Icons.delete,
label: 'Delete',
),
],
],
),
endActionPane: ActionPane(
motion: const ScrollMotion(),
dismissible: DismissiblePane(
onDismissed: () => delete(),
),
child: ExpandedItemBox(
show: showPreview,
preTag: "${listName}_",
showType: ShowType.USER_LIST,
)),
);
children: [
SlidableAction(
onPressed: (context) => delete(),
autoClose: true,
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
icon: Icons.delete,
label: 'Delete',
),
],
),
child: ExpandedItemBox(
show: showPreview,
preTag: "${listName}_",
width: width,
showType: showType ?? ShowType.USER_LIST,
));
}
Future<void> delete() async {

322
lib/widgets/timeline.dart Normal file
View File

@ -0,0 +1,322 @@
import 'package:flutter/material.dart';
import 'package:movielab/pages/show/show_box/compressed_item_box/compressed_item_box.dart';
class Config {
static String rightDirection = 'R';
static String leftDirection = 'L';
}
/// A Bubble Timeline Widget.
class BubbleTimeline extends StatefulWidget {
final double bubbleDiameter;
final List<CompressedItemBox> items;
final Color stripColor;
/// This is color of your scaffold.
/// Use same color as used for Scaffold background.
final Color scaffoldColor;
const BubbleTimeline({
required this.bubbleDiameter,
required this.items,
required this.stripColor,
required this.scaffoldColor,
});
@override
_BubbleTimelineState createState() => _BubbleTimelineState();
}
class _BubbleTimelineState extends State<BubbleTimeline> {
bool checkEven(int n) {
return n % 2 == 0;
}
// List<TimelineBubble> createTimeline() {
// final List<TimelineBubble> items = [];
// for (var i = 0; i < widget.items.length; i++) {
// items.add(
// TimelineBubble(
// direction:
// checkEven(i) ? Config.leftDirection : Config.rightDirection,
// size: widget.bubbleDiameter,
// title: widget.items[i].title,
// subtitle: widget.items[i].subtitle,
// description: widget.items[i].description,
// icon: widget.items[i].child,
// stripColor: widget.stripColor,
// bubbleColor: widget.items[i].bubbleColor,
// bgColor: widget.scaffoldColor,
// ),
// );
// }
// return items;
// }
@override
Widget build(BuildContext context) {
return Center(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TopHandle(widget.stripColor),
...widget.items,
BottomHandle(widget.stripColor),
],
),
),
);
}
}
class TopHandle extends StatelessWidget {
final Color handleColor;
const TopHandle(this.handleColor);
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Container(
decoration: BoxDecoration(
color: handleColor,
shape: BoxShape.circle,
),
height: 20,
),
Container(
height: 20,
width: 5,
color: handleColor,
),
],
);
}
}
class BottomHandle extends StatelessWidget {
final Color handleColor;
const BottomHandle(this.handleColor);
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Container(
height: 20,
width: 5,
color: handleColor,
),
Container(
decoration: BoxDecoration(
color: handleColor,
shape: BoxShape.circle,
),
height: 20,
),
],
);
}
}
class TimelineBubble extends StatelessWidget {
final String direction;
final double size;
final String title;
final String subtitle;
final String description;
final Widget icon;
final Color stripColor;
final Color bgColor;
final Color bubbleColor;
const TimelineBubble({
required this.direction,
required this.size,
required this.title,
required this.subtitle,
required this.description,
required this.icon,
required this.stripColor,
required this.bgColor,
required this.bubbleColor,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: direction == Config.leftDirection
? <Widget>[
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w700,
),
textAlign: TextAlign.right,
),
if (subtitle != null) ...[
const SizedBox(
height: 5,
),
Text(
subtitle,
textAlign: TextAlign.right,
),
],
if (description != null) ...[
const SizedBox(
height: 5,
),
Text(
description,
textAlign: TextAlign.right,
),
],
]
: [],
),
),
Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
height: 10,
width: 5,
color: stripColor,
),
Container(
alignment: Alignment.center,
child: Stack(
alignment: Alignment.center,
children: <Widget>[
SizedBox(
height: size,
child: ClipPath(
clipper: direction == Config.leftDirection
? LeftClipper()
: RightClipper(),
child: Container(
width: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: stripColor,
),
),
),
),
Container(
height: size - 10,
width: size - 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: bgColor,
),
),
Container(
height: size - 20,
width: size - 20,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: bubbleColor,
),
alignment: Alignment.center,
child: icon,
),
],
),
),
Container(height: 10, width: 5, color: stripColor),
],
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: direction == Config.rightDirection
? <Widget>[
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w700,
),
textAlign: TextAlign.left,
),
if (subtitle != null) ...[
const SizedBox(
height: 5,
),
Text(
subtitle,
textAlign: TextAlign.left,
),
],
if (description != null) ...[
const SizedBox(
height: 5,
),
Text(
description,
textAlign: TextAlign.left,
),
],
]
: [],
),
),
],
);
}
}
class RightClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
final path = Path();
path.lineTo(0, size.height);
path.lineTo(size.width / 2 + 3, size.height);
path.lineTo(size.width / 2 + 3, 0);
path.close();
return path;
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}
class LeftClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
final path = Path();
path.moveTo(size.width, 0);
path.lineTo(size.width, size.height);
path.lineTo(size.width / 2 - 3, size.height);
path.lineTo(size.width / 2 - 3, 0);
path.close();
return path;
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}
class TimelineItem {
final String title;
final String subtitle;
final String description;
final Widget child;
final Color bubbleColor;
const TimelineItem({
required this.title,
this.subtitle = '',
this.description = '',
required this.child,
required this.bubbleColor,
});
}

View File

@ -1015,6 +1015,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.13"
timeline_tile:
dependency: "direct main"
description:
name: timeline_tile
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
timing:
dependency: transitive
description:

View File

@ -47,6 +47,7 @@ dependencies:
file_picker: ^5.0.1
flashy_tab_bar2: ^0.0.4
animated_theme_switcher: ^2.0.6
timeline_tile: ^2.0.0