add structure

This commit is contained in:
2020-05-29 07:45:27 +06:30
parent 4c851d9971
commit bad27ba5c4
272 changed files with 36065 additions and 174 deletions

View File

@@ -0,0 +1,67 @@
import 'dart:async';
import 'dart:io';
import 'package:connectivity/connectivity.dart';
import 'package:logging/logging.dart';
import 'package:fcs/config.dart';
import 'package:fcs/model/api_helper.dart';
class NetworkConnectivity {
final log = Logger('NetworkConnectivity');
static final NetworkConnectivity instance = NetworkConnectivity._internal();
static String hostName;
NetworkConnectivity._internal() {
_initialise();
var uri = Uri.parse(Config.instance.apiURL);
hostName = uri.host;
log.info("host name:$hostName");
}
Connectivity connectivity = Connectivity();
final StreamController _controller = StreamController.broadcast();
Stream get statusStream => _controller.stream;
void _initialise() async {
ConnectivityResult result = await connectivity.checkConnectivity();
_checkStatus(result);
connectivity.onConnectivityChanged.listen((result) {
_checkStatus(result);
});
}
void _checkStatus(ConnectivityResult result) async {
bool isOnline = false;
// lookup if connectivity is not none
if (result != ConnectivityResult.none) {
try {
final hostNameLookup = await InternetAddress.lookup(hostName);
if (hostNameLookup.isNotEmpty &&
hostNameLookup[0].rawAddress.isNotEmpty) {
if (await checkHeartbeat()) {
isOnline = true;
}
} else
isOnline = false;
} on SocketException catch (_) {
isOnline = false;
}
}
if (_controller != null && !_controller.isClosed)
_controller.sink.add({"isOnline": isOnline});
}
Future<bool> checkHeartbeat() async {
var result = await requestAPI("/hb", "GET");
var status = result["status"];
if (status != null && status != "") {
return true;
}
return false;
}
void disposeStream() => _controller.close();
}

43
lib/widget/badge.dart Normal file
View File

@@ -0,0 +1,43 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
Widget badgeCounter(int counter, {VoidCallback onPressed}) {
return InkWell(
onTap: () => onPressed(),
child: new Stack(
children: <Widget>[
new IconButton(
padding: EdgeInsets.only(top: 10),
icon: Icon(Icons.notifications),
onPressed: () {
onPressed();
}),
counter != 0
? new Positioned(
right: 11,
top: 11,
child: new Container(
padding: EdgeInsets.all(2),
decoration: new BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(6),
),
constraints: BoxConstraints(
minWidth: 14,
minHeight: 14,
),
child: Text(
'+${counter > 99 ? 99 : counter}',
style: TextStyle(
color: Colors.white,
fontSize: 10,
),
textAlign: TextAlign.center,
),
),
)
: new Container()
],
),
);
}

35
lib/widget/banner.dart Normal file
View File

@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import '../config.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
),
),
);
}
}

View File

@@ -0,0 +1,50 @@
import 'dart:math';
import 'package:flutter/material.dart';
class TabIndicationPainter extends CustomPainter {
Paint painter;
final double dxTarget;
final double dxEntry;
final double radius;
final double dy;
final PageController pageController;
TabIndicationPainter(
{this.dxTarget = 125.0,
this.dxEntry = 25.0,
this.radius = 15.0,
this.dy = 20.0, this.pageController}) : super(repaint: pageController) {
painter = new Paint()
..color = Color(0xFFFFFFFF)
..style = PaintingStyle.fill;
}
@override
void paint(Canvas canvas, Size size) {
final pos = pageController.position;
double fullExtent = (pos.maxScrollExtent - pos.minScrollExtent + pos.viewportDimension);
double pageOffset = pos.extentBefore / fullExtent;
bool left2right = dxEntry < dxTarget;
Offset entry = new Offset(left2right ? dxEntry: dxTarget, dy);
Offset target = new Offset(left2right ? dxTarget : dxEntry, dy);
Path path = new Path();
path.addArc(
new Rect.fromCircle(center: entry, radius: radius), 0.5 * pi, 1 * pi);
path.addRect(
new Rect.fromLTRB(entry.dx, dy - radius, target.dx, dy + radius));
path.addArc(
new Rect.fromCircle(center: target, radius: radius), 1.5 * pi, 1 * pi);
canvas.translate(size.width * pageOffset, 0.0);
canvas.drawPath(path, painter);
}
@override
bool shouldRepaint(TabIndicationPainter oldDelegate) => true;
}

