component updates

This commit is contained in:
MohammadTaha Basiri 2022-01-29 13:55:03 +03:30
parent 17f43c3691
commit 0ef35cee2a
11 changed files with 448 additions and 132 deletions

View File

@ -4,15 +4,13 @@ import 'package:flutter/material.dart';
class BookmarkButton extends StatefulWidget {
final bool value;
final VoidCallback onMark;
final VoidCallback onUnmark;
final void Function(bool value) onMarkChanged;
final bool bigGestureSize;
const BookmarkButton({
Key? key,
required this.value,
required this.onMark,
required this.onUnmark,
this.bigGestureSize = false,
required this.onMarkChanged,
}) : super(key: key);
@override
@ -43,7 +41,7 @@ class _BookmarkButtonState extends State<BookmarkButton> {
setState(() {
_value = !_value;
});
_value ? widget.onMark() : widget.onUnmark();
widget.onMarkChanged(_value);
},
);
}

View File

@ -36,6 +36,8 @@ class DidvanAppBar extends StatelessWidget {
if (appBarData.subtitle != null)
DidvanText(
appBarData.subtitle!,
maxLines: 1,
overflow: TextOverflow.clip,
style: Theme.of(context).textTheme.overline,
color: Theme.of(context).colorScheme.caption,
),

View File

@ -124,7 +124,18 @@ class _DidvanPageViewState extends State<DidvanPageView> {
Widget _contentBuilder(dynamic item, int index) {
final content = item.contents[index];
if (content.text != null) {
return Html(data: item.contents[index].text!);
return Html(
data: content.text,
style: {
'*': Style(
direction: TextDirection.rtl,
lineHeight: LineHeight.percent(135),
// textAlign: TextAlign.justify,
margin: EdgeInsets.zero,
padding: EdgeInsets.zero,
),
},
);
}
if (content.image != null) {
return SkeletonImage(
@ -147,11 +158,13 @@ class _DidvanPageViewState extends State<DidvanPageView> {
for (var i = 0; i < item.categories.length; i++)
DidvanText(
item.categories[i].label +
' - ' +
DateTimeUtils.momentGenerator(item.createdAt) +
'${i != item.categories.length - 1 ? '،' : ''} ',
style: Theme.of(context).textTheme.caption,
),
DidvanText(
' - ' + DateTimeUtils.momentGenerator(item.createdAt),
style: Theme.of(context).textTheme.caption,
),
],
);
} else {

View File

@ -1,4 +1,4 @@
import 'package:didvan/pages/home/settings/widgets/menu_item.dart';
import 'package:didvan/pages/home/widgets/menu_item.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

View File

@ -105,7 +105,7 @@ class _DidvanTextFieldState extends State<DidvanTextField> {
enabled: widget.enabled,
border: InputBorder.none,
hintText: widget.hintText,
errorStyle: const TextStyle(height: 0),
errorStyle: const TextStyle(height: 0.01),
hintStyle: Theme.of(context)
.textTheme
.bodyText2!
@ -115,22 +115,23 @@ class _DidvanTextFieldState extends State<DidvanTextField> {
),
const SizedBox(height: 8),
AnimatedVisibility(
isVisible: _error != null,
duration: DesignConfig.lowAnimationDuration,
child: Row(
children: [
Icon(
DidvanIcons.lightbulb_exclamation_regular,
color: Theme.of(context).colorScheme.error,
size: 14,
),
DidvanText(
_error ?? '',
style: Theme.of(context).textTheme.caption,
color: Theme.of(context).colorScheme.error,
),
],
))
isVisible: _error != null,
duration: DesignConfig.lowAnimationDuration,
child: Row(
children: [
Icon(
DidvanIcons.lightbulb_exclamation_regular,
color: Theme.of(context).colorScheme.error,
size: 14,
),
DidvanText(
_error ?? '',
style: Theme.of(context).textTheme.caption,
color: Theme.of(context).colorScheme.error,
),
],
),
)
],
);
}

View File

@ -1,10 +1,9 @@
import 'package:didvan/config/design_config.dart';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/news_details_data.dart';
import 'package:didvan/models/radar_details_data.dart';
import 'package:didvan/models/category.dart';
import 'package:didvan/models/view/action_sheet_data.dart';
import 'package:didvan/pages/home/settings/widgets/menu_item.dart';
import 'package:didvan/pages/home/widgets/menu_item.dart';
import 'package:didvan/routes/routes.dart';
import 'package:didvan/utils/action_sheet.dart';
import 'package:didvan/widgets/bookmark_button.dart';
@ -16,18 +15,27 @@ import 'package:didvan/widgets/item_title.dart';
import 'package:flutter/material.dart';
class FloatingNavigationBar extends StatefulWidget {
final RadarDetailsData? radar;
final NewsDetailsData? news;
final ScrollController scrollController;
final VoidCallback onMark;
final VoidCallback onUnmark;
final void Function(int count) onCommentsChanged;
final bool isRadar;
final bool marked;
final int comments;
final int id;
final String title;
final List<Category>? categories;
final void Function(bool value) onMarkChanged;
const FloatingNavigationBar({
Key? key,
this.radar,
this.news,
required this.scrollController,
required this.onMark,
required this.onUnmark,
required this.onCommentsChanged,
required this.onMarkChanged,
required this.isRadar,
required this.marked,
required this.comments,
required this.id,
required this.title,
this.categories,
}) : super(key: key);
@override
@ -35,35 +43,13 @@ class FloatingNavigationBar extends StatefulWidget {
}
class _FloatingNavigationBarState extends State<FloatingNavigationBar> {
bool get _isRadar => widget.radar != null;
bool _isScrolled = false;
get _item => widget.radar ?? widget.news;
@override
void didUpdateWidget(covariant FloatingNavigationBar oldWidget) {
if (widget.radar != null && oldWidget.radar!.id != widget.radar!.id ||
widget.news != null && oldWidget.news!.id != widget.news!.id) {
_isScrolled = false;
}
super.didUpdateWidget(oldWidget);
}
int _comments = 0;
@override
void initState() {
widget.scrollController.addListener(() {
final position = widget.scrollController.position.pixels;
final offset = MediaQuery.of(context).size.width / 16 * 9 + 40;
if (position > offset && !_isScrolled) {
setState(() {
_isScrolled = true;
});
} else if (position < offset && _isScrolled) {
setState(() {
_isScrolled = false;
});
}
});
_isScrolled = false;
_comments = widget.comments;
super.initState();
}
@ -113,11 +99,10 @@ class _FloatingNavigationBarState extends State<FloatingNavigationBar> {
),
),
const Spacer(),
if (_isRadar)
if (widget.isRadar)
BookmarkButton(
value: _item.marked,
onMark: widget.onMark,
onUnmark: widget.onUnmark,
value: widget.marked,
onMarkChanged: widget.onMarkChanged,
bigGestureSize: true,
),
SizedBox(
@ -125,9 +110,9 @@ class _FloatingNavigationBarState extends State<FloatingNavigationBar> {
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_item.comments != 0)
if (_comments != 0)
DidvanText(
_item.comments.toString(),
_comments.toString(),
color: foregroundColor,
),
DidvanIconButton(
@ -135,9 +120,10 @@ class _FloatingNavigationBarState extends State<FloatingNavigationBar> {
onPressed: () => Navigator.of(context).pushNamed(
Routes.comments,
arguments: {
'id': _item.id,
'isRadar': _isRadar,
'title': _item.title
'id': widget.id,
'isRadar': widget.isRadar,
'title': widget.title,
'onCommentsChanged': widget.onCommentsChanged,
},
),
icon: DidvanIcons.chats_regular,
@ -145,15 +131,14 @@ class _FloatingNavigationBarState extends State<FloatingNavigationBar> {
],
),
),
if (!_isRadar) const SizedBox(width: 12),
if (!_isRadar)
if (!widget.isRadar) const SizedBox(width: 12),
if (!widget.isRadar)
BookmarkButton(
value: _item.marked,
onMark: widget.onMark,
onUnmark: widget.onUnmark,
value: widget.marked,
onMarkChanged: widget.onMarkChanged,
bigGestureSize: true,
),
if (_isRadar)
if (widget.isRadar)
DidvanIconButton(
gestureSize: 32,
onPressed: _showMoreOptions,
@ -166,6 +151,7 @@ class _FloatingNavigationBarState extends State<FloatingNavigationBar> {
}
void _showMoreOptions() {
final categories = widget.categories!;
ActionSheetUtils.showBottomSheet(
data: ActionSheetData(
content: Column(
@ -176,21 +162,21 @@ class _FloatingNavigationBarState extends State<FloatingNavigationBar> {
icon: DidvanIcons.profile_regular,
),
const SizedBox(height: 16),
for (var i = 0; i < _item.categories.length; i++) ...[
for (var i = 0; i < categories.length; i++) ...[
Padding(
padding: const EdgeInsets.only(right: 20),
child: MenuItem(
titleWidget: DidvanChip(label: _item.categories[i].label),
titleWidget: DidvanChip(label: categories[i].label),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushNamed(
Routes.direct,
arguments: _item.categories[i].id,
arguments: categories[i].id,
);
},
),
),
if (i != _item.categories.length - 1)
if (i != categories.length - 1)
const Padding(
padding: EdgeInsets.only(right: 20),
child: DidvanDivider(verticalPadding: 8),

View File

@ -0,0 +1,201 @@
import 'package:crop/crop.dart';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:flutter/material.dart';
class ImageCropper extends StatefulWidget {
final Map<String, dynamic> data;
const ImageCropper({Key? key, required this.data}) : super(key: key);
@override
State<ImageCropper> createState() => _ImageCropperState();
}
class _ImageCropperState extends State<ImageCropper> {
double _rotation = 0;
BoxShape shape = BoxShape.rectangle;
final _controller = CropController();
get _bytes => widget.data['bytes'];
get _onCropped => widget.data['onCropped'];
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('برش عکس'),
centerTitle: true,
actions: <Widget>[
IconButton(
onPressed: _cropImage,
tooltip: 'Crop',
icon: const Icon(Icons.crop),
)
],
),
body: Column(
children: <Widget>[
Expanded(
child: Container(
color: Colors.black,
padding: const EdgeInsets.all(8),
child: Crop(
onChanged: (decomposition) {
if (_rotation != decomposition.rotation) {
setState(() {
_rotation = ((decomposition.rotation + 180) % 360) - 180;
});
}
},
controller: _controller,
shape: shape,
child: Image.memory(
_bytes,
fit: BoxFit.cover,
),
helper: shape == BoxShape.rectangle
? Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.white, width: 2),
),
)
: null,
),
),
),
Row(
children: <Widget>[
IconButton(
icon: const Icon(Icons.redo),
tooltip: 'برگرداندن',
onPressed: () {
_controller.rotation = 0;
_controller.scale = 1;
_controller.offset = Offset.zero;
setState(() {
_rotation = 0;
});
},
),
Expanded(
child: SliderTheme(
data: theme.sliderTheme.copyWith(
trackShape: const RectangularSliderTrackShape(),
),
child: Slider(
thumbColor: Theme.of(context).colorScheme.title,
divisions: 360,
value: _rotation,
min: -180,
max: 180,
label: '$_rotation°',
onChanged: (n) {
setState(() {
_rotation = n.roundToDouble();
_controller.rotation = _rotation;
});
},
),
),
),
// PopupMenuButton<BoxShape>(
// icon: const Icon(Icons.crop_free),
// itemBuilder: (context) => [
// const PopupMenuItem(
// child: Text("Box"),
// value: BoxShape.rectangle,
// ),
// const PopupMenuItem(
// child: Text("Oval"),
// value: BoxShape.circle,
// ),
// ],
// tooltip: 'Crop Shape',
// onSelected: (x) {
// setState(() {
// shape = x;
// });
// },
// ),
// PopupMenuButton<double>(
// icon: const Icon(Icons.aspect_ratio),
// itemBuilder: (context) => [
// const PopupMenuItem(
// child: Text("Original"),
// value: 1000 / 667.0,
// ),
// const PopupMenuDivider(),
// const PopupMenuItem(
// child: Text("16:9"),
// value: 16.0 / 9.0,
// ),
// const PopupMenuItem(
// child: Text("4:3"),
// value: 4.0 / 3.0,
// ),
// const PopupMenuItem(
// child: Text("1:1"),
// value: 1,
// ),
// const PopupMenuItem(
// child: Text("3:4"),
// value: 3.0 / 4.0,
// ),
// const PopupMenuItem(
// child: Text("9:16"),
// value: 9.0 / 16.0,
// ),
// ],
// tooltip: 'Aspect Ratio',
// onSelected: (x) {
// _controller.aspectRatio = x;
// setState(() {});
// },
// ),
],
),
],
),
);
}
void _cropImage() async {
final pixelRatio = MediaQuery.of(context).devicePixelRatio;
final cropped = await _controller.crop(pixelRatio: pixelRatio);
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => Scaffold(
appBar: AppBar(
title: const Text('تایید برش'),
centerTitle: true,
actions: [
Builder(
builder: (context) => IconButton(
icon: const Icon(
DidvanIcons.check_circle_solid,
size: 32,
),
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context).pop();
_onCropped();
},
),
),
],
),
body: Center(
child: RawImage(
image: cropped,
),
),
),
fullscreenDialog: true,
),
);
}
}

