clean up
This commit is contained in:
38
lib/pages/widgets/badge.dart
Normal file
38
lib/pages/widgets/badge.dart
Normal file
@@ -0,0 +1,38 @@
|
||||
import 'package:fcs/helpers/theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Widget badgeCounter(Widget child, int counter) {
|
||||
return Container(
|
||||
width: 120,
|
||||
height: 140,
|
||||
child: new Stack(
|
||||
children: <Widget>[
|
||||
child,
|
||||
counter != null && counter != 0
|
||||
? new Positioned(
|
||||
right: 12,
|
||||
top: 12,
|
||||
child: new Container(
|
||||
padding: EdgeInsets.all(8),
|
||||
decoration: new BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: secondaryColor,
|
||||
),
|
||||
constraints: BoxConstraints(
|
||||
minWidth: 14,
|
||||
minHeight: 14,
|
||||
),
|
||||
child: Text(
|
||||
counter > 99 ? '+99' : '$counter',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
)
|
||||
: new Container()
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
30
lib/pages/widgets/banner.dart
Normal file
30
lib/pages/widgets/banner.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
import 'package:fcs/config.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FlavorBanner extends StatelessWidget {
|
||||
final Widget child;
|
||||
FlavorBanner({@required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (Config.isProduction()) return child;
|
||||
return Stack(
|
||||
children: <Widget>[child, _buildBanner(context)],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBanner(BuildContext context) {
|
||||
return Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
child: CustomPaint(
|
||||
painter: BannerPainter(
|
||||
message: Config.instance.name,
|
||||
textDirection: Directionality.of(context),
|
||||
layoutDirection: Directionality.of(context),
|
||||
location: BannerLocation.topStart,
|
||||
color: Config.instance.color),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
23
lib/pages/widgets/bottom_up_page_route.dart
Normal file
23
lib/pages/widgets/bottom_up_page_route.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class BottomUpPageRoute<T> extends PageRouteBuilder<T> {
|
||||
final Widget child;
|
||||
|
||||
BottomUpPageRoute(this.child)
|
||||
: super(
|
||||
pageBuilder: (context, animation, secondaryAnimation) => child,
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||
var begin = Offset(0.0, 1.0);
|
||||
var end = Offset.zero;
|
||||
var curve = Curves.ease;
|
||||
|
||||
var tween =
|
||||
Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
|
||||
|
||||
return SlideTransition(
|
||||
position: animation.drive(tween),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
49
lib/pages/widgets/bottom_widgets.dart
Normal file
49
lib/pages/widgets/bottom_widgets.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
import 'package:fcs/pages/contact/contact_page.dart';
|
||||
import 'package:fcs/pages/term/term_page.dart';
|
||||
import 'package:fcs/pages/widgets/bottom_up_page_route.dart';
|
||||
import 'package:fcs/pages/widgets/local_text.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_icons/flutter_icons.dart';
|
||||
|
||||
class BottomWidgets extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: <Widget>[
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).push(BottomUpPageRoute(ContactPage()));
|
||||
},
|
||||
child: _buildSmallButton(
|
||||
context, "contact.btn", SimpleLineIcons.support),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).push(BottomUpPageRoute(TermPage()));
|
||||
},
|
||||
child: _buildSmallButton(context, "term.btn", Icons.info_outline),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSmallButton(
|
||||
BuildContext context, String textKey, IconData iconData) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(18.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 5),
|
||||
child: Icon(iconData, color: Colors.white70, size: 20),
|
||||
),
|
||||
LocalText(context, textKey, color: Colors.white70)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
30
lib/pages/widgets/display_image_source.dart
Normal file
30
lib/pages/widgets/display_image_source.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class DisplayImageSource {
|
||||
String url;
|
||||
File file;
|
||||
DisplayImageSource({this.url, this.file});
|
||||
|
||||
ImageProvider get imageProvider =>
|
||||
file == null ? CachedNetworkImageProvider(url) : FileImage(file);
|
||||
|
||||
@override
|
||||
bool operator ==(other) {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return (other.file == this.file &&
|
||||
(other.file != null || this.file != null)) ||
|
||||
(other.url == this.url && (other.url != null || this.url != null));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
int result = 17;
|
||||
result = 37 * result + file.hashCode;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
75
lib/pages/widgets/display_text.dart
Normal file
75
lib/pages/widgets/display_text.dart
Normal file
@@ -0,0 +1,75 @@
|
||||
import 'package:fcs/helpers/theme.dart';
|
||||
import 'package:fcs/pages/main/model/language_model.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class DisplayText extends StatelessWidget {
|
||||
final String text;
|
||||
final String labelText;
|
||||
final IconData iconData;
|
||||
final int maxLines;
|
||||
final bool withBorder;
|
||||
final Color borderColor;
|
||||
final Widget icon;
|
||||
|
||||
const DisplayText({
|
||||
Key key,
|
||||
this.text,
|
||||
this.labelText,
|
||||
this.iconData,
|
||||
this.maxLines = 1,
|
||||
this.withBorder = false,
|
||||
this.borderColor,
|
||||
this.icon,
|
||||
}) : super(key: key);
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var languageModel = Provider.of<LanguageModel>(context);
|
||||
|
||||
var labelStyle = languageModel.isEng
|
||||
? TextStyle(
|
||||
color: Colors.black54,
|
||||
)
|
||||
: TextStyle(color: Colors.black54, fontFamily: "Myanmar3");
|
||||
var textStyle = languageModel.isEng
|
||||
? TextStyle(
|
||||
color: primaryColor,
|
||||
)
|
||||
: TextStyle(color: primaryColor, fontFamily: "Myanmar3");
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0, bottom: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
iconData == null
|
||||
? icon == null ? Container() : icon
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Icon(
|
||||
iconData,
|
||||
color: primaryColor,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
labelText == null
|
||||
? Container()
|
||||
: Text(
|
||||
labelText,
|
||||
style: labelStyle,
|
||||
),
|
||||
Text(
|
||||
text,
|
||||
style: textStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
71
lib/pages/widgets/fcs_expansion_tile.dart
Normal file
71
lib/pages/widgets/fcs_expansion_tile.dart
Normal file
@@ -0,0 +1,71 @@
|
||||
import 'package:fcs/helpers/theme.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'multi_img_controller.dart';
|
||||
|
||||
class FcsExpansionTile extends StatefulWidget {
|
||||
final ValueChanged<bool> onExpansionChanged;
|
||||
final CallBack onEditPress;
|
||||
final List<Widget> children;
|
||||
final Widget title;
|
||||
final bool isEdit;
|
||||
const FcsExpansionTile(
|
||||
{this.onExpansionChanged,
|
||||
this.children,
|
||||
this.title,
|
||||
this.isEdit = false,
|
||||
this.onEditPress});
|
||||
@override
|
||||
_FcsExpansionTileState createState() => _FcsExpansionTileState();
|
||||
}
|
||||
|
||||
class _FcsExpansionTileState extends State<FcsExpansionTile> {
|
||||
bool expanded;
|
||||
@override
|
||||
void initState() {
|
||||
this.expanded = false;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Theme(
|
||||
data: ThemeData(
|
||||
accentColor: primaryColor, dividerColor: Colors.transparent),
|
||||
child: ExpansionTile(
|
||||
onExpansionChanged: (value) {
|
||||
setState(() {
|
||||
expanded = value;
|
||||
});
|
||||
if (widget.onExpansionChanged != null)
|
||||
widget.onExpansionChanged(value);
|
||||
},
|
||||
title: widget.title,
|
||||
children: widget.children,
|
||||
trailing: widget.isEdit
|
||||
? IconButton(
|
||||
padding: EdgeInsets.all(0),
|
||||
iconSize: 20,
|
||||
onPressed: () =>
|
||||
widget.onEditPress != null ? widget.onEditPress() : {},
|
||||
icon: Icon(
|
||||
Icons.edit,
|
||||
color: primaryColor,
|
||||
))
|
||||
: AnimatedSwitcher(
|
||||
child: expanded
|
||||
? Icon(
|
||||
Icons.remove,
|
||||
color: primaryColor,
|
||||
)
|
||||
: Icon(
|
||||
Icons.add,
|
||||
color: primaryColor,
|
||||
),
|
||||
duration: Duration(seconds: 2)),
|
||||
childrenPadding: EdgeInsets.all(18),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
20
lib/pages/widgets/fcs_id_icon.dart
Normal file
20
lib/pages/widgets/fcs_id_icon.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class FcsIDIcon extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Container(
|
||||
width: 25,
|
||||
height: 25,
|
||||
child: FittedBox(
|
||||
child: Image.asset(
|
||||
"assets/logo.jpg",
|
||||
),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
37
lib/pages/widgets/img_url.dart
Normal file
37
lib/pages/widgets/img_url.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'show_img.dart';
|
||||
|
||||
typedef OnFile = void Function(File);
|
||||
|
||||
class ImageUrl extends StatefulWidget {
|
||||
final String title;
|
||||
final String url;
|
||||
|
||||
const ImageUrl({Key key, this.title, this.url}) : super(key: key);
|
||||
@override
|
||||
_ImageUrlState createState() => _ImageUrlState();
|
||||
}
|
||||
|
||||
class _ImageUrlState extends State<ImageUrl> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: () => {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
ShowImage(url: widget.url, fileName: widget.title)),
|
||||
)
|
||||
},
|
||||
child: Chip(
|
||||
avatar: Icon(Icons.image),
|
||||
label: Text(widget.title),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
75
lib/pages/widgets/input_text.dart
Normal file
75
lib/pages/widgets/input_text.dart
Normal file
@@ -0,0 +1,75 @@
|
||||
import 'package:fcs/helpers/theme.dart';
|
||||
import 'package:fcs/localization/app_translations.dart';
|
||||
import 'package:fcs/pages/main/model/language_model.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class InputText extends StatelessWidget {
|
||||
final String labelTextKey;
|
||||
final IconData iconData;
|
||||
final TextEditingController controller;
|
||||
final FormFieldValidator<String> validator;
|
||||
final int maxLines;
|
||||
final bool withBorder;
|
||||
final Color borderColor;
|
||||
final TextInputType textInputType;
|
||||
final bool autoFocus;
|
||||
|
||||
const InputText(
|
||||
{Key key,
|
||||
this.labelTextKey,
|
||||
this.iconData,
|
||||
this.controller,
|
||||
this.validator,
|
||||
this.maxLines = 1,
|
||||
this.withBorder = false,
|
||||
this.borderColor,
|
||||
this.autoFocus = false,
|
||||
this.textInputType})
|
||||
: super(key: key);
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var languageModel = Provider.of<LanguageModel>(context);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 15.0, bottom: 5),
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
autofocus: autoFocus,
|
||||
cursorColor: primaryColor,
|
||||
style: textStyle,
|
||||
maxLines: maxLines,
|
||||
keyboardType: textInputType,
|
||||
decoration: new InputDecoration(
|
||||
// hintText: '',
|
||||
hintStyle: TextStyle(
|
||||
height: 1.5,
|
||||
),
|
||||
labelText: labelTextKey == null
|
||||
? null
|
||||
: AppTranslations.of(context).text(labelTextKey),
|
||||
labelStyle: languageModel.isEng ? labelStyle : labelStyleMM,
|
||||
icon: iconData == null
|
||||
? null
|
||||
: Icon(
|
||||
iconData,
|
||||
color: primaryColor,
|
||||
),
|
||||
enabledBorder: withBorder
|
||||
? OutlineInputBorder(
|
||||
borderSide: BorderSide(color: primaryColor, width: 1.0),
|
||||
)
|
||||
: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: primaryColor, width: 1.0)),
|
||||
focusedBorder: withBorder
|
||||
? OutlineInputBorder(
|
||||
borderSide: BorderSide(color: primaryColor, width: 1.0),
|
||||
)
|
||||
: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: primaryColor, width: 1.0)),
|
||||
),
|
||||
validator: validator),
|
||||
);
|
||||
}
|
||||
}
|
||||
56
lib/pages/widgets/label_widgets.dart
Normal file
56
lib/pages/widgets/label_widgets.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'package:fcs/helpers/theme.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'img_url.dart';
|
||||
import 'local_text.dart';
|
||||
|
||||
Widget labeledText(BuildContext context, String text, String label,
|
||||
{bool number = false}) {
|
||||
final w = Container(
|
||||
padding: EdgeInsets.only(top: 3, left: 10, bottom: 3, right: 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: LocalText(
|
||||
context,
|
||||
label,
|
||||
fontSize: 14,
|
||||
color: primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
|
||||
// number ? Spacer() : Container(),
|
||||
Container(
|
||||
padding: EdgeInsets.only(top: 10),
|
||||
// alignment: number ? Alignment.topRight : null,
|
||||
child: Text(
|
||||
text == null ? "" : text,
|
||||
style: textStyle,
|
||||
maxLines: 3,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return w;
|
||||
}
|
||||
|
||||
Widget labeledImg(BuildContext context, String imgUrl, String label) {
|
||||
final _labeledImg = Container(
|
||||
padding: EdgeInsets.only(left: 10),
|
||||
child: Row(children: <Widget>[
|
||||
LocalText(context, label),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: ImageUrl(
|
||||
url: imgUrl,
|
||||
title: "Image",
|
||||
),
|
||||
),
|
||||
]));
|
||||
return _labeledImg;
|
||||
}
|
||||
61
lib/pages/widgets/local_text.dart
Normal file
61
lib/pages/widgets/local_text.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
import 'package:fcs/localization/app_translations.dart';
|
||||
import 'package:fcs/pages/main/model/language_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:fcs/helpers/theme.dart';
|
||||
|
||||
class LocalText extends Text {
|
||||
final BuildContext context;
|
||||
LocalText(this.context, String translationKey,
|
||||
{Color color,
|
||||
double fontSize,
|
||||
FontWeight fontWeight,
|
||||
List<String> translationVariables,
|
||||
String text,
|
||||
bool underline = false})
|
||||
: super(
|
||||
text ??
|
||||
AppTranslations.of(context).text(translationKey,
|
||||
translationVariables: translationVariables),
|
||||
style: Provider.of<LanguageModel>(context, listen: false).isEng
|
||||
? newLabelStyle(
|
||||
color: color,
|
||||
fontSize: fontSize,
|
||||
fontWeight: fontWeight,
|
||||
underline: underline)
|
||||
: newLabelStyleMM(
|
||||
color: color,
|
||||
fontSize: fontSize,
|
||||
fontWeight: fontWeight,
|
||||
underline: underline));
|
||||
}
|
||||
|
||||
class LocalLargeTitle extends Text {
|
||||
final BuildContext context;
|
||||
LocalLargeTitle(
|
||||
this.context,
|
||||
String translationKey, {
|
||||
Color color,
|
||||
List<String> translationVariables,
|
||||
}) : super(
|
||||
AppTranslations.of(context).text(translationKey,
|
||||
translationVariables: translationVariables),
|
||||
style: Provider.of<LanguageModel>(context).isEng
|
||||
? TextStyle(color: color)
|
||||
: TextStyle(color: color, fontFamily: "Myanmar3"));
|
||||
}
|
||||
|
||||
class TextLocalStyle extends Text {
|
||||
final BuildContext context;
|
||||
TextLocalStyle(this.context, String text,
|
||||
{Color color, double fontSize, FontWeight fontWeight})
|
||||
: super(text,
|
||||
style: Provider.of<LanguageModel>(context).isEng
|
||||
? TextStyle(
|
||||
color: color, fontSize: fontSize, fontWeight: fontWeight)
|
||||
: TextStyle(
|
||||
color: color,
|
||||
fontFamily: "Myanmar3",
|
||||
fontSize: fontSize,
|
||||
fontWeight: fontWeight));
|
||||
}
|
||||
67
lib/pages/widgets/multi_img_controller.dart
Normal file
67
lib/pages/widgets/multi_img_controller.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'display_image_source.dart';
|
||||
|
||||
typedef CallBack = void Function();
|
||||
|
||||
class MultiImgController {
|
||||
List<String> imageUrls;
|
||||
List<DisplayImageSource> addedFiles = [];
|
||||
List<DisplayImageSource> removedFiles = [];
|
||||
|
||||
List<DisplayImageSource> fileContainers = [];
|
||||
CallBack callback;
|
||||
MultiImgController() {
|
||||
fileContainers = [];
|
||||
}
|
||||
|
||||
set setImageUrls(List<String> imageUrls) {
|
||||
if (imageUrls == null) {
|
||||
return;
|
||||
}
|
||||
fileContainers.clear();
|
||||
|
||||
this.imageUrls = imageUrls;
|
||||
imageUrls.forEach((e) {
|
||||
fileContainers.add(DisplayImageSource(url: e));
|
||||
});
|
||||
if (callback != null) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
void onChange(CallBack callBack) {
|
||||
this.callback = callBack;
|
||||
}
|
||||
|
||||
set addFile(DisplayImageSource fileContainer) {
|
||||
// if (fileContainers.contains(fileContainer)) return;
|
||||
addedFiles.add(fileContainer);
|
||||
if (callback != null) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
set removeFile(DisplayImageSource fileContainer) {
|
||||
if (!fileContainers.contains(fileContainer)) return;
|
||||
fileContainers.remove(fileContainer);
|
||||
|
||||
if (addedFiles.contains(fileContainer)) {
|
||||
addedFiles.remove(fileContainer);
|
||||
}
|
||||
if (imageUrls.contains(fileContainer.url)) {
|
||||
removedFiles.add(fileContainer);
|
||||
}
|
||||
if (callback != null) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
List<File> get getAddedFile {
|
||||
return addedFiles.map((e) => e.file).toList();
|
||||
}
|
||||
|
||||
List<String> get getDeletedUrl {
|
||||
return removedFiles.map((e) => e.url).toList();
|
||||
}
|
||||
}
|
||||
285
lib/pages/widgets/multi_img_file.dart
Normal file
285
lib/pages/widgets/multi_img_file.dart
Normal file
@@ -0,0 +1,285 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:fcs/helpers/theme.dart';
|
||||
import 'package:fcs/pages/widgets/bottom_up_page_route.dart';
|
||||
import 'package:fcs/pages/widgets/right_left_page_rout.dart';
|
||||
import 'package:fcs/pages/widgets/show_img.dart';
|
||||
import 'package:fcs/pages/widgets/show_multiple_img.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_icons/flutter_icons.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
import 'display_image_source.dart';
|
||||
import 'multi_img_controller.dart';
|
||||
|
||||
typedef OnFile = void Function(File);
|
||||
|
||||
class MultiImageFile extends StatefulWidget {
|
||||
final String title;
|
||||
final bool enabled;
|
||||
final ImageSource imageSource;
|
||||
final MultiImgController controller;
|
||||
|
||||
const MultiImageFile(
|
||||
{Key key,
|
||||
this.title,
|
||||
this.enabled = true,
|
||||
this.controller,
|
||||
this.imageSource = ImageSource.gallery})
|
||||
: super(key: key);
|
||||
@override
|
||||
_MultiImageFileState createState() => _MultiImageFileState();
|
||||
}
|
||||
|
||||
class _MultiImageFileState extends State<MultiImageFile> {
|
||||
List<DisplayImageSource> fileContainers = [];
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
fileContainers = widget.controller.fileContainers;
|
||||
widget.controller.onChange(() {
|
||||
setState(() {
|
||||
this.fileContainers = widget.controller.fileContainers;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 130,
|
||||
width: 500,
|
||||
child: ListView.separated(
|
||||
separatorBuilder: (context, index) => Divider(
|
||||
color: Colors.black,
|
||||
),
|
||||
itemCount:
|
||||
widget.enabled ? fileContainers.length + 1 : fileContainers.length,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == fileContainers.length) {
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
bool camera = false, gallery = false;
|
||||
await _dialog(
|
||||
context, () => camera = true, () => gallery = true);
|
||||
if (camera || gallery) {
|
||||
var selectedFile = await ImagePicker().getImage(
|
||||
source: camera ? ImageSource.camera : ImageSource.gallery,
|
||||
imageQuality: 80,
|
||||
maxWidth: 1000);
|
||||
if (selectedFile != null) {
|
||||
_fileAdded(DisplayImageSource(), File(selectedFile.path));
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Container(
|
||||
width: 200,
|
||||
height: 130,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: primaryColor,
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
child: Icon(SimpleLineIcons.plus),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return InkWell(
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
RightLeftPageRoute(ShowMultiImage(
|
||||
displayImageSources: fileContainers,
|
||||
initialPage: index,
|
||||
))),
|
||||
child: Stack(alignment: Alignment.topLeft, children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Container(
|
||||
width: 200,
|
||||
height: 130,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: primaryColor,
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
child: fileContainers[index].file == null
|
||||
? CachedNetworkImage(
|
||||
width: 50,
|
||||
height: 50,
|
||||
imageUrl: fileContainers[index].url,
|
||||
placeholder: (context, url) => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 30,
|
||||
height: 30,
|
||||
child: CircularProgressIndicator()),
|
||||
],
|
||||
),
|
||||
errorWidget: (context, url, error) =>
|
||||
Icon(Icons.error),
|
||||
)
|
||||
// Image.network(fileContainers[index].url,
|
||||
// width: 50, height: 50)
|
||||
: Image.file(fileContainers[index].file,
|
||||
width: 50, height: 50),
|
||||
),
|
||||
),
|
||||
widget.enabled
|
||||
? Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
height: 50,
|
||||
width: 50,
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
color: primaryColor,
|
||||
),
|
||||
onPressed: () =>
|
||||
{_fileRemove(fileContainers[index])}),
|
||||
),
|
||||
)
|
||||
: Container(),
|
||||
]),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_fileAdded(DisplayImageSource fileContainer, File selectedFile) {
|
||||
fileContainer.file = selectedFile;
|
||||
setState(() {
|
||||
fileContainers.add(fileContainer);
|
||||
widget.controller.addFile = fileContainer;
|
||||
});
|
||||
}
|
||||
|
||||
_fileRemove(DisplayImageSource fileContainer) {
|
||||
setState(() {
|
||||
widget.controller.removeFile = fileContainer;
|
||||
});
|
||||
}
|
||||
|
||||
Widget getFile(DisplayImageSource fileContainer, int index) {
|
||||
return Container(
|
||||
height: 40,
|
||||
padding: EdgeInsets.only(top: 5, bottom: 5),
|
||||
child: fileContainer.file == null && fileContainer.url == null
|
||||
? IconButton(
|
||||
padding: const EdgeInsets.all(3.0),
|
||||
icon: Icon(Icons.attach_file),
|
||||
onPressed: () async {
|
||||
if (!widget.enabled) return;
|
||||
bool camera = false, gallery = false;
|
||||
await _dialog(
|
||||
context, () => camera = true, () => gallery = true);
|
||||
if (camera || gallery) {
|
||||
var selectedFile = await ImagePicker().getImage(
|
||||
source: camera ? ImageSource.camera : ImageSource.gallery,
|
||||
imageQuality: 80,
|
||||
maxWidth: 1000);
|
||||
if (selectedFile != null) {
|
||||
_fileAdded(fileContainer,
|
||||
File.fromRawPath(await selectedFile.readAsBytes()));
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.only(left: 3.0),
|
||||
child: InkWell(
|
||||
onTap: () => {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ShowImage(
|
||||
imageFile: fileContainer.file,
|
||||
url: fileContainer.file == null
|
||||
? fileContainer.url
|
||||
: null,
|
||||
fileName: widget.title)),
|
||||
)
|
||||
},
|
||||
child: Chip(
|
||||
avatar: Icon(Icons.image),
|
||||
onDeleted: !widget.enabled
|
||||
? null
|
||||
: () {
|
||||
_fileRemove(fileContainer);
|
||||
},
|
||||
deleteIcon: Icon(
|
||||
Icons.close,
|
||||
),
|
||||
label: Text(widget.title + " - ${index + 1}"),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _dialog(BuildContext context, cameraPress(), photoPress()) {
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
content: Container(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
FontAwesomeIcons.camera,
|
||||
size: 30,
|
||||
color: primaryColor,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
cameraPress();
|
||||
}),
|
||||
Text("Camera")
|
||||
],
|
||||
),
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.photo_library,
|
||||
size: 30,
|
||||
color: primaryColor,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
photoPress();
|
||||
}),
|
||||
Text("Gallery")
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
926
lib/pages/widgets/my_data_table.dart
Normal file
926
lib/pages/widgets/my_data_table.dart
Normal file
@@ -0,0 +1,926 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Signature for [MyDataColumn.onSort] callback.
|
||||
typedef MyDataColumnSortCallback = void Function(
|
||||
int columnIndex, bool ascending);
|
||||
|
||||
/// Column configuration for a [MyDataTable].
|
||||
///
|
||||
/// One column configuration must be provided for each column to
|
||||
/// display in the table. The list of [MyDataColumn] objects is passed
|
||||
/// as the `columns` argument to the [new MyDataTable] constructor.
|
||||
@immutable
|
||||
class MyDataColumn {
|
||||
/// Creates the configuration for a column of a [MyDataTable].
|
||||
///
|
||||
/// The [label] argument must not be null.
|
||||
const MyDataColumn({
|
||||
@required this.label,
|
||||
this.tooltip,
|
||||
this.numeric = false,
|
||||
this.onSort,
|
||||
}) : assert(label != null);
|
||||
|
||||
/// The column heading.
|
||||
///
|
||||
/// Typically, this will be a [Text] widget. It could also be an
|
||||
/// [Icon] (typically using size 18), or a [Row] with an icon and
|
||||
/// some text.
|
||||
///
|
||||
/// The label should not include the sort indicator.
|
||||
final Widget label;
|
||||
|
||||
/// The column heading's tooltip.
|
||||
///
|
||||
/// This is a longer description of the column heading, for cases
|
||||
/// where the heading might have been abbreviated to keep the column
|
||||
/// width to a reasonable size.
|
||||
final String tooltip;
|
||||
|
||||
/// Whether this column represents numeric data or not.
|
||||
///
|
||||
/// The contents of cells of columns containing numeric data are
|
||||
/// right-aligned.
|
||||
final bool numeric;
|
||||
|
||||
/// Called when the user asks to sort the table using this column.
|
||||
///
|
||||
/// If null, the column will not be considered sortable.
|
||||
///
|
||||
/// See [MyDataTable.sortColumnIndex] and [MyDataTable.sortAscending].
|
||||
final MyDataColumnSortCallback onSort;
|
||||
|
||||
bool get _debugInteractive => onSort != null;
|
||||
}
|
||||
|
||||
/// Row configuration and cell data for a [MyDataTable].
|
||||
///
|
||||
/// One row configuration must be provided for each row to
|
||||
/// display in the table. The list of [MyDataRow] objects is passed
|
||||
/// as the `rows` argument to the [new MyDataTable] constructor.
|
||||
///
|
||||
/// The data for this row of the table is provided in the [cells]
|
||||
/// property of the [MyDataRow] object.
|
||||
@immutable
|
||||
class MyDataRow {
|
||||
/// Creates the configuration for a row of a [MyDataTable].
|
||||
///
|
||||
/// The [cells] argument must not be null.
|
||||
const MyDataRow({
|
||||
this.key,
|
||||
this.selected = false,
|
||||
this.onSelectChanged,
|
||||
@required this.cells,
|
||||
}) : assert(cells != null);
|
||||
|
||||
/// Creates the configuration for a row of a [MyDataTable], deriving
|
||||
/// the key from a row index.
|
||||
///
|
||||
/// The [cells] argument must not be null.
|
||||
MyDataRow.byIndex({
|
||||
int index,
|
||||
this.selected = false,
|
||||
this.onSelectChanged,
|
||||
@required this.cells,
|
||||
}) : assert(cells != null),
|
||||
key = ValueKey<int>(index);
|
||||
|
||||
/// A [Key] that uniquely identifies this row. This is used to
|
||||
/// ensure that if a row is added or removed, any stateful widgets
|
||||
/// related to this row (e.g. an in-progress checkbox animation)
|
||||
/// remain on the right row visually.
|
||||
///
|
||||
/// If the table never changes once created, no key is necessary.
|
||||
final LocalKey key;
|
||||
|
||||
/// Called when the user selects or unselects a selectable row.
|
||||
///
|
||||
/// If this is not null, then the row is selectable. The current
|
||||
/// selection state of the row is given by [selected].
|
||||
///
|
||||
/// If any row is selectable, then the table's heading row will have
|
||||
/// a checkbox that can be checked to select all selectable rows
|
||||
/// (and which is checked if all the rows are selected), and each
|
||||
/// subsequent row will have a checkbox to toggle just that row.
|
||||
///
|
||||
/// A row whose [onSelectChanged] callback is null is ignored for
|
||||
/// the purposes of determining the state of the "all" checkbox,
|
||||
/// and its checkbox is disabled.
|
||||
final ValueChanged<bool> onSelectChanged;
|
||||
|
||||
/// Whether the row is selected.
|
||||
///
|
||||
/// If [onSelectChanged] is non-null for any row in the table, then
|
||||
/// a checkbox is shown at the start of each row. If the row is
|
||||
/// selected (true), the checkbox will be checked and the row will
|
||||
/// be highlighted.
|
||||
///
|
||||
/// Otherwise, the checkbox, if present, will not be checked.
|
||||
final bool selected;
|
||||
|
||||
/// The data for this row.
|
||||
///
|
||||
/// There must be exactly as many cells as there are columns in the
|
||||
/// table.
|
||||
final List<MyDataCell> cells;
|
||||
|
||||
bool get _debugInteractive =>
|
||||
onSelectChanged != null ||
|
||||
cells.any((MyDataCell cell) => cell._debugInteractive);
|
||||
}
|
||||
|
||||
/// The data for a cell of a [MyDataTable].
|
||||
///
|
||||
/// One list of [MyDataCell] objects must be provided for each [MyDataRow]
|
||||
/// in the [MyDataTable], in the [new MyDataRow] constructor's `cells`
|
||||
/// argument.
|
||||
@immutable
|
||||
class MyDataCell {
|
||||
/// Creates an object to hold the data for a cell in a [MyDataTable].
|
||||
///
|
||||
/// The first argument is the widget to show for the cell, typically
|
||||
/// a [Text] or [DropdownButton] widget; this becomes the [child]
|
||||
/// property and must not be null.
|
||||
///
|
||||
/// If the cell has no data, then a [Text] widget with placeholder
|
||||
/// text should be provided instead, and then the [placeholder]
|
||||
/// argument should be set to true.
|
||||
const MyDataCell(
|
||||
this.child, {
|
||||
this.placeholder = false,
|
||||
this.showEditIcon = false,
|
||||
this.onTap,
|
||||
}) : assert(child != null);
|
||||
|
||||
/// A cell that has no content and has zero width and height.
|
||||
static final MyDataCell empty =
|
||||
MyDataCell(Container(width: 0.0, height: 0.0));
|
||||
|
||||
/// The data for the row.
|
||||
///
|
||||
/// Typically a [Text] widget or a [DropdownButton] widget.
|
||||
///
|
||||
/// If the cell has no data, then a [Text] widget with placeholder
|
||||
/// text should be provided instead, and [placeholder] should be set
|
||||
/// to true.
|
||||
///
|
||||
/// {@macro flutter.widgets.child}
|
||||
final Widget child;
|
||||
|
||||
/// Whether the [child] is actually a placeholder.
|
||||
///
|
||||
/// If this is true, the default text style for the cell is changed
|
||||
/// to be appropriate for placeholder text.
|
||||
final bool placeholder;
|
||||
|
||||
/// Whether to show an edit icon at the end of the cell.
|
||||
///
|
||||
/// This does not make the cell actually editable; the caller must
|
||||
/// implement editing behavior if desired (initiated from the
|
||||
/// [onTap] callback).
|
||||
///
|
||||
/// If this is set, [onTap] should also be set, otherwise tapping
|
||||
/// the icon will have no effect.
|
||||
final bool showEditIcon;
|
||||
|
||||
/// Called if the cell is tapped.
|
||||
///
|
||||
/// If non-null, tapping the cell will call this callback. If
|
||||
/// null, tapping the cell will attempt to select the row (if
|
||||
/// [MyDataRow.onSelectChanged] is provided).
|
||||
final VoidCallback onTap;
|
||||
|
||||
bool get _debugInteractive => onTap != null;
|
||||
}
|
||||
|
||||
/// A material design data table.
|
||||
///
|
||||
/// {@youtube 560 315 https://www.youtube.com/watch?v=ktTajqbhIcY}
|
||||
///
|
||||
/// Displaying data in a table is expensive, because to lay out the
|
||||
/// table all the data must be measured twice, once to negotiate the
|
||||
/// dimensions to use for each column, and once to actually lay out
|
||||
/// the table given the results of the negotiation.
|
||||
///
|
||||
/// For this reason, if you have a lot of data (say, more than a dozen
|
||||
/// rows with a dozen columns, though the precise limits depend on the
|
||||
/// target device), it is suggested that you use a
|
||||
/// [PaginatedMyDataTable] which automatically splits the data into
|
||||
/// multiple pages.
|
||||
///
|
||||
/// {@tool snippet --template=stateless_widget_scaffold}
|
||||
///
|
||||
/// This sample shows how to display a [MyDataTable] with three columns: name, age, and
|
||||
/// role. The columns are defined by three [MyDataColumn] objects. The table
|
||||
/// contains three rows of data for three example users, the data for which
|
||||
/// is defined by three [MyDataRow] objects.
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// ```dart
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return MyDataTable(
|
||||
/// columns: const <MyDataColumn>[
|
||||
/// MyDataColumn(
|
||||
/// label: Text(
|
||||
/// 'Name',
|
||||
/// style: TextStyle(fontStyle: FontStyle.italic),
|
||||
/// ),
|
||||
/// ),
|
||||
/// MyDataColumn(
|
||||
/// label: Text(
|
||||
/// 'Age',
|
||||
/// style: TextStyle(fontStyle: FontStyle.italic),
|
||||
/// ),
|
||||
/// ),
|
||||
/// MyDataColumn(
|
||||
/// label: Text(
|
||||
/// 'Role',
|
||||
/// style: TextStyle(fontStyle: FontStyle.italic),
|
||||
/// ),
|
||||
/// ),
|
||||
/// ],
|
||||
/// rows: const <MyDataRow>[
|
||||
/// MyDataRow(
|
||||
/// cells: <MyDataCell>[
|
||||
/// MyDataCell(Text('Sarah')),
|
||||
/// MyDataCell(Text('19')),
|
||||
/// MyDataCell(Text('Student')),
|
||||
/// ],
|
||||
/// ),
|
||||
/// MyDataRow(
|
||||
/// cells: <MyDataCell>[
|
||||
/// MyDataCell(Text('Janine')),
|
||||
/// MyDataCell(Text('43')),
|
||||
/// MyDataCell(Text('Professor')),
|
||||
/// ],
|
||||
/// ),
|
||||
/// MyDataRow(
|
||||
/// cells: <MyDataCell>[
|
||||
/// MyDataCell(Text('William')),
|
||||
/// MyDataCell(Text('27')),
|
||||
/// MyDataCell(Text('Associate Professor')),
|
||||
/// ],
|
||||
/// ),
|
||||
/// ],
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// {@end-tool}
|
||||
// TODO(ianh): Also suggest [ScrollingMyDataTable] once we have it.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [MyDataColumn], which describes a column in the data table.
|
||||
/// * [MyDataRow], which contains the data for a row in the data table.
|
||||
/// * [MyDataCell], which contains the data for a single cell in the data table.
|
||||
/// * [PaginatedMyDataTable], which shows part of the data in a data table and
|
||||
/// provides controls for paging through the remainder of the data.
|
||||
/// * <https://material.io/design/components/data-tables.html>
|
||||
class MyDataTable extends StatelessWidget {
|
||||
/// Creates a widget describing a data table.
|
||||
///
|
||||
/// The [columns] argument must be a list of as many [MyDataColumn]
|
||||
/// objects as the table is to have columns, ignoring the leading
|
||||
/// checkbox column if any. The [columns] argument must have a
|
||||
/// length greater than zero and must not be null.
|
||||
///
|
||||
/// The [rows] argument must be a list of as many [MyDataRow] objects
|
||||
/// as the table is to have rows, ignoring the leading heading row
|
||||
/// that contains the column headings (derived from the [columns]
|
||||
/// argument). There may be zero rows, but the rows argument must
|
||||
/// not be null.
|
||||
///
|
||||
/// Each [MyDataRow] object in [rows] must have as many [MyDataCell]
|
||||
/// objects in the [MyDataRow.cells] list as the table has columns.
|
||||
///
|
||||
/// If the table is sorted, the column that provides the current
|
||||
/// primary key should be specified by index in [sortColumnIndex], 0
|
||||
/// meaning the first column in [columns], 1 being the next one, and
|
||||
/// so forth.
|
||||
///
|
||||
/// The actual sort order can be specified using [sortAscending]; if
|
||||
/// the sort order is ascending, this should be true (the default),
|
||||
/// otherwise it should be false.
|
||||
MyDataTable({
|
||||
Key key,
|
||||
@required this.columns,
|
||||
this.sortColumnIndex,
|
||||
this.sortAscending = true,
|
||||
this.onSelectAll,
|
||||
this.MyDataRowHeight = kMinInteractiveDimension,
|
||||
this.headingRowHeight = 56.0,
|
||||
this.horizontalMargin = 24.0,
|
||||
this.columnSpacing = 56.0,
|
||||
this.oddLine,
|
||||
this.evenLine,
|
||||
@required this.rows,
|
||||
}) : assert(columns != null),
|
||||
assert(columns.isNotEmpty),
|
||||
assert(sortColumnIndex == null ||
|
||||
(sortColumnIndex >= 0 && sortColumnIndex < columns.length)),
|
||||
assert(sortAscending != null),
|
||||
assert(MyDataRowHeight != null),
|
||||
assert(headingRowHeight != null),
|
||||
assert(horizontalMargin != null),
|
||||
assert(columnSpacing != null),
|
||||
assert(rows != null),
|
||||
assert(
|
||||
!rows.any((MyDataRow row) => row.cells.length != columns.length)),
|
||||
_onlyTextColumn = _initOnlyTextColumn(columns),
|
||||
super(key: key);
|
||||
|
||||
/// The configuration and labels for the columns in the table.
|
||||
final List<MyDataColumn> columns;
|
||||
|
||||
final Decoration oddLine;
|
||||
final Decoration evenLine;
|
||||
|
||||
/// The current primary sort key's column.
|
||||
///
|
||||
/// If non-null, indicates that the indicated column is the column
|
||||
/// by which the data is sorted. The number must correspond to the
|
||||
/// index of the relevant column in [columns].
|
||||
///
|
||||
/// Setting this will cause the relevant column to have a sort
|
||||
/// indicator displayed.
|
||||
///
|
||||
/// When this is null, it implies that the table's sort order does
|
||||
/// not correspond to any of the columns.
|
||||
final int sortColumnIndex;
|
||||
|
||||
/// Whether the column mentioned in [sortColumnIndex], if any, is sorted
|
||||
/// in ascending order.
|
||||
///
|
||||
/// If true, the order is ascending (meaning the rows with the
|
||||
/// smallest values for the current sort column are first in the
|
||||
/// table).
|
||||
///
|
||||
/// If false, the order is descending (meaning the rows with the
|
||||
/// smallest values for the current sort column are last in the
|
||||
/// table).
|
||||
final bool sortAscending;
|
||||
|
||||
/// Invoked when the user selects or unselects every row, using the
|
||||
/// checkbox in the heading row.
|
||||
///
|
||||
/// If this is null, then the [MyDataRow.onSelectChanged] callback of
|
||||
/// every row in the table is invoked appropriately instead.
|
||||
///
|
||||
/// To control whether a particular row is selectable or not, see
|
||||
/// [MyDataRow.onSelectChanged]. This callback is only relevant if any
|
||||
/// row is selectable.
|
||||
final ValueSetter<bool> onSelectAll;
|
||||
|
||||
/// The height of each row (excluding the row that contains column headings).
|
||||
///
|
||||
/// This value defaults to kMinInteractiveDimension to adhere to the Material
|
||||
/// Design specifications.
|
||||
final double MyDataRowHeight;
|
||||
|
||||
/// The height of the heading row.
|
||||
///
|
||||
/// This value defaults to 56.0 to adhere to the Material Design specifications.
|
||||
final double headingRowHeight;
|
||||
|
||||
/// The horizontal margin between the edges of the table and the content
|
||||
/// in the first and last cells of each row.
|
||||
///
|
||||
/// When a checkbox is displayed, it is also the margin between the checkbox
|
||||
/// the content in the first data column.
|
||||
///
|
||||
/// This value defaults to 24.0 to adhere to the Material Design specifications.
|
||||
final double horizontalMargin;
|
||||
|
||||
/// The horizontal margin between the contents of each data column.
|
||||
///
|
||||
/// This value defaults to 56.0 to adhere to the Material Design specifications.
|
||||
final double columnSpacing;
|
||||
|
||||
/// The data to show in each row (excluding the row that contains
|
||||
/// the column headings).
|
||||
///
|
||||
/// Must be non-null, but may be empty.
|
||||
final List<MyDataRow> rows;
|
||||
|
||||
// Set by the constructor to the index of the only Column that is
|
||||
// non-numeric, if there is exactly one, otherwise null.
|
||||
final int _onlyTextColumn;
|
||||
static int _initOnlyTextColumn(List<MyDataColumn> columns) {
|
||||
int result;
|
||||
for (int index = 0; index < columns.length; index += 1) {
|
||||
final MyDataColumn column = columns[index];
|
||||
if (!column.numeric) {
|
||||
if (result != null) return null;
|
||||
result = index;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool get _debugInteractive {
|
||||
return columns.any((MyDataColumn column) => column._debugInteractive) ||
|
||||
rows.any((MyDataRow row) => row._debugInteractive);
|
||||
}
|
||||
|
||||
static final LocalKey _headingRowKey = UniqueKey();
|
||||
|
||||
void _handleSelectAll(bool checked) {
|
||||
if (onSelectAll != null) {
|
||||
onSelectAll(checked);
|
||||
} else {
|
||||
for (MyDataRow row in rows) {
|
||||
if ((row.onSelectChanged != null) && (row.selected != checked))
|
||||
row.onSelectChanged(checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static const double _sortArrowPadding = 2.0;
|
||||
static const double _headingFontSize = 12.0;
|
||||
static const Duration _sortArrowAnimationDuration =
|
||||
Duration(milliseconds: 150);
|
||||
static const Color _grey100Opacity =
|
||||
Color(0x0A000000); // Grey 100 as opacity instead of solid color
|
||||
static const Color _grey300Opacity =
|
||||
Color(0x1E000000); // Dark theme variant is just a guess.
|
||||
|
||||
Widget _buildCheckbox({
|
||||
Color color,
|
||||
bool checked,
|
||||
VoidCallback onRowTap,
|
||||
ValueChanged<bool> onCheckboxChanged,
|
||||
}) {
|
||||
Widget contents = Semantics(
|
||||
container: true,
|
||||
child: Padding(
|
||||
padding: EdgeInsetsDirectional.only(
|
||||
start: horizontalMargin, end: horizontalMargin / 2.0),
|
||||
child: Center(
|
||||
child: Checkbox(
|
||||
activeColor: color,
|
||||
value: checked,
|
||||
onChanged: onCheckboxChanged,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (onRowTap != null) {
|
||||
contents = TableRowInkWell(
|
||||
onTap: onRowTap,
|
||||
child: contents,
|
||||
);
|
||||
}
|
||||
return TableCell(
|
||||
verticalAlignment: TableCellVerticalAlignment.fill,
|
||||
child: contents,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeadingCell({
|
||||
BuildContext context,
|
||||
EdgeInsetsGeometry padding,
|
||||
Widget label,
|
||||
String tooltip,
|
||||
bool numeric,
|
||||
VoidCallback onSort,
|
||||
bool sorted,
|
||||
bool ascending,
|
||||
}) {
|
||||
if (onSort != null) {
|
||||
final Widget arrow = _SortArrow(
|
||||
visible: sorted,
|
||||
down: sorted ? ascending : null,
|
||||
duration: _sortArrowAnimationDuration,
|
||||
);
|
||||
const Widget arrowPadding = SizedBox(width: _sortArrowPadding);
|
||||
label = Row(
|
||||
textDirection: numeric ? TextDirection.rtl : null,
|
||||
children: <Widget>[label, arrowPadding, arrow],
|
||||
);
|
||||
}
|
||||
label = Container(
|
||||
padding: padding,
|
||||
height: headingRowHeight,
|
||||
alignment:
|
||||
numeric ? Alignment.centerRight : AlignmentDirectional.centerStart,
|
||||
child: AnimatedDefaultTextStyle(
|
||||
style: TextStyle(
|
||||
// TODO(ianh): font family should match Theme; see https://github.com/flutter/flutter/issues/3116
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: _headingFontSize,
|
||||
height: math.min(1.0, headingRowHeight / _headingFontSize),
|
||||
color: (Theme.of(context).brightness == Brightness.light)
|
||||
? ((onSort != null && sorted) ? Colors.black87 : Colors.black54)
|
||||
: ((onSort != null && sorted) ? Colors.white : Colors.white70),
|
||||
),
|
||||
softWrap: false,
|
||||
duration: _sortArrowAnimationDuration,
|
||||
child: label,
|
||||
),
|
||||
);
|
||||
if (tooltip != null) {
|
||||
label = Tooltip(
|
||||
message: tooltip,
|
||||
child: label,
|
||||
);
|
||||
}
|
||||
if (onSort != null) {
|
||||
label = InkWell(
|
||||
onTap: onSort,
|
||||
child: label,
|
||||
);
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
Widget _buildMyDataCell({
|
||||
BuildContext context,
|
||||
EdgeInsetsGeometry padding,
|
||||
Widget label,
|
||||
bool numeric,
|
||||
bool placeholder,
|
||||
bool showEditIcon,
|
||||
VoidCallback onTap,
|
||||
VoidCallback onSelectChanged,
|
||||
}) {
|
||||
final bool isLightTheme = Theme.of(context).brightness == Brightness.light;
|
||||
if (showEditIcon) {
|
||||
const Widget icon = Icon(Icons.edit, size: 18.0);
|
||||
label = Expanded(child: label);
|
||||
label = Row(
|
||||
textDirection: numeric ? TextDirection.rtl : null,
|
||||
children: <Widget>[label, icon],
|
||||
);
|
||||
}
|
||||
label = Container(
|
||||
padding: padding,
|
||||
height: MyDataRowHeight,
|
||||
alignment:
|
||||
numeric ? Alignment.centerRight : AlignmentDirectional.centerStart,
|
||||
child: DefaultTextStyle(
|
||||
style: TextStyle(
|
||||
// TODO(ianh): font family should be Roboto; see https://github.com/flutter/flutter/issues/3116
|
||||
fontSize: 13.0,
|
||||
color: isLightTheme
|
||||
? (placeholder ? Colors.black38 : Colors.black87)
|
||||
: (placeholder ? Colors.white38 : Colors.white70),
|
||||
),
|
||||
child: IconTheme.merge(
|
||||
data: IconThemeData(
|
||||
color: isLightTheme ? Colors.black54 : Colors.white70,
|
||||
),
|
||||
child: DropdownButtonHideUnderline(child: label),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (onTap != null) {
|
||||
label = InkWell(
|
||||
onTap: onTap,
|
||||
child: label,
|
||||
);
|
||||
} else if (onSelectChanged != null) {
|
||||
label = TableRowInkWell(
|
||||
onTap: onSelectChanged,
|
||||
child: label,
|
||||
);
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(!_debugInteractive || debugCheckHasMaterial(context));
|
||||
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final BoxDecoration _kSelectedDecoration = BoxDecoration(
|
||||
border: Border(bottom: Divider.createBorderSide(context, width: 1.0)),
|
||||
// The backgroundColor has to be transparent so you can see the ink on the material
|
||||
color: (Theme.of(context).brightness == Brightness.light)
|
||||
? _grey100Opacity
|
||||
: _grey300Opacity,
|
||||
);
|
||||
final BoxDecoration _kUnselectedDecoration = BoxDecoration(
|
||||
border: Border(bottom: Divider.createBorderSide(context, width: 1.0)),
|
||||
);
|
||||
|
||||
final bool showCheckboxColumn = false;
|
||||
final bool allChecked = false;
|
||||
|
||||
final List<TableColumnWidth> tableColumns =
|
||||
List<TableColumnWidth>(columns.length + (showCheckboxColumn ? 1 : 0));
|
||||
final List<TableRow> tableRows = List<TableRow>.generate(
|
||||
rows.length + 1, // the +1 is for the header row
|
||||
(int index) {
|
||||
return TableRow(
|
||||
key: index == 0 ? _headingRowKey : rows[index - 1].key,
|
||||
decoration: index > 0 && rows[index - 1].selected
|
||||
? _kSelectedDecoration
|
||||
: index > 0 && index.isOdd && oddLine != null
|
||||
? oddLine
|
||||
: index.isEven && evenLine != null
|
||||
? evenLine
|
||||
: _kUnselectedDecoration,
|
||||
children: List<Widget>(tableColumns.length),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
int rowIndex;
|
||||
|
||||
int displayColumnIndex = 0;
|
||||
if (showCheckboxColumn) {
|
||||
tableColumns[0] = FixedColumnWidth(
|
||||
horizontalMargin + Checkbox.width + horizontalMargin / 2.0);
|
||||
tableRows[0].children[0] = _buildCheckbox(
|
||||
color: theme.accentColor,
|
||||
checked: allChecked,
|
||||
onCheckboxChanged: _handleSelectAll,
|
||||
);
|
||||
rowIndex = 1;
|
||||
for (MyDataRow row in rows) {
|
||||
tableRows[rowIndex].children[0] = _buildCheckbox(
|
||||
color: theme.accentColor,
|
||||
checked: row.selected,
|
||||
onRowTap: () => row.onSelectChanged != null
|
||||
? row.onSelectChanged(!row.selected)
|
||||
: null,
|
||||
onCheckboxChanged: row.onSelectChanged,
|
||||
);
|
||||
rowIndex += 1;
|
||||
}
|
||||
displayColumnIndex += 1;
|
||||
}
|
||||
|
||||
for (int MyDataColumnIndex = 0;
|
||||
MyDataColumnIndex < columns.length;
|
||||
MyDataColumnIndex += 1) {
|
||||
final MyDataColumn column = columns[MyDataColumnIndex];
|
||||
|
||||
double paddingStart;
|
||||
if (MyDataColumnIndex == 0 && showCheckboxColumn) {
|
||||
paddingStart = horizontalMargin / 2.0;
|
||||
} else if (MyDataColumnIndex == 0 && !showCheckboxColumn) {
|
||||
paddingStart = horizontalMargin;
|
||||
} else {
|
||||
paddingStart = columnSpacing / 2.0;
|
||||
}
|
||||
|
||||
double paddingEnd;
|
||||
if (MyDataColumnIndex == columns.length - 1) {
|
||||
paddingEnd = horizontalMargin;
|
||||
} else {
|
||||
paddingEnd = columnSpacing / 2.0;
|
||||
}
|
||||
|
||||
final EdgeInsetsDirectional padding = EdgeInsetsDirectional.only(
|
||||
start: paddingStart,
|
||||
end: paddingEnd,
|
||||
);
|
||||
if (MyDataColumnIndex == _onlyTextColumn) {
|
||||
tableColumns[displayColumnIndex] =
|
||||
const IntrinsicColumnWidth(flex: 1.0);
|
||||
} else {
|
||||
tableColumns[displayColumnIndex] = const IntrinsicColumnWidth();
|
||||
}
|
||||
tableRows[0].children[displayColumnIndex] = _buildHeadingCell(
|
||||
context: context,
|
||||
padding: padding,
|
||||
label: column.label,
|
||||
tooltip: column.tooltip,
|
||||
numeric: column.numeric,
|
||||
onSort: () => column.onSort != null
|
||||
? column.onSort(MyDataColumnIndex,
|
||||
sortColumnIndex != MyDataColumnIndex || !sortAscending)
|
||||
: null,
|
||||
sorted: MyDataColumnIndex == sortColumnIndex,
|
||||
ascending: sortAscending,
|
||||
);
|
||||
rowIndex = 1;
|
||||
for (MyDataRow row in rows) {
|
||||
final MyDataCell cell = row.cells[MyDataColumnIndex];
|
||||
tableRows[rowIndex].children[displayColumnIndex] = _buildMyDataCell(
|
||||
context: context,
|
||||
padding: padding,
|
||||
label: cell.child,
|
||||
numeric: column.numeric,
|
||||
placeholder: cell.placeholder,
|
||||
showEditIcon: cell.showEditIcon,
|
||||
onTap: cell.onTap,
|
||||
onSelectChanged: () => row.onSelectChanged != null
|
||||
? row.onSelectChanged(!row.selected)
|
||||
: null,
|
||||
);
|
||||
rowIndex += 1;
|
||||
}
|
||||
displayColumnIndex += 1;
|
||||
}
|
||||
|
||||
return Table(
|
||||
columnWidths: tableColumns.asMap(),
|
||||
children: tableRows,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A rectangular area of a Material that responds to touch but clips
|
||||
/// its ink splashes to the current table row of the nearest table.
|
||||
///
|
||||
/// Must have an ancestor [Material] widget in which to cause ink
|
||||
/// reactions and an ancestor [Table] widget to establish a row.
|
||||
///
|
||||
/// The [TableRowInkWell] must be in the same coordinate space (modulo
|
||||
/// translations) as the [Table]. If it's rotated or scaled or
|
||||
/// otherwise transformed, it will not be able to describe the
|
||||
/// rectangle of the row in its own coordinate system as a [Rect], and
|
||||
/// thus the splash will not occur. (In general, this is easy to
|
||||
/// achieve: just put the [TableRowInkWell] as the direct child of the
|
||||
/// [Table], and put the other contents of the cell inside it.)
|
||||
class TableRowInkWell extends InkResponse {
|
||||
/// Creates an ink well for a table row.
|
||||
const TableRowInkWell({
|
||||
Key key,
|
||||
Widget child,
|
||||
GestureTapCallback onTap,
|
||||
GestureTapCallback onDoubleTap,
|
||||
GestureLongPressCallback onLongPress,
|
||||
ValueChanged<bool> onHighlightChanged,
|
||||
}) : super(
|
||||
key: key,
|
||||
child: child,
|
||||
onTap: onTap,
|
||||
onDoubleTap: onDoubleTap,
|
||||
onLongPress: onLongPress,
|
||||
onHighlightChanged: onHighlightChanged,
|
||||
containedInkWell: true,
|
||||
highlightShape: BoxShape.rectangle,
|
||||
);
|
||||
|
||||
@override
|
||||
RectCallback getRectCallback(RenderBox referenceBox) {
|
||||
return () {
|
||||
RenderObject cell = referenceBox;
|
||||
AbstractNode table = cell.parent;
|
||||
final Matrix4 transform = Matrix4.identity();
|
||||
while (table is RenderObject && table is! RenderTable) {
|
||||
final RenderObject parentBox = table as RenderObject;
|
||||
parentBox.applyPaintTransform(cell, transform);
|
||||
assert(table == cell.parent);
|
||||
cell = parentBox;
|
||||
table = table.parent;
|
||||
}
|
||||
if (table is RenderTable) {
|
||||
final TableCellParentData cellParentData =
|
||||
cell.parentData as TableCellParentData;
|
||||
assert(cellParentData.y != null);
|
||||
final Rect rect = table.getRowBox(cellParentData.y);
|
||||
// The rect is in the table's coordinate space. We need to change it to the
|
||||
// TableRowInkWell's coordinate space.
|
||||
table.applyPaintTransform(cell, transform);
|
||||
final Offset offset = MatrixUtils.getAsTranslation(transform);
|
||||
if (offset != null) return rect.shift(-offset);
|
||||
}
|
||||
return Rect.zero;
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool debugCheckContext(BuildContext context) {
|
||||
assert(debugCheckHasTable(context));
|
||||
return super.debugCheckContext(context);
|
||||
}
|
||||
}
|
||||
|
||||
class _SortArrow extends StatefulWidget {
|
||||
const _SortArrow({
|
||||
Key key,
|
||||
this.visible,
|
||||
this.down,
|
||||
this.duration,
|
||||
}) : super(key: key);
|
||||
|
||||
final bool visible;
|
||||
|
||||
final bool down;
|
||||
|
||||
final Duration duration;
|
||||
|
||||
@override
|
||||
_SortArrowState createState() => _SortArrowState();
|
||||
}
|
||||
|
||||
class _SortArrowState extends State<_SortArrow> with TickerProviderStateMixin {
|
||||
AnimationController _opacityController;
|
||||
Animation<double> _opacityAnimation;
|
||||
|
||||
AnimationController _orientationController;
|
||||
Animation<double> _orientationAnimation;
|
||||
double _orientationOffset = 0.0;
|
||||
|
||||
bool _down;
|
||||
|
||||
static final Animatable<double> _turnTween =
|
||||
Tween<double>(begin: 0.0, end: math.pi)
|
||||
.chain(CurveTween(curve: Curves.easeIn));
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_opacityAnimation = CurvedAnimation(
|
||||
parent: _opacityController = AnimationController(
|
||||
duration: widget.duration,
|
||||
vsync: this,
|
||||
),
|
||||
curve: Curves.fastOutSlowIn,
|
||||
)..addListener(_rebuild);
|
||||
_opacityController.value = widget.visible ? 1.0 : 0.0;
|
||||
_orientationController = AnimationController(
|
||||
duration: widget.duration,
|
||||
vsync: this,
|
||||
);
|
||||
_orientationAnimation = _orientationController.drive(_turnTween)
|
||||
..addListener(_rebuild)
|
||||
..addStatusListener(_resetOrientationAnimation);
|
||||
if (widget.visible) _orientationOffset = widget.down ? 0.0 : math.pi;
|
||||
}
|
||||
|
||||
void _rebuild() {
|
||||
setState(() {
|
||||
// The animations changed, so we need to rebuild.
|
||||
});
|
||||
}
|
||||
|
||||
void _resetOrientationAnimation(AnimationStatus status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
assert(_orientationAnimation.value == math.pi);
|
||||
_orientationOffset += math.pi;
|
||||
_orientationController.value =
|
||||
0.0; // TODO(ianh): This triggers a pointless rebuild.
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(_SortArrow oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
bool skipArrow = false;
|
||||
final bool newDown = widget.down ?? _down;
|
||||
if (oldWidget.visible != widget.visible) {
|
||||
if (widget.visible &&
|
||||
(_opacityController.status == AnimationStatus.dismissed)) {
|
||||
_orientationController.stop();
|
||||
_orientationController.value = 0.0;
|
||||
_orientationOffset = newDown ? 0.0 : math.pi;
|
||||
skipArrow = true;
|
||||
}
|
||||
if (widget.visible) {
|
||||
_opacityController.forward();
|
||||
} else {
|
||||
_opacityController.reverse();
|
||||
}
|
||||
}
|
||||
if ((_down != newDown) && !skipArrow) {
|
||||
if (_orientationController.status == AnimationStatus.dismissed) {
|
||||
_orientationController.forward();
|
||||
} else {
|
||||
_orientationController.reverse();
|
||||
}
|
||||
}
|
||||
_down = newDown;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_opacityController.dispose();
|
||||
_orientationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
static const double _arrowIconBaselineOffset = -1.5;
|
||||
static const double _arrowIconSize = 16.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Opacity(
|
||||
opacity: _opacityAnimation.value,
|
||||
child: Transform(
|
||||
transform:
|
||||
Matrix4.rotationZ(_orientationOffset + _orientationAnimation.value)
|
||||
..setTranslationRaw(0.0, _arrowIconBaselineOffset, 0.0),
|
||||
alignment: Alignment.center,
|
||||
child: Icon(
|
||||
Icons.arrow_downward,
|
||||
size: _arrowIconSize,
|
||||
color: (Theme.of(context).brightness == Brightness.light)
|
||||
? Colors.black87
|
||||
: Colors.white70,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
20
lib/pages/widgets/number_cell.dart
Normal file
20
lib/pages/widgets/number_cell.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:fcs/helpers/theme.dart' as theme;
|
||||
|
||||
class NumberCell extends StatelessWidget {
|
||||
final int number;
|
||||
final numberFormatter;
|
||||
final TextStyle textStyle;
|
||||
|
||||
NumberCell(this.number, {Key key, this.textStyle})
|
||||
: numberFormatter = new NumberFormat("#,###"),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return new Text(
|
||||
this.number == null ? '0' : numberFormatter.format(this.number),
|
||||
style: textStyle == null ? theme.textStyle : textStyle);
|
||||
}
|
||||
}
|
||||
52
lib/pages/widgets/offline_redirect.dart
Normal file
52
lib/pages/widgets/offline_redirect.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fcs/pages/main/model/main_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class OfflineRedirect extends StatefulWidget {
|
||||
final Widget child;
|
||||
OfflineRedirect({@required this.child});
|
||||
|
||||
@override
|
||||
_OfflineRedirectState createState() => _OfflineRedirectState();
|
||||
}
|
||||
|
||||
class _OfflineRedirectState extends State<OfflineRedirect> {
|
||||
Timer offlineTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
_startOfflineTimer();
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
_startOfflineTimer() async {
|
||||
if (offlineTimer != null && offlineTimer.isActive) return;
|
||||
var _duration = new Duration(milliseconds: 500);
|
||||
this.offlineTimer = new Timer.periodic(_duration, offlineNav);
|
||||
}
|
||||
|
||||
Future<void> offlineNav(Timer timer) async {
|
||||
MainModel mainModel = Provider.of<MainModel>(context, listen: false);
|
||||
if (!mainModel.isOnline) {
|
||||
timer.cancel();
|
||||
Navigator.pushNamedAndRemoveUntil(context, "/", (r) => false);
|
||||
}
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
offlineTimer.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(child: widget.child);
|
||||
}
|
||||
}
|
||||
45
lib/pages/widgets/progress.dart
Normal file
45
lib/pages/widgets/progress.dart
Normal file
@@ -0,0 +1,45 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:progress/progress.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:fcs/helpers/theme.dart';
|
||||
|
||||
class LocalProgress extends Progress {
|
||||
LocalProgress({bool inAsyncCall, Widget child})
|
||||
: super(
|
||||
inAsyncCall: inAsyncCall,
|
||||
child: child,
|
||||
opacity: 0.6,
|
||||
progressIndicator: Center(
|
||||
child: Container(
|
||||
height: 100,
|
||||
width: 300,
|
||||
child: Card(
|
||||
color: Colors.white,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
CircularProgressIndicator(
|
||||
valueColor:
|
||||
new AlwaysStoppedAnimation<Color>(primaryColor),
|
||||
),
|
||||
SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Text("Loading...")
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
// hide keyboard
|
||||
if (inAsyncCall) {
|
||||
FocusScopeNode currentFocus = FocusScope.of(context);
|
||||
if (!currentFocus.hasPrimaryFocus) {
|
||||
currentFocus.unfocus();
|
||||
}
|
||||
}
|
||||
return super.build(context);
|
||||
}
|
||||
}
|
||||
23
lib/pages/widgets/right_left_page_rout.dart
Normal file
23
lib/pages/widgets/right_left_page_rout.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class RightLeftPageRoute extends PageRouteBuilder {
|
||||
final Widget child;
|
||||
|
||||
RightLeftPageRoute(this.child)
|
||||
: super(
|
||||
pageBuilder: (context, animation, secondaryAnimation) => child,
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||
var begin = Offset(1.0, 0.0);
|
||||
var end = Offset.zero;
|
||||
var curve = Curves.ease;
|
||||
|
||||
var tween =
|
||||
Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
|
||||
|
||||
return SlideTransition(
|
||||
position: animation.drive(tween),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
35
lib/pages/widgets/show_img.dart
Normal file
35
lib/pages/widgets/show_img.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fcs/helpers/theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
|
||||
class ShowImage extends StatefulWidget {
|
||||
final String url;
|
||||
final File imageFile;
|
||||
final String fileName;
|
||||
const ShowImage({Key key, this.imageFile, this.fileName, this.url})
|
||||
: super(key: key);
|
||||
@override
|
||||
_ShowImageState createState() => _ShowImageState();
|
||||
}
|
||||
|
||||
class _ShowImageState extends State<ShowImage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: primaryColor,
|
||||
title: Text(widget.fileName),
|
||||
),
|
||||
body: Center(
|
||||
child: widget.url != null || widget.imageFile != null
|
||||
? PhotoView(
|
||||
imageProvider: widget.url != null
|
||||
? NetworkImage(widget.url)
|
||||
: FileImage(widget.imageFile),
|
||||
minScale: PhotoViewComputedScale.contained * 1)
|
||||
: Container()),
|
||||
);
|
||||
}
|
||||
}
|
||||
74
lib/pages/widgets/show_multiple_img.dart
Normal file
74
lib/pages/widgets/show_multiple_img.dart
Normal file
@@ -0,0 +1,74 @@
|
||||
import 'package:fcs/helpers/theme.dart';
|
||||
import 'package:fcs/pages/widgets/display_image_source.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
import 'package:photo_view/photo_view_gallery.dart';
|
||||
|
||||
class ShowMultiImage extends StatefulWidget {
|
||||
final List<DisplayImageSource> displayImageSources;
|
||||
final int initialPage;
|
||||
const ShowMultiImage(
|
||||
{Key key, this.displayImageSources, this.initialPage = 0})
|
||||
: super(key: key);
|
||||
@override
|
||||
_ShowMultiImageState createState() => _ShowMultiImageState();
|
||||
}
|
||||
|
||||
class _ShowMultiImageState extends State<ShowMultiImage> {
|
||||
PageController pageController;
|
||||
@override
|
||||
void initState() {
|
||||
pageController = PageController(initialPage: widget.initialPage);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
backgroundColor: primaryColor,
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: PhotoViewGallery.builder(
|
||||
scrollPhysics: const BouncingScrollPhysics(),
|
||||
builder: (BuildContext context, int index) {
|
||||
return PhotoViewGalleryPageOptions(
|
||||
imageProvider:
|
||||
widget.displayImageSources[index].imageProvider,
|
||||
initialScale: PhotoViewComputedScale.contained * 0.95,
|
||||
heroAttributes: PhotoViewHeroAttributes(
|
||||
tag: widget.displayImageSources[index].hashCode),
|
||||
);
|
||||
},
|
||||
itemCount: widget.displayImageSources.length,
|
||||
loadingBuilder: (context, event) => Center(
|
||||
child: Container(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
child: CircularProgressIndicator(
|
||||
value: event == null
|
||||
? 0
|
||||
: event.cumulativeBytesLoaded /
|
||||
event.expectedTotalBytes,
|
||||
),
|
||||
),
|
||||
),
|
||||
backgroundDecoration: const BoxDecoration(
|
||||
color: primaryColor,
|
||||
),
|
||||
pageController: pageController,
|
||||
)),
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
66
lib/pages/widgets/task_button.dart
Normal file
66
lib/pages/widgets/task_button.dart
Normal file
@@ -0,0 +1,66 @@
|
||||
import 'package:fcs/localization/app_translations.dart';
|
||||
import 'package:fcs/pages/main/model/language_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
typedef BtnCallback();
|
||||
|
||||
/// TaskButton is used to navigate to eash task
|
||||
class TaskButton extends StatelessWidget {
|
||||
final String titleKey;
|
||||
final IconData icon;
|
||||
final BtnCallback btnCallback;
|
||||
|
||||
const TaskButton(this.titleKey, {Key key, this.icon, this.btnCallback})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var languageModel = Provider.of<LanguageModel>(context);
|
||||
return InkWell(
|
||||
onTap: btnCallback != null ? btnCallback : () => {},
|
||||
child: Container(
|
||||
width: 120,
|
||||
height: 140,
|
||||
padding: EdgeInsets.only(top: 10.0, left: 5, right: 5),
|
||||
decoration: new BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
borderRadius: new BorderRadius.only(
|
||||
topLeft: const Radius.circular(40.0),
|
||||
topRight: const Radius.circular(40.0))),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
child: Column(children: <Widget>[
|
||||
ClipOval(
|
||||
child: Material(
|
||||
color: Colors.black54, // button color
|
||||
child: SizedBox(
|
||||
width: 60,
|
||||
height: 60,
|
||||
child: Icon(icon, color: Colors.white, size: 30)),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 60,
|
||||
alignment: Alignment.topCenter,
|
||||
child: Text(AppTranslations.of(context).text(titleKey),
|
||||
textAlign: TextAlign.center,
|
||||
style: languageModel.isEng
|
||||
? TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 15.0,
|
||||
fontFamily: "Roboto")
|
||||
: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14.0,
|
||||
fontFamily: "Myanmar3")),
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user