154
lib/widget/img_file.dart Normal file
View File

@@ -0,0 +1,154 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:image_picker/image_picker.dart';
import 'package:fcs/widget/show_img.dart';
typedef OnFile = void Function(File);
class ImageFile extends StatefulWidget {
final String title;
final OnFile onFile;
final bool enabled;
final String initialImgUrl;
final ImageSource imageSource;
const ImageFile(
{Key key,
this.title,
this.onFile,
this.enabled = true,
this.initialImgUrl,
this.imageSource = ImageSource.gallery})
: super(key: key);
@override
_ImageFileState createState() => _ImageFileState();
}
class _ImageFileState extends State<ImageFile> {
String url;
File file;
@override
void initState() {
super.initState();
this.url = widget.initialImgUrl == null || widget.initialImgUrl == ""
? null
: widget.initialImgUrl;
}
@override
Widget build(BuildContext context) {
return Container(
height: 30,
child: this.file == null && this.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.pickImage(
source: camera ? ImageSource.camera : ImageSource.gallery,
imageQuality: 80,
maxWidth: 1000);
if (selectedFile != null) {
setState(() {
this.file = selectedFile;
});
if (widget.onFile != null) {
widget.onFile(selectedFile);
}
}
}
},
)
: Padding(
padding: const EdgeInsets.only(left: 3.0),
child: Chip(
avatar: Icon(Icons.image),
onDeleted: !widget.enabled
? null
: () {
setState(() {
this.file = null;
this.url = null;
if (widget.onFile != null) {
widget.onFile(null);
}
});
},
deleteIcon: Icon(
Icons.close,
),
label: InkWell(
onTap: () => {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ShowImage(
imageFile: file,
url: file == null
? widget.initialImgUrl
: null,
fileName: widget.title)),
)
},
child: Text(widget.title)),
),
),
);
}
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,
),
onPressed: () {
Navigator.pop(context);
cameraPress();
}),
Text("Camera")
],
),
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
IconButton(
icon: Icon(Icons.photo_library, size: 30),
onPressed: () {
Navigator.pop(context);
photoPress();
}),
Text("Gallery")
],
),
],
),
),
),
);
},
);
}
}

36
lib/widget/img_url.dart Normal file
View File

@@ -0,0 +1,36 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:fcs/widget/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),
),
);
}
}

View File

@@ -0,0 +1,46 @@
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:fcs/theme/theme.dart';
import 'package:fcs/widget/img_url.dart';
import 'package:fcs/widget/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: Wrap(
alignment: number ? WrapAlignment.spaceBetween : WrapAlignment.start,
// scrollDirection: Axis.horizontal,
children: <Widget>[
LocalText(context, label),
// number ? Spacer() : Container(),
Container(
padding: EdgeInsets.only(left: 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;
}

View File

@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:fcs/model/language_model.dart';
import 'package:fcs/theme/theme.dart';
import 'localization/app_translations.dart';
class LocalText extends Text {
final BuildContext context;
LocalText(this.context, String translationKey,
{Color color,
double fontSize,
FontWeight fontWeight,
List<String> translationVariables,
bool underline=false})
: super(
AppTranslations.of(context).text(translationKey,
translationVariables: translationVariables),
style: Provider.of<LanguageModel>(context).isEng
? newLabelStyle(
color: color,
fontSize: fontSize,
fontWeight: fontWeight,
underline: underline)
: newLabelStyleMM(
color: color,
fontSize: fontSize,
fontWeight: fontWeight,
underline: underline));
}

View File

@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:fcs/model/language_model.dart';
import 'package:fcs/theme/theme.dart';
import 'localization/app_translations.dart';
class LocalTextField extends StatelessWidget {
final TextEditingController textEditingController;
final String labelKey;
final Widget icon;
final String Function(String) validator;
final TextInputType textInputType;
final int maxLines;
const LocalTextField(
{Key key,
this.textEditingController,
this.labelKey,
this.icon,
this.validator,
this.textInputType,
this.maxLines})
: super(key: key);
Widget build(BuildContext context) {
var languageModel = Provider.of<LanguageModel>(context);
return Padding(
padding: const EdgeInsets.all(5.0),
child: TextFormField(
controller: textEditingController,
autofocus: false,
cursorColor: primaryColor,
keyboardType: textInputType,
maxLines: maxLines,
decoration: new InputDecoration(
labelText: AppTranslations.of(context).text(labelKey),
labelStyle: languageModel.isEng ? labelStyle : labelStyleMM,
icon: this.icon,
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: primaryColor, width: 1.0)),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: primaryColor, width: 1.0)),
),
validator: this.validator,
),
);
}
}

