statistics base version

This commit is contained in:
MohammadTaha Basiri 2022-04-15 02:30:37 +04:30
parent 4d77442337
commit 06e828b8c6
21 changed files with 1019 additions and 224 deletions

View File

@ -192,6 +192,7 @@ extension DidvanColorScheme on ColorScheme {
Color get overlay => brightness == Brightness.dark
? const Color(0xFF0F1011)
: const Color(0xFF292929);
Color get yellow => const Color(0XFFEAA92A);
// Error and success colors
Color get errorBack => brightness == Brightness.dark

View File

@ -0,0 +1,64 @@
class Data {
final String p;
final String h;
final String l;
final String d;
final double dp;
final String dt;
final String t;
final String? tEn;
final String tG;
final String ts;
Data({
required this.p,
required this.h,
required this.l,
required this.d,
required this.dp,
required this.dt,
required this.t,
required this.tEn,
required this.tG,
required this.ts,
});
factory Data.fromJson(Map<String, dynamic> json) => Data(
p: json['p'],
h: json['h'],
l: json['l'],
d: json['d'],
dp: double.parse(json['dp'].toString()),
dt: json['dt'],
t: json['t'],
tEn: json['t_en'],
tG: json['t-g'],
ts: json['ts'],
);
factory Data.fromList(List list) => Data(
p: list[0],
h: list[1],
l: list[2],
d: list[3],
dp: double.parse(list[4].toString().replaceAll('-', '0')),
dt: list[5],
t: list[6],
tEn: list[7],
tG: '',
ts: '',
);
Map<String, dynamic> toJson() => {
'p': p,
'h': h,
'l': l,
'd': d,
'dp': dp,
'dt': dt,
't': t,
't_en': tEn,
't-g': tG,
'ts': ts,
};
}

View File

@ -0,0 +1,29 @@
import 'data.dart';
class StatisticData {
final int id;
final String label;
final String title;
final Data data;
StatisticData({
required this.id,
required this.label,
required this.title,
required this.data,
});
factory StatisticData.fromJson(Map<String, dynamic> json) => StatisticData(
id: json['id'],
label: json['label'],
title: json['title'],
data: Data.fromJson(json['data']),
);
Map<String, dynamic> toJson() => {
'id': id,
'label': label,
'title': title,
'data': data.toJson(),
};
}

View File

@ -16,6 +16,7 @@ class UserProvider extends CoreProvier {
static final List<MapEntry> _radarMarkQueue = [];
static final List<MapEntry> _newsMarkQueue = [];
static final List<MapEntry> _studioMarkQueue = [];
static final List<MapEntry> _statisticMarkQueue = [];
Future<String?> setAndGetToken({String? newToken}) async {
if (newToken == null) {
@ -184,4 +185,20 @@ class UserProvider extends CoreProvier {
_newsMarkQueue.removeWhere((element) => element.key == id);
});
}
static Future<void> changeStatisticMark(int id, bool value) async {
_statisticMarkQueue.add(MapEntry(id, value));
Future.delayed(const Duration(milliseconds: 500), () async {
final MapEntry? lastChange =
_statisticMarkQueue.lastWhereOrNull((item) => item.key == id);
if (lastChange == null) return;
final service = RequestService(RequestHelper.mark(id, 'statistic'));
if (lastChange.value) {
await service.post();
} else {
await service.delete();
}
_statisticMarkQueue.removeWhere((element) => element.key == id);
});
}
}

View File

@ -24,7 +24,9 @@ import 'package:didvan/views/home/settings/direct_list/direct_list_state.dart';
import 'package:didvan/views/home/settings/general_settings/settings.dart';
import 'package:didvan/views/home/settings/general_settings/settings_state.dart';
import 'package:didvan/views/home/settings/profile/profile.dart';
import 'package:didvan/views/home/statistics/statistics_state.dart';
import 'package:didvan/views/home/statistic/statistic_details/statistic_details.dart';
import 'package:didvan/views/home/statistic/statistic_details/statistic_details_state.dart';
import 'package:didvan/views/home/statistic/statistic_state.dart';
import 'package:didvan/views/home/studio/studio_details/studio_details.mobile.dart'
if (dart.library.io) 'package:didvan/views/home/studio/studio_details/studio_details.mobile.dart'
if (dart.library.html) 'package:didvan/views/home/studio/studio_details/studio_details.web.dart';
@ -65,8 +67,8 @@ class RouteGenerator {
ChangeNotifierProvider<StudioState>(
create: (context) => StudioState(),
),
ChangeNotifierProvider<StatisticsState>(
create: (context) => StatisticsState(),
ChangeNotifierProvider<StatisticState>(
create: (context) => StatisticState(),
),
],
child: const Home(),
@ -111,6 +113,15 @@ class RouteGenerator {
pageData: settings.arguments as Map<String, dynamic>,
),
);
case Routes.statisticDetails:
return _createRoute(
ChangeNotifierProvider<StatisticDetailsState>(
create: (context) => StatisticDetailsState(),
child: StatisticDetails(
pageData: settings.arguments as Map<String, dynamic>,
),
),
);
case Routes.directList:
return _createRoute(
ChangeNotifierProvider<DirectListState>(

View File

@ -9,6 +9,7 @@ class Routes {
static const String radarDetails = '/radar-details';
static const String newsDetails = '/news-details';
static const String studioDetails = '/studio-details';
static const String statisticDetails = '/statistic-details';
static const String directList = '/direct-list';
static const String direct = '/direct';
static const String comments = '/comments';

View File

@ -8,6 +8,7 @@ class RequestHelper {
static const String _baseRadarUrl = baseUrl + '/radar';
static const String _baseNewsUrl = baseUrl + '/news';
static const String _baseStudioUrl = baseUrl + '/studio';
static const String _baseStatisticUrl = baseUrl + '/statistic';
static const String _baseDirectUrl = _baseUserUrl + '/direct';
static const String confirmUsername = _baseUserUrl + '/confirmUsername';
@ -118,6 +119,21 @@ class RequestHelper {
MapEntry('asc', args.asc),
]);
static String statisticOverviews(int? category) =>
_baseStatisticUrl +
_urlConcatGenerator(
[MapEntry('category', category)],
);
static String statisticDetails(
String label,
String period,
) =>
_baseStatisticUrl +
'/$label' +
_urlConcatGenerator([
MapEntry('period', period),
]);
static String mark(int id, String type) => baseUrl + '/$type/$id/mark';
static String tracking(int id, String type) =>
baseUrl + '/$type/$id/tracking';

View File

@ -3,7 +3,7 @@ import 'package:didvan/views/home/home_state.dart';
import 'package:didvan/views/home/news/news.dart';
import 'package:didvan/views/home/radar/radar.dart';
import 'package:didvan/views/home/settings/settings.dart';
import 'package:didvan/views/home/statistics/statistics.dart';
import 'package:didvan/views/home/statistic/statistic.dart';
import 'package:didvan/views/home/studio/studio.dart';
import 'package:didvan/views/widgets/didvan/bnb.dart';
import 'package:flutter/material.dart';
@ -37,7 +37,7 @@ class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
controller: _tabController,
children: const [
News(),
Statistics(),
Statistic(),
Radar(),
Studio(),
Settings(),

View File

@ -37,7 +37,7 @@ class _NewsState extends State<News> {
onRetry: () => state.getNews(page: state.page),
state: state,
builder: (context, state) => ListView.builder(
cacheExtent: 1000,
cacheExtent: 1500,
itemBuilder: (context, index) {
if (index == 0) {
return const LogoAppBar();
@ -68,14 +68,21 @@ class _NewsState extends State<News> {
return NewsOverview.placeholder;
}
final news = state.news[index];
return NewsOverview(
news: news,
onMarkChanged: state.onMarkChanged,
newsRequestArgs: NewsRequestArgs(
page: state.page,
endDate: state.endDate,
startDate: state.startDate,
search: state.search,
return Padding(
padding: const EdgeInsets.only(
left: 16,
right: 16,
bottom: 16,
),
child: NewsOverview(
news: news,
onMarkChanged: state.onMarkChanged,
newsRequestArgs: NewsRequestArgs(
page: state.page,
endDate: state.endDate,
startDate: state.startDate,
search: state.search,
),
),
);
},

View File

@ -164,6 +164,7 @@ class _RadarState extends State<Radar> {
if (state.appState != AppState.failed)
CategoriesRow1(
topPadding: 300,
rightPadding: 124,
onSelected: _onCategorySelected,
categories: state.categories,
isColapsed:
@ -180,11 +181,10 @@ class _RadarState extends State<Radar> {
!state.searching &&
!state.filtering)
CategoriesList(
isRadar: true,
categories: state.categories,
isColapsed:
state.isColapsed || state.searching || state.filtering,
onSelected: () => state.getRadars(page: 1),
onSelected: (_) => state.getRadars(page: 1),
selectedCats: state.selectedCats,
),
],

View File

@ -2,30 +2,29 @@ import 'dart:math';
import 'package:didvan/config/design_config.dart';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/assets.dart';
import 'package:didvan/models/category.dart';
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/requests/radar.dart';
import 'package:didvan/views/home/statistics/statistics_state.dart';
import 'package:didvan/models/statistic_data/statistic_data.dart';
import 'package:didvan/views/home/statistic/statistic_state.dart';
import 'package:didvan/views/home/statistic/widgets/statistic_overview.dart';
import 'package:didvan/views/home/widgets/categories_gird.dart';
import 'package:didvan/views/home/widgets/categories_list.dart';
import 'package:didvan/views/home/widgets/logo_app_bar.dart';
import 'package:didvan/views/home/widgets/overview/radar.dart';
import 'package:didvan/views/widgets/animated_visibility.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/state_handlers/empty_state.dart';
import 'package:didvan/views/widgets/state_handlers/empty_list.dart';
import 'package:didvan/views/widgets/state_handlers/sliver_state_handler.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Statistics extends StatefulWidget {
const Statistics({Key? key}) : super(key: key);
class Statistic extends StatefulWidget {
const Statistic({Key? key}) : super(key: key);
@override
State<Statistics> createState() => _RadarState();
State<Statistic> createState() => _StatisticState();
}
class _RadarState extends State<Statistics> {
class _StatisticState extends State<Statistic> {
final ScrollController _scrollController = ScrollController();
bool _isAnimating = false;
@ -35,7 +34,7 @@ class _RadarState extends State<Statistics> {
_scrollController.addListener(() {
_handleAnimations();
});
final state = context.read<StatisticsState>();
final state = context.read<StatisticState>();
state.addListener(() {
if (state.shouldColapse && mounted) {
_handleAnimations(true);
@ -48,7 +47,7 @@ class _RadarState extends State<Statistics> {
@override
Widget build(BuildContext context) {
return Consumer<StatisticsState>(
return Consumer<StatisticState>(
builder: (context, state, child) => Stack(
children: [
CustomScrollView(
@ -60,7 +59,7 @@ class _RadarState extends State<Statistics> {
const SliverToBoxAdapter(child: LogoAppBar()),
if (state.appState != AppState.failed)
const SliverToBoxAdapter(
child: SizedBox(height: 156),
child: SizedBox(height: 180),
),
if (state.appState != AppState.failed)
SliverPadding(
@ -80,85 +79,89 @@ class _RadarState extends State<Statistics> {
),
),
),
SliverStateHandler<StatisticsState>(
onRetry: () => state.getStatistics(page: state.page),
SliverStateHandler<StatisticState>(
onRetry: () => state.getStatistic(page: state.page),
state: state,
itemPadding: const EdgeInsets.only(
bottom: 20,
left: 16,
right: 16,
),
enableEmptyState: state.statistics.isEmpty,
emptyState: Padding(
padding: const EdgeInsets.only(bottom: 120),
child: EmptyState(
asset: Assets.emptyResult,
title: 'موردی برای نمایش وجود ندارد.',
),
),
placeholder: RadarOverview.placeholder,
emptyState: const EmptyList(),
enableEmptyState: _itemCount(state) == 0,
placeholder: StatisticOverview.placeHolder,
builder: (context, state, index) {
index += 2;
if (index % 15 == 0 && state.lastPage != state.page) {
state.getStatistics(page: state.page + 1);
bool isMarked = false;
StatisticData statistic;
if (index < state.markedStatistics.length) {
isMarked = true;
statistic = state.markedStatistics[index];
} else {
statistic =
state.statistics[index - state.markedStatistics.length];
}
index -= 2;
if (index >= state.statistics.length) {
return RadarOverview.placeholder;
}
final radar = state.statistics[index];
return RadarOverview(
radar: radar,
return StatisticOverview(
statistic: statistic,
isMarked: isMarked,
onMarkChanged: state.changeMark,
onCommentsChanged: (id, count) => {},
radarRequestArgs: RadarRequestArgs(
page: state.page,
categories:
List.from(state.selectedCats.map((cat) => cat.id)),
isSingleItem: false,
),
);
},
childCount: state.statistics.length +
(state.lastPage == state.page ? 0 : 3),
childCount: _itemCount(state),
),
if (state.statistics.length == 1)
const SliverToBoxAdapter(
child: SizedBox(height: 320),
SliverToBoxAdapter(
child: SizedBox(
height: state.appState == AppState.busy
? 300
: _itemCount(state) == 0
? 150
: max(
MediaQuery.of(context).size.height -
_itemCount(state) * 120,
0),
),
),
],
),
if (state.appState != AppState.failed)
CategoriesRow1(
onSelected: _onCategorySelected,
categories: state.categories,
categories: List.from(state.categories)..removeAt(0),
isColapsed: state.isColapsed,
topPadding: 120,
topPadding: 144,
rightPadding: 300,
),
if (state.appState != AppState.failed)
CategoriesList(
isRadar: false,
categories: state.categories,
isColapsed: state.isColapsed,
onSelected: () => state.getStatistics(page: 1),
selectedCats: state.selectedCats,
onSelected: (id) {
state.selectedCategoryId = id;
state.getStatistic(page: 1);
},
selectedCats: state.selectedCategory == null
? []
: [state.selectedCategory!],
),
],
),
);
}
int _itemCount(state) =>
state.markedStatistics.length +
(state.selectedCategoryId == 1 ? 0 : state.statistics.length);
void _onCategorySelected(CategoryData category) {
final state = context.read<StatisticsState>();
state.selectedCats.clear();
final state = context.read<StatisticState>();
state.selectedCategoryId = 0;
if (category.id != 0) {
state.selectedCats.add(category);
state.selectedCategoryId = category.id;
}
state.getStatistics(page: 1);
state.getStatistic(page: 1);
}
void _handleAnimations([bool forceAnimate = false]) async {
final state = context.read<StatisticsState>();
final state = context.read<StatisticState>();
if (_isAnimating) return;
final double position = _scrollController.offset;
if (position > 5 && !state.isColapsed || forceAnimate) {
@ -166,14 +169,14 @@ class _RadarState extends State<Statistics> {
_isAnimating = true;
setState(() {});
await _scrollController.animateTo(
200,
228,
duration: DesignConfig.mediumAnimationDuration,
curve: Curves.easeIn,
);
_isAnimating = false;
setState(() {});
} else if (position <
min(_scrollController.position.maxScrollExtent, 200) &&
min(_scrollController.position.maxScrollExtent, 228) &&
state.isColapsed) {
state.isScrolled = false;
_isAnimating = true;

View File

@ -0,0 +1,298 @@
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/view/app_bar_data.dart';
import 'package:didvan/views/home/statistic/statistic_details/statistic_details_state.dart';
import 'package:didvan/views/home/widgets/categories_list.dart';
import 'package:didvan/views/home/widgets/overview/multitype.dart';
import 'package:didvan/views/home/widgets/tag_item.dart';
import 'package:didvan/views/widgets/didvan/card.dart';
import 'package:didvan/views/widgets/didvan/divider.dart';
import 'package:didvan/views/widgets/didvan/icon_button.dart';
import 'package:didvan/views/widgets/didvan/scaffold.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/state_handlers/state_handler.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart' as intl;
class StatisticDetails extends StatefulWidget {
final Map<String, dynamic> pageData;
const StatisticDetails({Key? key, required this.pageData}) : super(key: key);
@override
State<StatisticDetails> createState() => _StatisticDetailsState();
}
class _StatisticDetailsState extends State<StatisticDetails> {
@override
void initState() {
final state = context.read<StatisticDetailsState>();
state.label = widget.pageData['label'];
state.marked = widget.pageData['marked'];
state.currentDateRangeId = 0;
Future.delayed(Duration.zero, state.getStatisticDetails);
super.initState();
}
@override
Widget build(BuildContext context) {
return Consumer<StatisticDetailsState>(
builder: (context, state, child) => DidvanScaffold(
padding: EdgeInsets.zero,
appBarData: AppBarData(
title: widget.pageData['title'],
hasBack: true,
subtitle: 'رادار قیمت‌ها',
trailing: DidvanIconButton(
icon: state.marked ? Icons.star : Icons.star_border,
color: state.marked
? Theme.of(context).colorScheme.yellow
: Theme.of(context).colorScheme.focusedBorder,
size: 32,
onPressed: () {
state.marked = !state.marked;
state.update();
widget.pageData['onMarkChanged'](state.marked);
},
),
),
children: [
StateHandler<StatisticDetailsState>(
topPadding: MediaQuery.of(context).size.height / 3,
builder: (context, state) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 20),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: DidvanText('نمودار تغییرات'),
),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SizedBox(
width: double.infinity,
height: 120,
child: state.chartState == AppState.busy
? SpinKitThreeBounce(
color: Theme.of(context).colorScheme.primary,
size: 24,
)
: LineChart(
LineChartData(
lineTouchData: LineTouchData(
touchTooltipData: LineTouchTooltipData(
tooltipBgColor:
Theme.of(context).colorScheme.navigation,
getTooltipItems: (data) => [
LineTooltipItem(
state.datas[data.first.spotIndex].tEn! +
'\n' +
intl.NumberFormat("###,000", "en_US")
.format(
data.first.bar
.spots[data.first.spotIndex].y,
),
Theme.of(context)
.textTheme
.caption!
.copyWith(
color: Colors.white,
),
),
],
),
),
minX: 0,
maxX: state.datas.length.toDouble() - 1,
maxY: state.maxValue * 1.001,
minY: state.minValue,
gridData: FlGridData(show: false),
borderData: FlBorderData(show: false),
titlesData: FlTitlesData(show: false),
lineBarsData: [
LineChartBarData(
spots: [
for (var i = 0; i < state.datas.length; i++)
FlSpot(
i.toDouble(),
_stringToDouble(state.datas[i].p),
)
],
barWidth: 2,
dotData: FlDotData(
getDotPainter: (p0, p1, p2, p3) =>
FlDotCirclePainter(
color: Colors.transparent,
strokeWidth: 1,
strokeColor:
Theme.of(context).colorScheme.success,
),
),
color: Theme.of(context).colorScheme.success,
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
tileMode: TileMode.decal,
colors: [
Theme.of(context)
.colorScheme
.background,
const Color(0XFFF5B763)
.withOpacity(0.2),
],
),
),
),
],
),
),
),
),
const SizedBox(height: 20),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: DidvanText('بازه نمایش:'),
),
const SizedBox(height: 20),
CategoriesList(
isColapsed: false,
isAppBar: false,
selectedCats: [state.currentDateRange],
categories: state.dateRanges,
onSelected: (id) {
state.currentDateRangeId = id;
state.getStatisticDetails();
},
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: DidvanCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDataItem('قیمت لحظه‌ای', state.data!.p),
const DidvanDivider(verticalPadding: 8),
_buildDataItem('بالاترین قیمت روز', state.data!.h),
const SizedBox(height: 8),
_buildDataItem('پایین‌ترین قیمت روز', state.data!.l),
const SizedBox(height: 8),
_buildDataItem(
'درصد تغییر نسبت به دیروز',
'${state.data!.dp}%',
icon: _diffIcon(state),
color: _diffColor(state),
),
const SizedBox(height: 8),
_buildDataItem(
'میزان تغییر نسبت به دیروز',
state.data!.d,
icon: _diffIcon(state),
color: _diffColor(state),
),
const SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (var i = 0; i < state.tags.length; i++)
TagItem(
tag: state.tags[i],
onMarkChanged: (_, __) {},
type: 'statistic',
),
],
),
],
),
),
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: DidvanCard(
child: Column(
children: [
if (state.relatedContents.isEmpty)
for (var i = 0; i < 3; i++) ...[
MultitypeOverview.placeholder,
if (i != 2) const SizedBox(height: 16)
],
for (var i = 0;
i < state.relatedContents.length;
i++) ...[
MultitypeOverview(
item: state.relatedContents[i],
onMarkChanged: (id, value) {},
),
if (i != state.relatedContents.length - 1)
const SizedBox(height: 16)
]
],
),
),
),
const SizedBox(height: 16),
],
),
onRetry: state.getStatisticDetails,
state: state,
)
],
),
);
}
double _stringToDouble(String value) =>
double.parse(value.replaceAll(',', ''));
Color? _diffColor(StatisticDetailsState state) {
if (state.data!.dp == 0) {
return null;
}
if (state.data!.dt == 'low') {
return Theme.of(context).colorScheme.success;
} else {
return Theme.of(context).colorScheme.error;
}
}
IconData? _diffIcon(StatisticDetailsState state) {
if (state.data!.dp == 0) {
return null;
}
if (state.data!.dt == 'low') {
return DidvanIcons.angle_up_regular;
} else {
return DidvanIcons.angle_down_regular;
}
}
Widget _buildDataItem(
String title,
String value, {
IconData? icon,
bool isBold = false,
Color? color,
}) {
return Row(
children: [
DidvanText(
title,
style: isBold
? Theme.of(context).textTheme.bodyText1
: Theme.of(context).textTheme.bodyText2,
),
const Spacer(),
if (icon != null) Icon(icon, color: color),
DidvanText(value, color: color),
],
);
}
}

View File

@ -0,0 +1,168 @@
import 'package:didvan/models/category.dart';
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/overview_data.dart';
import 'package:didvan/models/statistic_data/data.dart';
import 'package:didvan/models/tag.dart';
import 'package:didvan/providers/core.dart';
import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart';
import 'package:collection/collection.dart';
class StatisticDetailsState extends CoreProvier {
late bool marked;
late String label;
String? startDate;
String? endDate;
int currentDateRangeId = 0;
final List<Data> datas = [];
final List<OverviewData> relatedContents = [];
final List<Tag> tags = [];
Data? data;
double maxValue = 0;
double? minValue;
AppState chartState = AppState.idle;
final dateRanges = [
CategoryData(
id: 0,
label: 'هفتگی',
asset: 'weekly',
),
CategoryData(
id: 1,
label: 'ماهانه',
asset: 'monthly',
),
CategoryData(
id: 2,
label: 'شش ماهه',
asset: 'semiyearly',
),
CategoryData(
id: 3,
label: 'سالانه',
asset: 'yearly',
),
];
CategoryData get currentDateRange => dateRanges.firstWhere(
(element) => element.id == currentDateRangeId,
);
Future<void> getStatisticDetails() async {
if (data == null) {
final result = await getStatisticCurrentDetails();
if (!result) {
appState = AppState.failed;
return;
}
}
minValue = null;
maxValue = 0;
if (datas.isEmpty) {
appState = AppState.busy;
} else {
chartState = AppState.busy;
notifyListeners();
}
datas.clear();
final service = RequestService(
RequestHelper.statisticDetails(
label,
dateRanges[currentDateRangeId].asset!,
),
);
await service.httpGet();
if (service.isSuccess) {
final result = service.result['data'];
tags.clear();
for (var i = 0; i < service.result['tags'].length; i++) {
tags.add(Tag.fromJson(service.result['tags'][i]));
}
for (var i = 0; i < result.length; i++) {
datas.add(Data.fromList(result[i]));
final highest = _stringToDouble(datas.last.h);
final lowest = _stringToDouble(datas.last.l);
if (highest > maxValue) {
maxValue = highest;
}
if (lowest < (minValue ?? _stringToDouble(datas.last.p))) {
minValue = lowest;
}
}
if (currentDateRangeId != 0 && currentDateRangeId != 1) {
final grouped =
datas.groupListsBy((element) => element.tEn!.split('/')[1]);
datas.clear();
grouped.forEach((key, value) {
datas.add(
Data(
p: _average(value),
h: maxValue.toString(),
l: minValue.toString(),
d: '',
dp: 0,
dt: '',
t: '',
tEn: value.first.tEn!.substring(0, 7),
tG: '',
ts: '',
),
);
});
}
getRelatedContents();
datas.replaceRange(0, datas.length, datas.reversed);
chartState = AppState.idle;
appState = AppState.idle;
return;
}
if (datas.isEmpty) {
appState = AppState.failed;
} else {
chartState = AppState.failed;
notifyListeners();
}
}
Future<bool> getStatisticCurrentDetails() async {
final service = RequestService(
RequestHelper.statisticDetails(
label,
'current',
),
);
await service.httpGet();
if (service.isSuccess) {
data = Data.fromJson(service.result['data']);
}
return service.isSuccess;
}
String _average(List<Data> inputs) {
double sum = 0;
for (var i = 0; i < inputs.length; i++) {
sum += _stringToDouble(inputs[i].p);
}
return (sum / inputs.length).toString();
}
double _stringToDouble(String value) =>
double.parse(value.replaceAll(',', ''));
Future<void> getRelatedContents() async {
if (relatedContents.isNotEmpty) return;
final service = RequestService(RequestHelper.tag(
ids: tags.map((tag) => tag.id).toList(),
));
await service.httpGet();
if (service.isSuccess) {
final relateds = service.result['contents'];
for (var i = 0; i < relateds.length; i++) {
relatedContents.add(OverviewData.fromJson(relateds[i]));
}
notifyListeners();
}
}
}

View File

@ -0,0 +1,115 @@
import 'package:didvan/constants/assets.dart';
import 'package:collection/collection.dart';
import 'package:didvan/models/category.dart';
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/statistic_data/statistic_data.dart';
import 'package:didvan/providers/core.dart';
import 'package:didvan/providers/user.dart';
import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart';
class StatisticState extends CoreProvier {
int page = 1;
bool isScrolled = false;
bool shouldColapse = false;
int selectedCategoryId = -1;
List<CategoryData> categories = [];
final List<StatisticData> statistics = [];
final List<StatisticData> markedStatistics = [];
bool get isColapsed => (isCategorySelected && isScrolled) || isScrolled;
CategoryData? get selectedCategory => categories.firstWhereOrNull(
(element) => element.id == selectedCategoryId,
);
bool get isCategorySelected => selectedCategoryId != 0;
void resetFilters(bool isInit) {
selectedCategoryId = 0;
isScrolled = false;
if (!isInit) {
getStatistic(page: 1);
}
}
Future<void> getStatistic({
required int page,
}) async {
this.page = page;
if (this.page == page) {
statistics.clear();
}
if (page == 1) {
appState = AppState.busy;
}
final RequestService service = RequestService(
RequestHelper.statisticOverviews(
selectedCategoryId == 0 || selectedCategoryId == 1
? null
: selectedCategoryId - 1,
),
);
await service.httpGet();
if (service.isSuccess) {
final others = service.result['others'];
for (var i = 0; i < others.length; i++) {
statistics.add(StatisticData.fromJson(others[i]));
}
final marked = service.result['marked'];
for (var i = 0; i < marked.length; i++) {
statistics.add(StatisticData.fromJson(marked[i]));
}
if (isColapsed || isCategorySelected) {
shouldColapse = true;
}
appState = AppState.idle;
return;
}
appState = AppState.failed;
}
Future<void> changeMark(int id, bool value) async {
final item = statistics.firstWhereOrNull((element) => element.id == id) ??
markedStatistics.firstWhere((element) => element.id == id);
if (value) {
markedStatistics.add(item);
statistics.remove(item);
} else {
markedStatistics.remove(item);
statistics.add(item);
}
UserProvider.changeStatisticMark(id, value);
notifyListeners();
}
void init() {
resetFilters(true);
Future.delayed(Duration.zero, () {
getStatistic(page: 1);
});
categories = [
CategoryData(
id: 1,
label: 'منتخب',
asset: Assets.economicCategoryIcon,
),
CategoryData(
id: 2,
label: 'اقتصاد کلان',
asset: Assets.economicCategoryIcon,
),
CategoryData(
id: 3,
label: 'صنعت فولاد',
asset: Assets.politicalCategoryIcon,
),
CategoryData(
id: 4,
label: 'بازار سرمایه',
asset: Assets.techCategoryIcon,
),
];
}
}

View File

@ -0,0 +1,135 @@
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/statistic_data/statistic_data.dart';
import 'package:didvan/routes/routes.dart';
import 'package:didvan/views/widgets/didvan/card.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/shimmer_placeholder.dart';
import 'package:flutter/material.dart';
class StatisticOverview extends StatelessWidget {
final StatisticData statistic;
final bool isMarked;
final void Function(int id, bool value) onMarkChanged;
const StatisticOverview({
Key? key,
required this.statistic,
required this.isMarked,
required this.onMarkChanged,
}) : super(key: key);
Color _diffColor(context) => statistic.data.dt == 'low'
? Theme.of(context).colorScheme.success
: Theme.of(context).colorScheme.error;
bool get _hasDiff => statistic.data.d != '0';
@override
Widget build(BuildContext context) {
return DidvanCard(
onTap: () =>
Navigator.of(context).pushNamed(Routes.statisticDetails, arguments: {
'onMarkChanged': (value) => onMarkChanged(statistic.id, value),
'label': statistic.label,
'title': statistic.title,
'marked': isMarked,
}),
child: Column(
children: [
Row(
children: [
if (isMarked)
Icon(
Icons.star,
color: Theme.of(context).colorScheme.yellow,
size: 18,
),
DidvanText(
statistic.title,
style: Theme.of(context).textTheme.bodyText1,
),
const Spacer(),
if (_hasDiff)
DidvanText(
'(${statistic.data.d})',
color: _diffColor(context),
),
if (_hasDiff) const SizedBox(width: 8),
DidvanText(
statistic.data.p,
style: Theme.of(context).textTheme.bodyText1,
)
],
),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.trending_down,
size: 18,
color: Theme.of(context).colorScheme.hint,
),
DidvanText(
statistic.data.l,
style: Theme.of(context).textTheme.caption,
color: Theme.of(context).colorScheme.hint,
),
const SizedBox(width: 8),
Icon(
Icons.trending_up,
size: 18,
color: Theme.of(context).colorScheme.hint,
),
DidvanText(
statistic.data.h,
style: Theme.of(context).textTheme.caption,
color: Theme.of(context).colorScheme.hint,
),
const Spacer(),
if (_hasDiff)
Icon(
statistic.data.dt == 'low'
? DidvanIcons.angle_up_regular
: DidvanIcons.angle_down_regular,
size: 18,
color: _diffColor(context),
),
if (_hasDiff) const SizedBox(width: 4),
if (_hasDiff)
DidvanText(
statistic.data.dp.toString() + '%',
style: Theme.of(context).textTheme.caption,
color: _diffColor(context),
),
],
),
],
),
);
}
static Widget get placeHolder => DidvanCard(
child: Column(
children: [
const SizedBox(height: 4),
Row(
children: const [
ShimmerPlaceholder(width: 80, height: 16),
Spacer(),
ShimmerPlaceholder(width: 50, height: 14),
SizedBox(width: 8),
ShimmerPlaceholder(width: 50, height: 16),
],
),
const SizedBox(height: 16),
Row(
children: const [
ShimmerPlaceholder(width: 150, height: 12),
Spacer(),
ShimmerPlaceholder(width: 80, height: 12),
],
),
],
),
);
}

View File

@ -1,96 +0,0 @@
import 'package:didvan/constants/assets.dart';
import 'package:didvan/models/category.dart';
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/overview_data.dart';
import 'package:didvan/models/requests/radar.dart';
import 'package:didvan/providers/core.dart';
import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart';
class StatisticsState extends CoreProvier {
int page = 1;
int lastPage = 1;
bool isScrolled = false;
bool shouldColapse = false;
final List<CategoryData> selectedCats = [];
List<CategoryData> categories = [];
final List<OverviewData> statistics = [];
bool get isColapsed => (isCategorySelected && isScrolled) || isScrolled;
bool get isCategorySelected => selectedCats.length == 1;
void resetFilters(bool isInit) {
selectedCats.clear();
isScrolled = false;
if (!isInit) {
getStatistics(page: 1);
}
}
Future<void> getStatistics({
required int page,
}) async {
this.page = page;
if (this.page == page) {
statistics.clear();
}
if (page == 1) {
appState = AppState.busy;
}
final RequestService service = RequestService(
RequestHelper.radarOverviews(
args: RadarRequestArgs(
page: page,
categories: selectedCats.map((e) => e.id).toList(),
),
),
);
await service.httpGet();
if (service.isSuccess) {
lastPage = service.result['lastPage'];
final radarsList = service.result['radars'];
for (var i = 0; i < radarsList.length; i++) {
statistics.add(OverviewData.fromJson(radarsList[i]));
}
if (isColapsed || isCategorySelected) {
shouldColapse = true;
}
appState = AppState.idle;
return;
}
appState = AppState.failed;
}
Future<void> changeMark(int id, bool value, bool shouldUpdate) async {
statistics.firstWhere((element) => element.id == id).marked = value;
if (shouldUpdate) {
notifyListeners();
}
}
void init() {
resetFilters(true);
Future.delayed(Duration.zero, () {
getStatistics(page: 1);
});
categories = [
CategoryData(
id: 1,
label: 'اقتصاد کلان',
asset: Assets.economicCategoryIcon,
),
CategoryData(
id: 2,
label: 'صنعت فولاد',
asset: Assets.politicalCategoryIcon,
),
CategoryData(
id: 3,
label: 'بازار سرمایه',
asset: Assets.techCategoryIcon,
),
];
}
}

View File

@ -7,6 +7,7 @@ class CategoriesRow1 extends StatelessWidget {
final List<CategoryData> categories;
final bool isColapsed;
final double topPadding;
final double rightPadding;
final void Function(CategoryData data) onSelected;
const CategoriesRow1({
Key? key,
@ -14,6 +15,7 @@ class CategoriesRow1 extends StatelessWidget {
required this.isColapsed,
required this.onSelected,
required this.topPadding,
required this.rightPadding,
}) : super(key: key);
@override
@ -23,8 +25,8 @@ class CategoriesRow1 extends StatelessWidget {
curve: Curves.easeIn,
duration: DesignConfig.mediumAnimationDuration,
top: isColapsed ? -60 : topPadding + d.padding.top,
left: isColapsed ? -80 : 0,
right: isColapsed ? 124 : 0,
left: isColapsed ? -rightPadding : 0,
right: isColapsed ? rightPadding : 0,
child: Row(
children: categories
.sublist(0, 3)

View File

@ -7,17 +7,17 @@ import 'package:flutter/material.dart';
class CategoriesList extends StatefulWidget {
final bool isColapsed;
final bool isAppBar;
final List<CategoryData> selectedCats;
final List<CategoryData> categories;
final VoidCallback onSelected;
final bool isRadar;
final void Function(int id) onSelected;
const CategoriesList({
Key? key,
required this.isColapsed,
required this.selectedCats,
required this.categories,
required this.onSelected,
required this.isRadar,
this.isAppBar = true,
}) : super(key: key);
@override
@ -46,54 +46,60 @@ class _CategoriesListState extends State<CategoriesList> {
@override
Widget build(BuildContext context) {
final MediaQueryData d = MediaQuery.of(context);
return Positioned(
top: 0,
left: 0,
right: 0,
child: AnimatedCrossFade(
crossFadeState: widget.isColapsed
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration: DesignConfig.mediumAnimationDuration,
reverseDuration: DesignConfig.lowAnimationDuration,
sizeCurve: Curves.easeIn,
firstChild: const SizedBox(),
secondChild: Container(
height: 60 + d.padding.top,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
boxShadow: DesignConfig.defaultShadow,
),
child: AnimatedVisibility(
isVisible: widget.isColapsed,
duration: DesignConfig.mediumAnimationDuration,
child: SingleChildScrollView(
controller: _scrollController,
// physics: const BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
padding: EdgeInsets.only(
top: d.padding.top + 12,
bottom: 12,
right: 12,
),
child: Row(
children: [
_itemBuilder(
CategoryData(
label: widget.isRadar ? 'همه' : 'منتخب',
id: 0,
),
context,
),
for (var i = 0; i < widget.categories.length; i++)
_itemBuilder(widget.categories[i], context),
],
final child = SingleChildScrollView(
controller: _scrollController,
scrollDirection: Axis.horizontal,
padding: widget.isAppBar
? EdgeInsets.only(
top: d.padding.top + 12,
bottom: 12,
)
: null,
child: Row(
children: [
const SizedBox(width: 12),
if (widget.isAppBar)
_itemBuilder(
CategoryData(
label: 'همه',
id: 0,
),
context,
),
for (var i = 0; i < widget.categories.length; i++)
_itemBuilder(widget.categories[i], context),
],
),
);
if (widget.isAppBar) {
return Positioned(
top: 0,
left: 0,
right: 0,
child: AnimatedCrossFade(
crossFadeState: widget.isColapsed
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration: DesignConfig.mediumAnimationDuration,
reverseDuration: DesignConfig.lowAnimationDuration,
sizeCurve: Curves.easeIn,
firstChild: const SizedBox(),
secondChild: Container(
height: 60 + d.padding.top,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
boxShadow: DesignConfig.defaultShadow,
),
child: AnimatedVisibility(
isVisible: widget.isColapsed,
duration: DesignConfig.mediumAnimationDuration,
child: child,
),
),
),
),
);
);
}
return child;
}
Widget _itemBuilder(CategoryData category, BuildContext context) {
@ -108,7 +114,7 @@ class _CategoriesListState extends State<CategoriesList> {
duration: DesignConfig.lowAnimationDuration,
curve: Curves.easeIn,
);
widget.onSelected();
widget.onSelected(category.id);
},
child: Container(
margin: const EdgeInsets.only(left: 12),
@ -127,7 +133,7 @@ class _CategoriesListState extends State<CategoriesList> {
color: widget.selectedCats.length == 1 &&
widget.selectedCats.contains(category) ||
category.id == 0 && widget.selectedCats.isEmpty
? Theme.of(context).colorScheme.focused
? Theme.of(context).colorScheme.splash
: null,
border: Border.all(
color: Theme.of(context).colorScheme.focusedBorder,

View File

@ -38,10 +38,13 @@ class StateHandler<T extends CoreProvier> extends StatelessWidget {
case AppState.idle:
return builder(context, state);
case AppState.busy:
return placeholder ??
SpinKitSpinningLines(
color: Theme.of(context).colorScheme.primary,
);
return Padding(
padding: EdgeInsets.only(top: topPadding),
child: placeholder ??
SpinKitSpinningLines(
color: Theme.of(context).colorScheme.primary,
),
);
case AppState.failed:
return Center(child: EmptyConnection(onRetry: onRetry));
default:

View File

@ -141,6 +141,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
equatable:
dependency: transitive
description:
name: equatable
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.3"
expandable_bottom_sheet:
dependency: "direct main"
description:
@ -211,6 +218,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.10"
fl_chart:
dependency: "direct main"
description:
name: fl_chart
url: "https://pub.dartlang.org"
source: hosted
version: "0.50.1"
flutter:
dependency: "direct main"
description: flutter

View File

@ -66,6 +66,7 @@ dependencies:
permission_handler: ^9.2.0
better_player: ^0.0.81
assets_audio_player: ^3.0.4+1
fl_chart: ^0.50.1
dev_dependencies: