From 48ccf550409cae6a1ed7f66a3f1b65eb31af2d4c Mon Sep 17 00:00:00 2001 From: OkaykOrhmn Date: Mon, 13 May 2024 17:08:58 +0330 Subject: [PATCH] 24-02-1403 / Rhmn / changes: Customize category Create. --- lib/constants/assets.dart | 3 + .../favorites_response.dart | 23 ++ lib/models/day_time.dart | 11 + lib/routes/route_generator.dart | 34 +- lib/routes/routes.dart | 3 + lib/services/network/request.dart | 12 +- lib/services/network/request_helper.dart | 3 + lib/utils/date_time.dart | 65 ++++ .../customize_category_state.dart | 121 +++++++ .../customize_category/favorites_step.dart | 128 +++++++ .../notification_status_step.dart | 136 +++++++ .../widgets/customize_category_checkbox.dart | 79 ++++ .../widgets/customize_category_switch.dart | 63 ++++ .../notification_time/notification_time.dart | 123 +++++++ .../notification_time_state.dart | 61 ++++ .../widgets/custom_cupertino_date_picker.dart | 341 ++++++++++++++++++ .../profile/general_settings/settings.dart | 49 ++- .../general_settings/settings_state.dart | 20 + lib/views/widgets/didvan/checkbox.dart | 3 + pubspec.yaml | 2 +- 20 files changed, 1269 insertions(+), 11 deletions(-) create mode 100644 lib/models/customize_categories/favorites_response.dart create mode 100644 lib/models/day_time.dart create mode 100644 lib/views/customize_category/customize_category_state.dart create mode 100644 lib/views/customize_category/favorites_step.dart create mode 100644 lib/views/customize_category/notification_status_step.dart create mode 100644 lib/views/customize_category/widgets/customize_category_checkbox.dart create mode 100644 lib/views/customize_category/widgets/customize_category_switch.dart create mode 100644 lib/views/notification_time/notification_time.dart create mode 100644 lib/views/notification_time/notification_time_state.dart create mode 100644 lib/views/notification_time/widgets/custom_cupertino_date_picker.dart diff --git a/lib/constants/assets.dart b/lib/constants/assets.dart index fea1193..0b7cce4 100644 --- a/lib/constants/assets.dart +++ b/lib/constants/assets.dart @@ -33,6 +33,9 @@ class Assets { static String get stockStatCat => '$_baseFeaturesPath/stock-$_themeSuffix.svg'; + static String get note => + '$_baseFeaturesPath/note.svg'; + static String get verticalLogoWithText => '$_baseLogosPath/logo-vertical-$_themeSuffix.svg'; static String get horizontalLogoWithText => diff --git a/lib/models/customize_categories/favorites_response.dart b/lib/models/customize_categories/favorites_response.dart new file mode 100644 index 0000000..02750df --- /dev/null +++ b/lib/models/customize_categories/favorites_response.dart @@ -0,0 +1,23 @@ + + +class FavoritesResponse { + int? id; + String? name; + bool? selected; + + FavoritesResponse({this.id, this.name, this.selected}); + + FavoritesResponse.fromJson(Map json) { + id = json['id']; + name = json['name']; + selected = json['selected']; + } + + Map toJson() { + final Map data = new Map(); + data['id'] = this.id; + data['name'] = this.name; + data['selected'] = this.selected; + return data; + } +} \ No newline at end of file diff --git a/lib/models/day_time.dart b/lib/models/day_time.dart new file mode 100644 index 0000000..4314fb3 --- /dev/null +++ b/lib/models/day_time.dart @@ -0,0 +1,11 @@ +class DayTime{ + String hour; + String minute; + Meridiem meridiem; + + DayTime({this.hour = "1", this.minute = "0", this.meridiem = Meridiem.AM}); +} + +enum SelectorType { meridiem, hour, minute } + +enum Meridiem { AM, PM } diff --git a/lib/routes/route_generator.dart b/lib/routes/route_generator.dart index 8491061..1cf7211 100644 --- a/lib/routes/route_generator.dart +++ b/lib/routes/route_generator.dart @@ -2,6 +2,8 @@ import 'package:didvan/views/authentication/authentication.dart'; import 'package:didvan/views/authentication/authentication_state.dart'; import 'package:didvan/views/comments/comments.dart'; import 'package:didvan/views/comments/comments_state.dart'; +import 'package:didvan/views/customize_category/customize_category_state.dart'; +import 'package:didvan/views/customize_category/favorites_step.dart'; import 'package:didvan/views/direct/direct.dart'; import 'package:didvan/views/direct/direct_state.dart'; import 'package:didvan/views/hashtag/hashtag.dart'; @@ -20,6 +22,7 @@ import 'package:didvan/views/news/news.dart'; import 'package:didvan/views/news/news_details/news_details.dart'; import 'package:didvan/views/news/news_details/news_details_state.dart'; import 'package:didvan/views/news/news_state.dart'; +import 'package:didvan/views/notification_time/notification_time_state.dart'; import 'package:didvan/views/podcasts/podcasts.dart'; import 'package:didvan/views/profile/profile.dart'; import 'package:didvan/views/radar/radar.dart'; @@ -47,6 +50,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import '../views/customize_category/notification_status_step.dart'; +import '../views/notification_time/notification_time.dart'; + class RouteGenerator { static Route generateRoute(RouteSettings settings) { switch (settings.name) { @@ -54,6 +60,28 @@ class RouteGenerator { return _createRoute( const Splash(), ); + case Routes.notificationTime: + return _createRoute( + ChangeNotifierProvider( + create: (context) => NotificationTimeState(), + child: NotificationTime( + pageData: settings.arguments as Map, + )), + ); + + case Routes.favouritesStep: + return _createRoute( + ChangeNotifierProvider( + create: (context) => CustomizeCategoryState(), + child: FavoritesStep()), + ); + + case Routes.notificationStatusStep: + return _createRoute( + ChangeNotifierProvider( + create: (context) => CustomizeCategoryState(), + child: NotificationStatusStep()), + ); case Routes.authenticaion: return _createRoute( ChangeNotifierProvider( @@ -263,7 +291,8 @@ class RouteGenerator { size: Size( deviceSize.height * 9 / 16, deviceSize.height, - ), textScaler: const TextScaler.linear(1.0), + ), + textScaler: const TextScaler.linear(1.0), ), child: Container( color: Theme.of(context).colorScheme.background, @@ -273,7 +302,8 @@ class RouteGenerator { ); } return MediaQuery( - data: MediaQuery.of(context).copyWith(textScaler: const TextScaler.linear(1.0)), + data: MediaQuery.of(context) + .copyWith(textScaler: const TextScaler.linear(1.0)), child: Container( color: Theme.of(context).colorScheme.surface, child: SafeArea( diff --git a/lib/routes/routes.dart b/lib/routes/routes.dart index 7244d43..6c8db11 100644 --- a/lib/routes/routes.dart +++ b/lib/routes/routes.dart @@ -25,4 +25,7 @@ class Routes { static const String hashtag = '/hashtag'; static const String statGeneral = '/general'; static const String stock = '/stock'; + static const String favouritesStep = '/favourites-step'; + static const String notificationStatusStep = '/notification-status-step'; + static const String notificationTime = '/notification-time'; } diff --git a/lib/services/network/request.dart b/lib/services/network/request.dart index 411ae74..287d6b8 100644 --- a/lib/services/network/request.dart +++ b/lib/services/network/request.dart @@ -1,8 +1,10 @@ import 'dart:convert'; import 'dart:developer'; import 'package:didvan/services/storage/storage.dart'; + // ignore: depend_on_referenced_packages import 'package:http/http.dart' as http; + // ignore: depend_on_referenced_packages import 'package:http_parser/http_parser.dart' as parser; import 'package:permission_handler/permission_handler.dart'; @@ -11,10 +13,12 @@ class RequestService { static late String token; int? statusCode; - dynamic data(String s){ + dynamic data(String s) { return _body?[s] ?? const {}; } + Map get result => _body?['result'] ?? const {}; + Map get errors => _body?['errors'] ?? const {}; String errorMessage = @@ -39,7 +43,11 @@ class RequestService { }) { if (body != null) _requestBody = body; if (requestHeaders != null) _headers.addAll(requestHeaders); - if (useAutherization) _headers.addAll({'Authorization': 'Bearer $token'}); + // if (useAutherization) _headers.addAll({'Authorization': 'Bearer $token'}); + _headers.addAll({ + 'Authorization': + 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NCwicm9sZUlkIjo0LCJhcHBJZCI6MCwiaWF0IjoxNzEzOTM1NzkwfQ.i-SO9tLy0M9j-_C2Wh8tdp01vtYGlDZIBFPygglHQF0' + }); if (body != null) _requestBody = body; } diff --git a/lib/services/network/request_helper.dart b/lib/services/network/request_helper.dart index cb0b9ca..37a4245 100644 --- a/lib/services/network/request_helper.dart +++ b/lib/services/network/request_helper.dart @@ -187,6 +187,9 @@ class RequestHelper { static String mark(int id, String type) => '$baseUrl/$type/$id/mark'; static String tracking(int id, String type) => '$baseUrl/$type/$id/tracking'; static String comments(int id, String type) => '$baseUrl/$type/$id/comments/v2'; + static String favourites() => '$baseUrl/user/favorites'; + static String notificationStatus() => '$baseUrl/user/notification/status'; + static String notificationTime() => '$baseUrl/user/notification/time'; static String usersMentions(String search) => '$baseUrl/comment/user?search=$search'; static String feedback(int id, int commentId, String type) => '$baseUrl/$type/$id/comments/$commentId/feedback/v2'; diff --git a/lib/utils/date_time.dart b/lib/utils/date_time.dart index 0002582..a280907 100644 --- a/lib/utils/date_time.dart +++ b/lib/utils/date_time.dart @@ -1,8 +1,41 @@ import 'package:didvan/config/design_config.dart'; import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; import 'package:persian_datetime_picker/persian_datetime_picker.dart'; +import '../models/day_time.dart'; + class DateTimeUtils { + static DayTime handleDayTime(String dayTime) { + DateFormat format = DateFormat("HH:mm"); + DateTime dateTime = format.parse(dayTime); + int _selectedTimeHour = dateTime.hour; + String _selectedTimeMinute = dateTime.minute.toString(); + Meridiem _selectedMeridiem = Meridiem.AM; + + if (_selectedTimeHour > 12) { + _selectedTimeHour -= 12; + _selectedMeridiem = Meridiem.PM; + } else if (_selectedTimeHour == 0) { + _selectedTimeHour = 24; + _selectedMeridiem = Meridiem.AM; + } + return DayTime( + hour: _selectedTimeHour.toString(), + minute: _selectedTimeMinute, + meridiem: _selectedMeridiem); + } + + static String handleDayTimeReverse(DayTime dayTime) { + String h = dayTime.hour; + String m = dayTime.minute.padLeft(2, "0"); + if (dayTime.meridiem == Meridiem.PM) { + h = (int.parse(h) + 12).toString().padLeft(2, "0"); + } + + return "$h:$m"; + } + static TimeOfDay stringToTimeOfDay(String input) => TimeOfDay( hour: int.parse(input.split(':')[0]), minute: int.parse(input.split(':')[1]), @@ -58,6 +91,38 @@ class DateTimeUtils { return result?.toDateTime().toString(); } + static Future showCupertinoDatePicker({ + String? initialDate, + String? startDate, + String? endDate, + }) async { + final initDate = initialDate == null ? null : DateTime.parse(initialDate); + final initialJalali = Jalali.fromDateTime(initDate ?? DateTime.now()); + + final firstDate = Jalali.fromDateTime( + startDate == null ? DateTime(2021) : DateTime.parse(startDate), + ); + + final lastDate = Jalali.fromDateTime( + endDate == null ? DateTime.now() : DateTime.parse(endDate), + ); + + final Jalali? result = await showPersianDatePicker( + context: DesignConfig.context!, + initialDate: initialJalali, + firstDate: firstDate, + lastDate: lastDate, + builder: (context, child) => Theme( + data: Theme.of(context).copyWith( + textTheme: Theme.of(context).textTheme.copyWith( + headlineMedium: Theme.of(context).textTheme.displaySmall)), + child: child!, + ), + ); + + return result?.toDateTime().toString(); + } + static String timeWithAmPm(String input) { final dateTime = utcToLocalTime(input); bool isAm = true; diff --git a/lib/views/customize_category/customize_category_state.dart b/lib/views/customize_category/customize_category_state.dart new file mode 100644 index 0000000..933bc53 --- /dev/null +++ b/lib/views/customize_category/customize_category_state.dart @@ -0,0 +1,121 @@ +import 'package:didvan/models/customize_categories/favorites_response.dart'; +import 'package:didvan/models/enums.dart'; +import 'package:flutter/cupertino.dart'; + +import '../../providers/core.dart'; +import '../../routes/routes.dart'; +import '../../services/network/request.dart'; +import '../../services/network/request_helper.dart'; + +class CustomizeCategoryState extends CoreProvier { + CustomizeCategoryState() { + appState = AppState.idle; + } + + List selectedFavIds = []; + List selectedNotifIds = []; + final List faves = []; + final List notifs = []; + + Future getFavourites() async { + appState = AppState.busy; + + final service = RequestService( + RequestHelper.favourites(), + ); + await service.httpGet(); + if (service.isSuccess) { + faves.clear(); + final favourites = service.data('types'); + for (var i = 0; i < favourites.length; i++) { + faves.add(FavoritesResponse.fromJson(favourites[i])); + } + selectedFavIds.clear(); + faves.forEach((element) { + if (element.selected!) { + selectedFavIds.add(element.id!); + } + }); + + appState = AppState.idle; + } else { + appState = AppState.failed; + } + + update(); + } + + Future getNotificationStatuses() async { + appState = AppState.busy; + + final service = RequestService( + RequestHelper.notificationStatus(), + ); + await service.httpGet(); + if (service.isSuccess) { + final notificationsStatuses = service.data('types'); + for (var i = 0; i < notificationsStatuses.length; i++) { + notifs.add(FavoritesResponse.fromJson(notificationsStatuses[i])); + } + selectedNotifIds.clear(); + notifs.forEach((element) { + if (element.selected!) { + selectedNotifIds.add(element.id!); + } + }); + appState = AppState.idle; + } else { + appState = AppState.failed; + } + + update(); + } + + Future putFavourites(BuildContext context) async { + // appState = AppState.busy; + final favoritesRequest = {"favorites": selectedFavIds}; + final service = RequestService( + RequestHelper.favourites(), + body: favoritesRequest, + ); + await service.put(); + if (service.isSuccess) { + Navigator.of(context).pushNamed( + Routes.notificationStatusStep, + ); + appState = AppState.idle; + return; + } + + appState = AppState.failed; + } + + Future putNotificationsStatuses(BuildContext context) async { + // appState = AppState.busy; + final notificationsStatusesRequest = {"notification": selectedNotifIds}; + final service = RequestService( + RequestHelper.notificationStatus(), + body: notificationsStatusesRequest, + ); + await service.put(); + if (service.isSuccess) { + Navigator.of(context).pop(); + Navigator.of(context).pop(); + appState = AppState.idle; + return; + } + + appState = AppState.failed; + } + + Future changeSelected(int index, List list,List selectedIds) async { + list[index].selected = !list[index].selected!; + selectedIds.clear(); + for (var ls in list) { + if (ls.selected!) { + selectedIds.add(ls.id!); + } + } + update(); + } +} diff --git a/lib/views/customize_category/favorites_step.dart b/lib/views/customize_category/favorites_step.dart new file mode 100644 index 0000000..049c839 --- /dev/null +++ b/lib/views/customize_category/favorites_step.dart @@ -0,0 +1,128 @@ +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/models/enums.dart'; +import 'package:didvan/models/users_mention.dart'; +import 'package:didvan/models/view/app_bar_data.dart'; +import 'package:didvan/views/customize_category/customize_category_state.dart'; +import 'package:didvan/views/customize_category/widgets/customize_category_checkbox.dart'; +import 'package:didvan/views/widgets/didvan/checkbox.dart'; +import 'package:didvan/views/widgets/didvan/scaffold.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../../config/design_config.dart'; +import '../../constants/assets.dart'; +import '../widgets/didvan/button.dart'; +import '../widgets/shimmer_placeholder.dart'; +import '../widgets/state_handlers/empty_state.dart'; +import '../widgets/state_handlers/sliver_state_handler.dart'; +import '../widgets/user_mention.dart'; + +class FavoritesStep extends StatefulWidget { + const FavoritesStep({super.key}); + + @override + State createState() => _FavoritesStepState(); +} + +class _FavoritesStepState extends State { + late CustomizeCategoryState state; + + @override + void initState() { + super.initState(); + state = context.read(); + + Future.delayed( + Duration.zero, + () => state.getFavourites(), + ); + } + + @override + Widget build(BuildContext context) { + state = context.read(); + + return Material( + child: Stack( + children: [ + DidvanScaffold( + appBarData: AppBarData( + hasBack: true, + title: 'شخصی‌سازی برنامه', + ), + physics: const BouncingScrollPhysics(), + hidePlayer: true, + padding: const EdgeInsets.all(16).copyWith(bottom: 92), + showSliversFirst: false, + slivers: [ + Consumer( + builder: (context, state, child) => + SliverStateHandler( + onRetry: state.getFavourites, + state: state, + itemPadding: const EdgeInsets.only(bottom: 16), + childCount: state.faves.length, + placeholder: ShimmerPlaceholder( + height: 48, + width: MediaQuery.sizeOf(context).width, + borderRadius: DesignConfig.mediumBorderRadius, + ), + centerEmptyState: false, + enableEmptyState: false, + emptyState: EmptyState( + asset: Assets.emptyChat, + title: 'اولین نظر را بنویسید...', + ), + builder: (context, state, index) => Center( + child: Container( + height: 48, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all( + width: 1, + color: state.faves[index].selected! + ? Theme.of(context) + .colorScheme + .focusedBorder + : Theme.of(context).colorScheme.cardBorder), + color: state.faves[index].selected! + ? Theme.of(context).colorScheme.focused + : Theme.of(context).colorScheme.cardBorder), + child: CustomizeCategoryCheckbox( + title: state.faves[index].name!, + value: state.faves[index].selected, + color: Theme.of(context).colorScheme.title, + onChanged: (val) { + state.changeSelected(index, state.faves,state.selectedFavIds); + })), + ), + ), + ), + ], + children: [ + DidvanText( + "کاربر گرامی لطفا دسته‌بندی‌های مورد علاقه خود را انتخاب کنید", + style: Theme.of(context).textTheme.bodyMedium, + ) + ], + ), + Positioned( + bottom: 40, + left: 16, + right: 16, + child: DidvanButton( + onPressed: state.appState == AppState.idle + ? () async { + await state.putFavourites(context); + } + : null, + title: 'تایید', + style: ButtonStyleMode.primary, + )) + ], + ), + ); + } +} diff --git a/lib/views/customize_category/notification_status_step.dart b/lib/views/customize_category/notification_status_step.dart new file mode 100644 index 0000000..0e7ab5b --- /dev/null +++ b/lib/views/customize_category/notification_status_step.dart @@ -0,0 +1,136 @@ +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/models/enums.dart'; +import 'package:didvan/models/users_mention.dart'; +import 'package:didvan/models/view/app_bar_data.dart'; +import 'package:didvan/views/customize_category/customize_category_state.dart'; +import 'package:didvan/views/customize_category/widgets/customize_category_checkbox.dart'; +import 'package:didvan/views/widgets/didvan/checkbox.dart'; +import 'package:didvan/views/widgets/didvan/scaffold.dart'; +import 'package:didvan/views/widgets/didvan/switch.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../../config/design_config.dart'; +import '../../constants/assets.dart'; +import '../widgets/didvan/button.dart'; +import '../widgets/shimmer_placeholder.dart'; +import '../widgets/state_handlers/empty_state.dart'; +import '../widgets/state_handlers/sliver_state_handler.dart'; +import '../widgets/user_mention.dart'; + +class NotificationStatusStep extends StatefulWidget { + const NotificationStatusStep({super.key}); + + @override + State createState() => _NotificationStatusStepState(); +} + +class _NotificationStatusStepState extends State { + late CustomizeCategoryState state; + + @override + void initState() { + super.initState(); + state = context.read(); + + Future.delayed( + Duration.zero, + () => state.getNotificationStatuses(), + ); + } + + @override + Widget build(BuildContext context) { + state = context.watch(); + + return Material( + child: Stack( + children: [ + DidvanScaffold( + appBarData: AppBarData( + hasBack: true, + title: 'شخصی‌سازی برنامه', + ), + physics: const BouncingScrollPhysics(), + hidePlayer: true, + padding: const EdgeInsets.all(16), + showSliversFirst: false, + slivers: [ + Consumer( + builder: (context, state, child) => + SliverStateHandler( + onRetry: state.getNotificationStatuses, + state: state, + itemPadding: const EdgeInsets.only(bottom: 16), + childCount: state.notifs.length, + placeholder: ShimmerPlaceholder( + height: 48, + width: MediaQuery.sizeOf(context).width, + borderRadius: DesignConfig.mediumBorderRadius, + ), + centerEmptyState: false, + enableEmptyState: false, + emptyState: EmptyState( + asset: Assets.emptyChat, + title: 'اولین نظر را بنویسید...', + ), + builder: (context, state, index) => Column( + children: [ + Center( + child: Column( + children: [ + DidvanSwitch( + value: state.notifs[index].selected!, + title: state.notifs[index].name!, + onChanged: (val) { + state.changeSelected(index, state.notifs,state.selectedNotifIds); + }), + index != state.notifs.length - 1 + ? Padding( + padding: const EdgeInsets.only( + bottom: 16, top: 16), + child: Divider( + color: + Theme.of(context).colorScheme.border, + height: 1, + ), + ) + : const SizedBox() + ], + ), + ), + SizedBox( + height: index == state.notifs.length - 1 ? 92 : 0, + ) + ], + ), + ), + ), + ], + children: [ + DidvanText( + "همیشه با اعلانات شخصی‌سازی شده خود بروز بمانید", + style: Theme.of(context).textTheme.bodyMedium, + ) + ], + ), + Positioned( + bottom: 40, + left: 16, + right: 16, + child: DidvanButton( + onPressed: state.appState == AppState.idle + ? () async { + await state.putNotificationsStatuses(context); + } + : null, + title: 'تایید', + style: ButtonStyleMode.primary, + )) + ], + ), + ); + } +} diff --git a/lib/views/customize_category/widgets/customize_category_checkbox.dart b/lib/views/customize_category/widgets/customize_category_checkbox.dart new file mode 100644 index 0000000..07ec708 --- /dev/null +++ b/lib/views/customize_category/widgets/customize_category_checkbox.dart @@ -0,0 +1,79 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/views/widgets/animated_visibility.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class CustomizeCategoryCheckbox extends StatefulWidget { + final String title; + final bool? value; + final double size; + final Color? color; + final void Function(bool value) onChanged; + const CustomizeCategoryCheckbox({ + Key? key, + required this.title, + required this.value, + required this.onChanged, this.size = 15, this.color, + }) : super(key: key); + + @override + State createState() => _CustomizeCategoryCheckboxState(); +} + +class _CustomizeCategoryCheckboxState extends State { + bool _value = false; + + @override + void initState() { + if (widget.value != null) { + _value = widget.value!; + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + Color? _color = widget.color; + _color ??= Theme.of(context).colorScheme.caption; + + return GestureDetector( + onTap: () { + setState(() { + _value = !_value; + }); + widget.onChanged(_value); + }, + child: Container( + color: Colors.transparent, + child: Row( + children: [ + Transform.scale( + scale: widget.size ==15 ? 1 : 0.8, + child: Container( + width: 18, + height: 18, + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(6), + border: Border.all(width: 1.0, color: _value? _color: Theme.of(context).colorScheme.caption), + + ), + margin: EdgeInsets.all(8), + child: AnimatedVisibility( + isVisible: _value, + duration: DesignConfig.mediumAnimationDuration, + child: Center(child: Icon(Icons.check,size: 12,color: _value? _color: Theme.of(context).colorScheme.caption,)) + ), + ), + ), DidvanText(widget.title,fontSize: widget.size,color: _value? _color: Theme.of(context).colorScheme.caption, ), + ], + ), + ), + ); + } +} + + + diff --git a/lib/views/customize_category/widgets/customize_category_switch.dart b/lib/views/customize_category/widgets/customize_category_switch.dart new file mode 100644 index 0000000..f5dc44c --- /dev/null +++ b/lib/views/customize_category/widgets/customize_category_switch.dart @@ -0,0 +1,63 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/views/widgets/animated_visibility.dart'; +import 'package:didvan/views/widgets/didvan/switch.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class CustomizeCategorySwitch extends StatefulWidget { + final String title; + final bool? value; + final double size; + final Color? color; + final void Function(bool value) onChanged; + + const CustomizeCategorySwitch({ + Key? key, + required this.title, + required this.value, + required this.onChanged, + this.size = 15, + this.color, + }) : super(key: key); + + @override + State createState() => + _CustomizeCategorySwitchState(); +} + +class _CustomizeCategorySwitchState extends State { + bool _value = false; + + @override + void initState() { + if (widget.value != null) { + _value = widget.value!; + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + Color? _color = widget.color; + _color ??= Theme.of(context).colorScheme.caption; + + return GestureDetector( + onTap: () { + setState(() { + _value = !_value; + }); + widget.onChanged(_value); + }, + child: Container( + color: Colors.transparent, + child: Transform.scale( + scale: widget.size == 15 ? 1 : 0.8, + child: DidvanSwitch( + value: _value, title: widget.title, onChanged: (val) {}), + ), + ), + ); + } +} diff --git a/lib/views/notification_time/notification_time.dart b/lib/views/notification_time/notification_time.dart new file mode 100644 index 0000000..8ddb499 --- /dev/null +++ b/lib/views/notification_time/notification_time.dart @@ -0,0 +1,123 @@ +import 'package:didvan/config/theme_data.dart'; +import 'package:didvan/models/day_time.dart'; +import 'package:didvan/models/enums.dart'; +import 'package:didvan/models/users_mention.dart'; +import 'package:didvan/models/view/app_bar_data.dart'; +import 'package:didvan/views/customize_category/customize_category_state.dart'; +import 'package:didvan/views/notification_time/notification_time_state.dart'; +import 'package:didvan/views/notification_time/widgets/custom_cupertino_date_picker.dart'; +import 'package:didvan/views/customize_category/widgets/customize_category_checkbox.dart'; +import 'package:didvan/views/widgets/didvan/checkbox.dart'; +import 'package:didvan/views/widgets/didvan/scaffold.dart'; +import 'package:didvan/views/widgets/didvan/switch.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:persian_datetime_picker/persian_datetime_picker.dart'; +import 'package:provider/provider.dart'; + +import '../../constants/assets.dart'; +import '../widgets/date_picker_button.dart'; +import '../widgets/didvan/button.dart'; +import '../widgets/state_handlers/empty_state.dart'; +import '../widgets/state_handlers/sliver_state_handler.dart'; +import '../widgets/user_mention.dart'; + +class NotificationTime extends StatefulWidget { + final Map pageData; + + const NotificationTime({super.key, required this.pageData}); + + @override + State createState() => _NotificationTimeState(); +} + +class _NotificationTimeState extends State { + late NotificationTimeState state; + + @override + void initState() { + super.initState(); + state = context.read(); + state.onTimeChanged = widget.pageData['onTimeChanged']; + Future.delayed( + Duration.zero, + () => state.getTime(), + ); + } + + @override + Widget build(BuildContext context) { + state = context.watch(); + return Material( + child: Stack( + children: [ + DidvanScaffold( + appBarData: AppBarData( + hasBack: true, + title: 'شخصی‌سازی برنامه', + ), + physics: const BouncingScrollPhysics(), + hidePlayer: true, + padding: const EdgeInsets.all(16), + showSliversFirst: false, + backgroundColor: Theme.of(context).colorScheme.background, + slivers: [], + children: [ + Consumer( + builder: (context, state, child) => Column( + children: [ + DidvanText( + "لطفا زمان دریافت اعلانات خود را مشخص کنید", + style: Theme.of(context).textTheme.bodyMedium, + ), + state.appState == AppState.idle + ? Container( + margin: EdgeInsets.all(24), + height: 210, + child: CustomCupertinoDatePicker( + itemExtent: 64, + selectedTime: state.selectedTime, + selectedStyle: Theme.of(context) + .textTheme + .titleMedium! + .copyWith( + color: Theme.of(context) + .colorScheme + .white), + unselectedStyle: + Theme.of(context).textTheme.titleSmall, + disabledStyle: Theme.of(context) + .textTheme + .titleMedium! + .copyWith( + color: Theme.of(context) + .colorScheme + .disabledText), + onSelectedItemChanged: (date) { + state.selectedTime = date; + }), + ) + : SizedBox(), + ], + ), + ) + ], + ), + Positioned( + bottom: 40, + left: 16, + right: 16, + child: DidvanButton( + onPressed: () { + state.putTime(context); + }, + title: 'تایید', + style: ButtonStyleMode.primary, + )) + ], + ), + ); + } +} diff --git a/lib/views/notification_time/notification_time_state.dart b/lib/views/notification_time/notification_time_state.dart new file mode 100644 index 0000000..7ab4a03 --- /dev/null +++ b/lib/views/notification_time/notification_time_state.dart @@ -0,0 +1,61 @@ +import 'package:didvan/models/enums.dart'; +import 'package:didvan/providers/core.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; + +import '../../models/day_time.dart'; +import '../../routes/routes.dart'; +import '../../services/network/request.dart'; +import '../../services/network/request_helper.dart'; +import '../../utils/date_time.dart'; +import '../profile/general_settings/settings_state.dart'; + +class NotificationTimeState extends CoreProvier { + late DayTime selectedTime; + + late void Function( ) onTimeChanged; + + Future getTime() async { + appState = AppState.busy; + + final service = RequestService( + RequestHelper.notificationTime(), + ); + await service.httpGet(); + if (service.isSuccess) { + final time = service.data('time'); + selectedTime = DateTimeUtils.handleDayTime(time); + + appState = AppState.idle; + } else { + appState = AppState.failed; + } + + update(); + } + + Future putTime(BuildContext context) async { + appState = AppState.busy; + + final favoritesRequest = { + "time": DateTimeUtils.handleDayTimeReverse(selectedTime) + }; + final service = RequestService( + RequestHelper.notificationTime(), + body: favoritesRequest, + ); + await service.put(); + if (service.isSuccess) { + onTimeChanged(); + + // Navigator.of(context).pushNamed( + // Routes.notificationStatusStep, + // ); + appState = AppState.idle; + return; + } + + appState = AppState.failed; + } +} diff --git a/lib/views/notification_time/widgets/custom_cupertino_date_picker.dart b/lib/views/notification_time/widgets/custom_cupertino_date_picker.dart new file mode 100644 index 0000000..2977a9b --- /dev/null +++ b/lib/views/notification_time/widgets/custom_cupertino_date_picker.dart @@ -0,0 +1,341 @@ +import 'package:day_night_time_picker/lib/state/time.dart'; +import 'package:didvan/config/theme_data.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; + +import '../../../models/day_time.dart'; +import '../../../utils/date_time.dart'; + +class CustomCupertinoDatePicker extends StatefulWidget { + final double itemExtent; + final void Function(DayTime) + onSelectedItemChanged; // Text style of selected item + final TextStyle? selectedStyle; // Text style of unselected item + final TextStyle? unselectedStyle; // Text style of disabled item + final TextStyle? disabledStyle; // Minimum selectable date + final DayTime? selectedTime; + + const CustomCupertinoDatePicker({ + Key? key, + required this.itemExtent, + required this.onSelectedItemChanged, + this.selectedTime, + this.selectedStyle, + this.unselectedStyle, + this.disabledStyle, + }) : super(key: key); + + @override + State createState() => + _CustomCupertinoDatePickerState(); +} + +class _CustomCupertinoDatePickerState extends State { + late DayTime selectedTime = DateTimeUtils.handleDayTime( + "${DateTime.now().hour.toString()}:${DateTime.now().minute.toString()}"); + + late int _selectedMeridiemIndex; + late int _selectedHourIndex; + late int _selectedMinuteIndex; + + late final FixedExtentScrollController _meridiemScrollController; + late final FixedExtentScrollController _hourScrollController; + late final FixedExtentScrollController _minuteScrollController; + + final _meridiem = [ + 'قبل از ظهر', + 'بعد از ظهر', + ]; + + final _timeH = []; + + final _timeM = []; + + @override + void initState() { + super.initState(); + + + for (int i = 1; i < 12 + 1; i++) { + String twoDigitNumber = i.toString().padLeft(2, '0'); + _timeH.add(twoDigitNumber); + } + for (int i = 1; i < 12 + 1; i++) { + String twoDigitNumber = i.toString().padLeft(2, '0'); + _timeH.add(twoDigitNumber); + } + for (int i = 0; i < 59 + 1; i++) { + String twoDigitNumber = i.toString().padLeft(2, '0'); + _timeM.add(twoDigitNumber); + } + _meridiemScrollController = FixedExtentScrollController(); + _hourScrollController = FixedExtentScrollController(); + _minuteScrollController = FixedExtentScrollController(); + if(widget.selectedTime !=null){ + selectedTime = widget.selectedTime!; + + } + _initDates(); + } + + void _initDates() { + _selectedMeridiemIndex = 0; + + switch(selectedTime.meridiem){ + case Meridiem.AM: + _selectedMeridiemIndex = 0; + _selectedHourIndex = int.parse(selectedTime.hour)-1; + + break; + + case Meridiem.PM: + _selectedMeridiemIndex = 1; + _selectedHourIndex = (int.parse(selectedTime.hour)+12)-1; + + break; + } + _selectedMinuteIndex = int.parse(selectedTime.minute); + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollList(_meridiemScrollController, _selectedMeridiemIndex); + _scrollList(_hourScrollController, _selectedHourIndex); + _scrollList(_minuteScrollController, _selectedMinuteIndex); + }); + } + + void _scrollList(FixedExtentScrollController controller, int index) { + controller.animateToItem( + index, + curve: Curves.easeIn, + duration: const Duration(milliseconds: 300), + ); + } + + @override + void dispose() { + _meridiemScrollController.dispose(); + _hourScrollController.dispose(); + _minuteScrollController.dispose(); + super.dispose(); + } + + void _onSelectedItemChanged(int index, SelectorType type) { + switch (type) { + case SelectorType.meridiem: + + _selectedMeridiemIndex = index; // if month is changed to february & + if (_selectedMeridiemIndex == 0) { + if (_selectedHourIndex > 12 - 1) { + _selectedHourIndex -= 12; + _hourScrollController.jumpToItem(_selectedHourIndex); + } + } else { + if (_selectedHourIndex < 12 - 1) { + _selectedHourIndex += 12; + _hourScrollController.jumpToItem(_selectedHourIndex); + } + } + if(index == 0){ + selectedTime.meridiem = Meridiem.AM; + }else{ + selectedTime.meridiem = Meridiem.PM; + + } + break; + + case SelectorType.hour: + + _selectedHourIndex = index; // if month is changed to february & + if (_selectedHourIndex > 12 - 1) { + _selectedMeridiemIndex = 1; + _meridiemScrollController.jumpToItem(_selectedMeridiemIndex); + } else { + _selectedMeridiemIndex = 0; + _meridiemScrollController.jumpToItem(_selectedMeridiemIndex); + } + selectedTime.hour = _timeH[index]; + + break; + + case SelectorType.minute: + _selectedMinuteIndex = index; + selectedTime.minute = _timeM[index]; + + break; + } + + setState(() {}); + widget.onSelectedItemChanged(selectedTime!); + } + + /// check if the given day, month or year index is disabled + bool _isDisabled(int index, SelectorType type) { + switch (type) { + case SelectorType.meridiem: + + break; + + case SelectorType.hour: + break; + + case SelectorType.minute: + + break; + } + return false; + } + + @override + Widget build(BuildContext context) { + Color _shadow = Theme.of(context).colorScheme.background; + return Stack( + children: [ + Positioned.fill( + child: Center( + child: Container( + height: 64, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular(18)), + child: const Padding( + padding: EdgeInsets.symmetric(vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + SizedBox(), + VerticalDivider( + color: Colors.white, + ), + VerticalDivider( + color: Colors.white, + ), + SizedBox(), + ], + ), + ), + ), + ), + ), + Row( + children: [ + Expanded(child: _meridiemSelector()), + Expanded(child: _minuteSelector()), + Expanded(child: _hourSelector()), + ], + ), + Positioned( + top: 0, + left: 0, + right: 0, + child: Container( + height: 32, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + _shadow.withOpacity(1), + _shadow.withOpacity(0.9), + _shadow.withOpacity(0.8), + _shadow.withOpacity(0.6), + _shadow.withOpacity(0.5), + _shadow.withOpacity(0.4), + ])), + )), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + height: 32, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + _shadow.withOpacity(1), + _shadow.withOpacity(0.9), + _shadow.withOpacity(0.8), + _shadow.withOpacity(0.6), + _shadow.withOpacity(0.5), + _shadow.withOpacity(0.4), + ])), + )), + ], + ); + } + + Widget _selector({ + required List values, + required int selectedValueIndex, + required bool Function(int) isDisabled, + required void Function(int) onSelectedItemChanged, + required FixedExtentScrollController scrollController, + }) { + return CupertinoPicker.builder( + childCount: values.length, + itemExtent: widget.itemExtent, + scrollController: scrollController, + squeeze: 1.45, + useMagnifier: false, + diameterRatio: 2.3, + magnification: 1.0, + offAxisFraction: 0.0, + selectionOverlay: const SizedBox(), + onSelectedItemChanged: onSelectedItemChanged, + itemBuilder: (context, index) => Container( + height: widget.itemExtent, + alignment: Alignment.center, + decoration: BoxDecoration(), + child: Text( + '${values[index]}', + style: index == selectedValueIndex + ? widget.selectedStyle + : isDisabled(index) + ? widget.disabledStyle + : widget.unselectedStyle, + ), + ), + ); + } + + Widget _meridiemSelector() { + return _selector( + values: _meridiem, + selectedValueIndex: _selectedMeridiemIndex, + scrollController: _meridiemScrollController, + isDisabled: (index) => _isDisabled(index, SelectorType.meridiem), + onSelectedItemChanged: (v) => _onSelectedItemChanged( + v, + SelectorType.meridiem, + ), + ); + } + + Widget _hourSelector() { + return _selector( + values: _timeH, + selectedValueIndex: _selectedHourIndex, + scrollController: _hourScrollController, + isDisabled: (index) => _isDisabled(index, SelectorType.hour), + onSelectedItemChanged: (v) => _onSelectedItemChanged( + v, + SelectorType.hour, + ), + ); + } + + Widget _minuteSelector() { + return _selector( + values: _timeM, + selectedValueIndex: _selectedMinuteIndex, + scrollController: _minuteScrollController, + isDisabled: (index) => _isDisabled(index, SelectorType.minute), + onSelectedItemChanged: (v) => _onSelectedItemChanged( + v, + SelectorType.minute, + ), + ); + } +} diff --git a/lib/views/profile/general_settings/settings.dart b/lib/views/profile/general_settings/settings.dart index 3fb52ea..341934b 100644 --- a/lib/views/profile/general_settings/settings.dart +++ b/lib/views/profile/general_settings/settings.dart @@ -23,6 +23,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; +import '../../../routes/routes.dart'; + class GeneralSettings extends StatefulWidget { const GeneralSettings({Key? key}) : super(key: key); @@ -41,6 +43,15 @@ class _GeneralSettingsState extends State { int _intervalStart = 0; int _intervalEnd = 24; + @override + void initState() { + final state = context.read(); + Future.delayed( + Duration.zero, + () => state.getTime(), + ); + } + @override Widget build(BuildContext context) { return Consumer( @@ -54,13 +65,39 @@ class _GeneralSettingsState extends State { DidvanCard( child: MenuOption( title: 'زمان دریافت اعلان', - onTap: () => _pickTimeRange(context), + onTap: () => Navigator.of(context).pushNamed( + Routes.notificationTime, + arguments: { + 'onTimeChanged': () => Future.delayed( + Duration.zero, + () => state.getTime(), + ) + }, + ), icon: DidvanIcons.notification_regular, - suffix: 'از${DateTimeUtils.normalizeTimeDuration( - Duration(minutes: state.notificationTimeRange[0]), - )} تا ${DateTimeUtils.normalizeTimeDuration( - Duration(minutes: state.notificationTimeRange[1]), - )}', + suffix: state.time, + // suffix: 'از${DateTimeUtils.normalizeTimeDuration( + // Duration(minutes: state.notificationTimeRange[0]), + // )} تا ${DateTimeUtils.normalizeTimeDuration( + // Duration(minutes: state.notificationTimeRange[1]), + // )}', + ), + ), + const SizedBox( + height: 16, + ), + DidvanCard( + child: MenuOption( + title: 'شخصی سازی محتوا', + onTap: () => Navigator.of(context).pushNamed( + Routes.favouritesStep, + ), + icon: DidvanIcons.notification_regular, + // suffix: 'از${DateTimeUtils.normalizeTimeDuration( + // Duration(minutes: state.notificationTimeRange[0]), + // )} تا ${DateTimeUtils.normalizeTimeDuration( + // Duration(minutes: state.notificationTimeRange[1]), + // )}', ), ), const ItemTitle( diff --git a/lib/views/profile/general_settings/settings_state.dart b/lib/views/profile/general_settings/settings_state.dart index 8f5249f..3474fc6 100644 --- a/lib/views/profile/general_settings/settings_state.dart +++ b/lib/views/profile/general_settings/settings_state.dart @@ -13,6 +13,26 @@ class GeneralSettingsState extends CoreProvier { String _fontFamily = 'Dana-FA'; double _fontSizeScale = 1; String _brightness = 'light'; + String time = ""; + + Future getTime() async { + appState = AppState.busy; + + final service = RequestService( + RequestHelper.notificationTime(), + ); + await service.httpGet(); + if (service.isSuccess) { + time = service.data('time'); + + appState = AppState.idle; + } else { + appState = AppState.failed; + } + + update(); + } + set notificationTimeRange(List value) { _notificationTimeRange = value; diff --git a/lib/views/widgets/didvan/checkbox.dart b/lib/views/widgets/didvan/checkbox.dart index 9200422..3bbb8d9 100644 --- a/lib/views/widgets/didvan/checkbox.dart +++ b/lib/views/widgets/didvan/checkbox.dart @@ -69,3 +69,6 @@ class _DidvanCheckboxState extends State { ); } } + + + diff --git a/pubspec.yaml b/pubspec.yaml index f11cb7f..15212d6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,7 +53,7 @@ dependencies: universal_html: ^2.0.8 record: ^4.4.3 record_web: ^0.5.0 - persian_datetime_picker: ^2.4.0 + persian_datetime_picker: ^2.6.0 persian_number_utility: ^1.1.1 bot_toast: ^4.0.1 flutter_secure_storage: ^8.0.0