View File

@@ -0,0 +1,42 @@
import 'dart:async';
import 'dart:convert';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
class AppTranslations {
Locale locale;
static Map<dynamic, dynamic> _localisedValues;
AppTranslations(Locale locale) {
this.locale = locale;
}
static AppTranslations of(BuildContext context) {
return Localizations.of<AppTranslations>(context, AppTranslations);
}
static Future<AppTranslations> load(Locale locale) async {
AppTranslations appTranslations = AppTranslations(locale);
String jsonContent = await rootBundle
.loadString("assets/local/localization_${locale.languageCode}.json");
_localisedValues = json.decode(jsonContent);
return appTranslations;
}
get currentLanguage => locale.languageCode;
String text(String key, {List<String> translationVariables}) {
String value = _localisedValues[key];
if (value == null) {
return "$key not found";
}
if (translationVariables != null) {
translationVariables.asMap().forEach((i, s) {
value = value.replaceAll("{$i}", s);
});
}
return value;
}
}

View File

@@ -0,0 +1,25 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'app_translations.dart';
import 'transalation.dart';
class AppTranslationsDelegate extends LocalizationsDelegate<AppTranslations> {
final Locale newLocale;
const AppTranslationsDelegate({this.newLocale});
@override
bool isSupported(Locale locale) {
return Translation().supportedLanguagesCodes.contains(locale.languageCode);
}
@override
Future<AppTranslations> load(Locale locale) {
return AppTranslations.load(newLocale ?? locale);
}
@override
bool shouldReload(LocalizationsDelegate<AppTranslations> old) {
return true;
}
}

View File

@@ -0,0 +1,27 @@
import 'dart:ui';
typedef void LocaleChangeCallback(Locale locale);
class Translation {
static final Translation _translation = Translation._internal();
factory Translation() {
return _translation;
}
Translation._internal();
final List<String> supportedLanguages = [
"English",
"မြန်မာ ",
];
final List<String> supportedLanguagesCodes = ["en", "mu"];
//returns the list of supported Locales
Iterable<Locale> supportedLocales() =>
supportedLanguagesCodes.map<Locale>((language) => Locale(language, ""));
//function to be invoked when changing the language
LocaleChangeCallback onLocaleChanged;
}

View File

@@ -0,0 +1,70 @@
import 'dart:io';
import 'package:fcs/widget/multi_img_file.dart';
typedef CallBack = void Function();
class MultiImgController {
List<String> imageUrls;
List<FileContainer> addedFiles = [];
List<FileContainer> removedFiles = [];
List<FileContainer> fileContainers = [];
CallBack callback;
MultiImgController() {
fileContainers = [FileContainer()];
}
set setImageUrls(List<String> imageUrls) {
if (imageUrls == null) {
fileContainers.add(FileContainer());
return;
}
fileContainers.clear();
this.imageUrls = imageUrls;
imageUrls.forEach((e) {
fileContainers.add(FileContainer(url: e));
});
fileContainers.add(FileContainer());
if (callback != null) {
callback();
}
}
void onChange(CallBack callBack) {
this.callback = callBack;
}
set addFile(FileContainer fileContainer) {
// if (fileContainers.contains(fileContainer)) return;
addedFiles.add(fileContainer);
fileContainers.add(FileContainer());
if (callback != null) {
callback();
}
}
set removeFile(FileContainer 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();
}
}