View File

@ -0,0 +1,75 @@
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/item_overview.dart';
import 'package:didvan/widgets/didvan/card.dart';
import 'package:didvan/widgets/didvan/text.dart';
import 'package:didvan/widgets/skeleton_image.dart';
import 'package:flutter/material.dart';
import 'package:persian_number_utility/persian_number_utility.dart';
class MultitypeItem extends StatelessWidget {
final ItemOverview item;
const MultitypeItem({Key? key, required this.item}) : super(key: key);
@override
Widget build(BuildContext context) {
return DidvanCard(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
SkeletonImage(imageUrl: item.image, height: 80, width: 80),
Container(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondary,
borderRadius: const BorderRadius.horizontal(
left: Radius.circular(10),
),
),
child: Icon(
item.type == 'radar'
? DidvanIcons.radar_light
: DidvanIcons.news_light,
color: Theme.of(context).colorScheme.white,
size: 18,
),
),
],
),
const SizedBox(width: 8),
Expanded(
child: SizedBox(
height: 80,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
DidvanText(
item.title,
style: Theme.of(context).textTheme.bodyText1,
),
Row(
children: [
const Icon(
DidvanIcons.calendar_day_light,
size: 18,
),
const SizedBox(width: 4),
DidvanText(
DateTime.parse(item.createdAt).toPersianDateStr(),
style: Theme.of(context).textTheme.overline,
),
// DidvanText('text'),
],
),
],
),
),
),
],
),
);
}
}