View File

@@ -0,0 +1,200 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:image_picker/image_picker.dart';
import 'package:fcs/widget/multi_img_controller.dart';
import 'package:fcs/widget/show_img.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<FileContainer> 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 new Column(
children: new List.generate(
widget.enabled ? fileContainers.length : fileContainers.length - 1,
(i) => getFile(fileContainers[i], i)).toList(),
);
// return Container(
// height: 30,
// width: 100,
// child: ListView.builder(
// itemCount: fileContainers.length,
// itemBuilder: (context, i) {
// return getFile(fileContainers[i]);
// },
// ),
// );
}
_fileAdded(FileContainer fileContainer, File selectedFile) {
fileContainer.file = selectedFile;
setState(() {
widget.controller.addFile = fileContainer;
});
}
_fileRemove(FileContainer fileContainer) {
setState(() {
widget.controller.removeFile = fileContainer;
});
}
Widget getFile(FileContainer 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.pickImage(
source: camera ? ImageSource.camera : ImageSource.gallery,
imageQuality: 80,
maxWidth: 1000);
if (selectedFile != null) {
_fileAdded(fileContainer, selectedFile);
}
}
},
)
: 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,
),
onPressed: () {
Navigator.pop(context);
cameraPress();
}),
Text("Camera")
],
),
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
IconButton(
icon: Icon(Icons.photo_library, size: 30),
onPressed: () {
Navigator.pop(context);
photoPress();
}),
Text("Gallery")
],
),
],
),
),
),
);
},
);
}
}
class FileContainer {
String url;
File file;
FileContainer({this.url, this.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;
}
}

View 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.
///
/// ![](https://flutter.github.io/assets-for-api-docs/assets/material/data_table.png)
///
/// ```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,
),
),
);
}
}

View File

@@ -0,0 +1,20 @@
import 'package:flutter/cupertino.dart';
import 'package:intl/intl.dart';
import 'package:fcs/theme/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);
}
}

View File

@@ -0,0 +1,52 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:fcs/model/main_model.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 widget.child;
}
}

113
lib/widget/popupmenu.dart Normal file
View File

@@ -0,0 +1,113 @@
import 'package:flutter/material.dart';
import 'package:fcs/vo/popup_menu.dart';
class SelectedOption extends StatelessWidget {
final PopupMenu choice;
SelectedOption({Key key, this.choice}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
choice.status,
style: TextStyle(color: Colors.white, fontSize: 30),
)
],
),
),
);
}
}
class PopupMenuList {
PopupMenuList({this.status, this.index, this.context});
final String status;
final String index;
final BuildContext context;
}
List<PopupMenu> userpopup = <PopupMenu>[
PopupMenu(index: 1, status: "Block"),
PopupMenu(index: 2, status: "Unblock"),
// PopupMenu(status: "Delete"),
];
List<PopupMenu> blocklistpopup = <PopupMenu>[
PopupMenu(index: 1, status: "Unblock"),
// PopupMenu(status: "Reset Password"),
// PopupMenu(status: "Delete"),
];
List<PopupMenu> poMenus = <PopupMenu>[
PopupMenu(status: "Create DO"),
];
List<PopupMenu> storageMenus = <PopupMenu>[
PopupMenu(status: "Inventory Takings"),
];
List<PopupMenu> buyerMenu = <PopupMenu>[
PopupMenu(status: "Allocate Quota"),
];
List<PopupMenu> statusMenu = <PopupMenu>[
PopupMenu(index: 0, status: "All"),
PopupMenu(index: 1, status: "pending"),
PopupMenu(index: 2, status: "approved"),
PopupMenu(index: 3, status: "rejected"),
PopupMenu(index: 4, status: "closed"),
PopupMenu(index: 5, status: "canceled"),
PopupMenu(index: 6, status: "expired"),
];
List<PopupMenu> deliveryStatusMenu = <PopupMenu>[
PopupMenu(index: 0, status: "All"),
PopupMenu(index: 2, status: "approved"),
PopupMenu(index: 4, status: "closed"),
];
List<PopupMenu> buyerStatusMenu = <PopupMenu>[
PopupMenu(index: 0, status: "All"),
PopupMenu(index: 1, status: "pending"),
PopupMenu(index: 2, status: "approved"),
PopupMenu(index: 3, status: "rejected"),
];
List<PopupMenu> deviceMenu = <PopupMenu>[
PopupMenu(index: 0, status: "Confirm"),
PopupMenu(index: 1, status: "Logout"),
PopupMenu(index: 2, status: "Set parimary Device"),
];
List<PopupMenu> notificationMenu = <PopupMenu>[
PopupMenu(index: 0, status: "All"),
PopupMenu(index: 1, status: "PO"),
PopupMenu(index: 2, status: "DO"),
PopupMenu(index: 3, status: "Buyer"),
];
List<PopupMenu> reportpopup = <PopupMenu>[
PopupMenu(index: 0, status: "Duplicate"),
PopupMenu(index: 1, status: "Edit"),
PopupMenu(index: 2, status: "Delete"),
PopupMenu(index: 3, status: "Assign User"),
];
List<PopupMenu> reportUserPopup = <PopupMenu>[
PopupMenu(index: 3, status: "Assign User"),
];
List<PopupMenu> announcementMenu = <PopupMenu>[
PopupMenu(index: 1, status: "Edit"),
PopupMenu(index: 2, status: "Delete"),
];
List<PopupMenu> userMenu = <PopupMenu>[
PopupMenu(index: 0, status: "Ascending by name"),
PopupMenu(index: 1, status: "Descending by name"),
PopupMenu(index: 2, status: "Ascending by phone"),
PopupMenu(index: 3, status: "Descending by phone"),
];

231
lib/widget/products.dart Normal file
View File