View File

@ -1,7 +1,5 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:didvan/services/storage/storage.dart';
import 'package:didvan/widgets/shimmer_placeholder.dart';
import 'package:http/http.dart' as http;
import 'package:cached_network_image/cached_network_image.dart';
@ -31,8 +29,8 @@ class SkeletonImage extends StatefulWidget {
}
class _SkeletonImageState extends State<SkeletonImage> {
Uint8List? _bytes;
late Uint8List _bytes;
bool _isLoading = true;
@override
void initState() {
if (kIsWeb) _getImage();
@ -41,57 +39,76 @@ class _SkeletonImageState extends State<SkeletonImage> {
Future<void> _getImage() async {
final url = RequestHelper.baseUrl + widget.imageUrl;
final storage = StorageService.webStorage;
String? imageCache = storage['image-cache'];
final Map data = imageCache == null ? {} : jsonDecode(imageCache);
if (data.containsKey(url)) {
_bytes = Uint8List.fromList(
List<int>.from(data[url]),
);
} else {
_bytes = (await http.get(
Uri.parse(url),
headers: {'Authorization': 'Bearer ${RequestService.token}'},
))
.bodyBytes;
addImageToStorage();
}
// final storage = StorageService.webStorage;
// String? imageCache = storage['image-cache'];
// final Map data = imageCache == null ? {} : jsonDecode(imageCache);
// if (data.containsKey(url)) {
// _bytes = Uint8List.fromList(
// List<int>.from(data[url]),
// );
// } else {
_bytes = (await http.get(
Uri.parse(url),
headers: {'Authorization': 'Bearer ${RequestService.token}'},
))
.bodyBytes;
// addImageToStorage();
// }
if (mounted) {
setState(() {});
setState(() {
_isLoading = false;
});
}
}
void addImageToStorage() {
final storage = StorageService.webStorage;
String? imageCache = storage['image-cache'];
final Map data = imageCache == null ? {} : Map.from(jsonDecode(imageCache));
data.addAll({RequestHelper.baseUrl + widget.imageUrl: _bytes});
StorageService.webStorage.addAll({'image-cache': jsonEncode(data)});
}
// void addImageToStorage() {
// final storage = StorageService.webStorage;
// String? imageCache = storage['image-cache'];
// final Map data = imageCache == null ? {} : Map.from(jsonDecode(imageCache));
// data.addAll({RequestHelper.baseUrl + widget.imageUrl: _bytes});
// StorageService.webStorage.addAll({'image-cache': jsonEncode(data)});
// }
@override
Widget build(BuildContext context) {
return _aspectRatioGenerator(
child: Builder(builder: (context) {
if (kIsWeb) {
return Builder(
builder: (context) {
if (_bytes == null || _bytes!.isEmpty) {
return ShimmerPlaceholder(
borderRadius: widget.borderRadius,
);
}
return ClipRRect(
borderRadius: widget.borderRadius,
child: Image.memory(
_bytes!,
width: widget.width,
height: widget.height,
fit: BoxFit.cover,
),
);
},
if (_isLoading) {
return ShimmerPlaceholder(
borderRadius: widget.borderRadius,
width: widget.aspectRatio == null ? widget.width : null,
height: widget.aspectRatio == null ? widget.height : null,
);
}
return ClipRRect(
borderRadius: widget.borderRadius,
child: Image.memory(
_bytes,
fit: BoxFit.cover,
width: widget.width,
height: widget.height,
),
);
// Builder(
// builder: (context) {
// if (_bytes == null || _bytes!.isEmpty) {
// return ShimmerPlaceholder(
// borderRadius: widget.borderRadius,
// );
// }
// return ClipRRect(
// borderRadius: widget.borderRadius,
// child: Image.memory(
// _bytes!,
// width: widget.width,
// height: widget.height,
// fit: BoxFit.cover,
// ),
// );
// },
// );
}
return CachedNetworkImage(
httpHeaders: {'Authorization': 'Bearer ${RequestService.token}'},
@ -108,7 +125,9 @@ class _SkeletonImageState extends State<SkeletonImage> {
),
),
progressIndicatorBuilder: (context, url, progress) =>
const ShimmerPlaceholder(),
ShimmerPlaceholder(
borderRadius: widget.borderRadius,
),
);
}),
);

View File

@ -0,0 +1,15 @@
import 'package:didvan/constants/assets.dart';
import 'package:didvan/widgets/state_handlers/empty_state.dart';
import 'package:flutter/material.dart';
class EmptyList extends StatelessWidget {
const EmptyList({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return EmptyState(
asset: Assets.emptyBookmark,
title: 'لیست خالی است',
);
}
}

View File

@ -12,6 +12,7 @@ class SliverStateHandler<T extends CoreProvier> extends SliverList {
final Widget? emptyState;
final Widget? placeholder;
final EdgeInsets? itemPadding;
final bool centerEmptyState;
SliverStateHandler({
Key? key,
required this.state,
@ -22,21 +23,26 @@ class SliverStateHandler<T extends CoreProvier> extends SliverList {
this.placeholder,
this.emptyState,
this.enableEmptyState = false,
this.centerEmptyState = true,
}) : super(
key: key,
delegate: SliverChildBuilderDelegate(
(context, index) {
if (state.appState == AppState.failed) {
return SizedBox(
height: MediaQuery.of(context).size.height - 240,
child: EmptyConnection(
onRetry: onRetry,
return Padding(
padding: EdgeInsets.only(
top: centerEmptyState ? 120 : 20,
bottom: 20,
),
child: EmptyConnection(onRetry: onRetry),
);
}
if (enableEmptyState && state.appState == AppState.idle) {
return SizedBox(
height: MediaQuery.of(context).size.height - 240,
return Padding(
padding: EdgeInsets.only(
top: centerEmptyState ? 120 : 20,
bottom: 20,
),
child: emptyState,
);
}