@@ -0,0 +1,231 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.dart';
import 'package:progress/progress.dart';
import 'package:provider/provider.dart';
import 'package:fcs/charts/lines.dart';
import 'package:fcs/model/main_model.dart';
import 'package:fcs/model/product_model.dart';
import 'package:fcs/pages/po/po_submission_form.dart';
import 'package:fcs/theme/theme.dart';
import 'package:fcs/vo/product.dart';
import 'package:fcs/widget/local_text.dart';
import 'package:fcs/widget/localization/app_translations.dart';
import 'package:fcs/widget/products_price_table.dart';
import 'package:zefyr/zefyr.dart';
class ProductsWidget extends StatefulWidget {
final bool isWelcomePage;
const ProductsWidget({Key key, this.isWelcomePage = false}) : super(key: key);
@override
_ProductsWidgetState createState() =>
_ProductsWidgetState(this.isWelcomePage);
}
class _ProductsWidgetState extends State<ProductsWidget> {
final bool isWelcomePage;
final double dotSize = 10.0;
bool _isLoading = false;
DateTime date;
Map<String, int> measures;
_ProductsWidgetState(this.isWelcomePage);
@override
Widget build(BuildContext context) {
var productModel = Provider.of<ProductModel>(context);
var mainModel = Provider.of<MainModel>(context);
var chartWidget = ProductsChart.fromModel(productModel);
var openHour = "", closeHour = "", openDates = "", date = "";
if (mainModel.setting != null) {
var setting = mainModel.setting;
date = setting.priceLastUpdate != null
? DateFormat('dd MMM yyyy hh:mm:ss a')
.format(setting.priceLastUpdate)
: "";
openHour = setting.getPoOpenAt;
closeHour = setting.getPoCloseAt;
openDates = setting.getPoOpenOn;
}
return Progress(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
LocalText(context, "welcome.price.updateinfo",
color: Colors.black, translationVariables: ["$date"]),
Container(
padding: const EdgeInsets.only(left: 5, right: 5),
child: Column(
children: _buildProductList(productModel.products),
),
),
Padding(
padding: const EdgeInsets.only(left: 5, right: 5),
child: Card(
elevation: 10,
child: Column(
children: <Widget>[
LocalText(context, "welcome.price.trend",
color: Colors.black),
Container(
padding: EdgeInsets.only(left: 5),
height: 200,
child: chartWidget),
Row(
children: <Widget>[
!mainModel.setting.isPOClose && mainModel.isRegBuyer()
? Padding(
padding: const EdgeInsets.only(left: 8.0),
child: RaisedButton(
elevation: 8,
color: Colors.white,
child: Row(
children: <Widget>[
Image.asset(
"assets/pay.png",
width: 30,
height: 30,
color: primaryColor,
),
Padding(
padding:
const EdgeInsets.only(left: 8.0),
child: LocalText(
context, "product.purchase.order",
color: primaryColor),
),
],
),
onPressed: mainModel.setting.isPOClose ||
!mainModel.isRegBuyer()
? null
: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
POSubmissionForm()),
);
},
),
)
: Container(),
Spacer(),
Container(
alignment: Alignment.bottomRight,
padding: EdgeInsets.only(right: 15,bottom: 5),
child: InkWell(
onTap: () => {
Navigator.of(context).push(MaterialPageRoute(
builder: (_) => ProductPriceTable()))
},
child: LocalText(
context,
"welcome.price.detail",
color: Colors.blue,
underline: true,
fontSize: 15,
),
),
),
],
),
],
)),
),
SizedBox(height: 10),
mainModel.setting.isPOClose
? LocalText(
context,
"product.close",
color: Colors.red,
)
: Container(),
LocalText(context, "product.open",
color: Colors.blue,
translationVariables: [openHour, closeHour, openDates]),
],
),
),
inAsyncCall: _isLoading,
opacity: 0.7,
progressIndicator: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CircularProgressIndicator(
backgroundColor: primaryColor,
),
SizedBox(
height: 10,
),
Text(AppTranslations.of(context).text("load"))
],
),
);
}
List<Widget> _buildProductList(List<Product> products) {
List<Widget> productItems = [];
for (var p in products) {
productItems.add(_buildProductItem(p));
}
return productItems;
}
Widget _buildProductItem(Product product) {
return Card(
elevation: 10,
color: Colors.white,
child: Row(
children: <Widget>[
Expanded(
child: InkWell(
onTap: () {},
child: new Padding(
padding: const EdgeInsets.symmetric(vertical: 3.0),
child: new Row(
children: <Widget>[
new Padding(
padding: new EdgeInsets.symmetric(
horizontal: 30.0 - dotSize / 2),
child: Icon(
FontAwesomeIcons.tag,
color: Color(product.color),
size: 20,
),
),
new Expanded(
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Text(
product.name,
style: new TextStyle(
fontSize: 25.0, color: Colors.black),
),
],
),
)
],
),
),
),
),
Padding(
padding: const EdgeInsets.only(right: 20),
child: new Text(
product.price.toString(),
style: new TextStyle(fontSize: 20.0, color: Colors.grey),
),
)
],
),
);
}
}

View File

@@ -0,0 +1,83 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:fcs/model/language_model.dart';
import 'package:fcs/model/product_model.dart';
import 'package:fcs/theme/theme.dart';
import 'package:fcs/vo/product.dart';
import 'package:fcs/widget/local_text.dart';
import 'package:fcs/widget/localization/app_translations.dart';
class ProductPriceTable extends StatefulWidget {
const ProductPriceTable();
@override
_ProductPriceTableState createState() => _ProductPriceTableState();
}
class _ProductPriceTableState extends State<ProductPriceTable> {
final numberFormatter = new NumberFormat("#,###");
var dateFormatter = new DateFormat('dd MMM yyyy\nhh:mm:ss a');
@override
Widget build(BuildContext context) {
var productModel = Provider.of<ProductModel>(context);
return Scaffold(
appBar: AppBar(
backgroundColor: primaryColor,
title: Text(
AppTranslations.of(context).text("products.prices"),
style: Provider.of<LanguageModel>(context).isEng
? TextStyle(fontSize: 18)
: TextStyle(fontSize: 18, fontFamily: 'MyanmarUnicode'),
),
),
body: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
columns: [
DataColumn(label: LocalText(context, "product.update.date")),
DataColumn(label: LocalText(context, "prodcuts")),
DataColumn(label: LocalText(context, "products.prices"),numeric: true),
],
rows: getProductRow(productModel.getPrices),
),
),
),
);
}
List<DataRow> getProductRow(List<ProductPrice> productPrices) {
ProductPrice prev;
bool isOdd = false;
return productPrices.map((p) {
if (prev != null && prev.date.compareTo(p.date) != 0) {
isOdd = !isOdd;
}
var r = DataRow(
cells: [
DataCell(
new Text(dateFormatter.format(p.date),
style: isOdd ? textStyle : textStyleOdd),
),
DataCell(
new Text(
p.name,
style: isOdd ? textStyle : textStyleOdd,
),
),
DataCell(
new Text(numberFormatter.format(p.price),
style: isOdd ? textStyle : textStyleOdd),
),
],
);
prev = p;
return r;
}).toList();
}
}

47
lib/widget/progress.dart Normal file
View File

@@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:progress/progress.dart';
import 'package:provider/provider.dart';
import 'package:fcs/model/main_model.dart';
import 'package:fcs/theme/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) {
Provider.of<MainModel>(context).resetPinTimer();
// hide keyboard
if (inAsyncCall) {
FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus) {
currentFocus.unfocus();
}
}
return super.build(context);
}
}

35
lib/widget/show_img.dart Normal file
View File

@@ -0,0 +1,35 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
import 'package:fcs/theme/theme.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()),
);
}
}

101
lib/widget/sign_file.dart Normal file
View File

@@ -0,0 +1,101 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:fcs/widget/show_img.dart';
typedef OnFile = void Function(File);
class SignFile extends StatefulWidget {
final String title;
final OnFile onFile;
final bool enabled;
final String initialImgUrl;
final ImageSource imageSource;
const SignFile(
{Key key,
this.title,
this.onFile,
this.enabled = true,
this.initialImgUrl,
this.imageSource = ImageSource.gallery})
: super(key: key);
@override
_SignFileState createState() => _SignFileState();
}
class _SignFileState extends State<SignFile> {
String url;
File file;
@override
void initState() {
super.initState();
this.url = widget.initialImgUrl == null || widget.initialImgUrl == ""
? null
: widget.initialImgUrl;
}
@override
Widget build(BuildContext context) {
return Container(
height: 30,
child: this.file == null && this.url == null
? IconButton(
padding: const EdgeInsets.all(3.0),
icon: Icon(Icons.attach_file),
onPressed: () async {
if (!widget.enabled) return;
var selectedFile = await ImagePicker.pickImage(
source: widget.imageSource,
imageQuality: 80,
maxWidth: 1000);
if (selectedFile != null) {
setState(() {
this.file = selectedFile;
});
if (widget.onFile != null) {
widget.onFile(selectedFile);
}
}
},
)
: Padding(
padding: const EdgeInsets.only(left: 3.0),
child: Chip(
avatar: Icon(Icons.image),
onDeleted: !widget.enabled
? null
: () {
setState(() {
this.file = null;
this.url = null;
if (widget.onFile != null) {
widget.onFile(null);
}
});
},
deleteIcon: Icon(
Icons.close,
),
label: InkWell(
onTap: () => {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ShowImage(
imageFile: file,
url: file == null
? widget.initialImgUrl
: null,
fileName: widget.title)),
)
},
child: Text(widget.title)),
),
),
);
}
}