From 6b2509ab890b1a3cff5e08702d66702e4dbab388 Mon Sep 17 00:00:00 2001 From: "Mr.Jebelli" Date: Mon, 10 Nov 2025 16:09:11 +0330 Subject: [PATCH] redesign support and add fake live voice chat --- lib/assets/icons/Didvan.svg | 5 + lib/assets/icons/DidvanCircle.svg | 10 + lib/assets/icons/Seen.svg | 3 + lib/assets/icons/add.svg | 4 + lib/assets/icons/sms-notification.svg | 5 + lib/assets/images/sky/Moon.png | Bin 0 -> 6332 bytes lib/assets/images/sky/Moon.svg | 52 ++ lib/assets/images/sky/sun.png | Bin 0 -> 5242 bytes lib/models/message_data/message_data.dart | 12 + lib/views/direct/direct.dart | 60 +- lib/views/direct/widgets/message.dart | 169 +++- lib/views/direct/widgets/message_box.dart | 184 ++-- .../notification_settings.dart | 234 +++-- .../profile/direct_list/direct_list.dart | 130 ++- .../direct_list/widgets/direct_item.dart | 139 ++- lib/views/widgets/ai_chat_dialog.dart | 377 ++++---- lib/views/widgets/ai_voice_chat_dialog.dart | 815 ++++++++++++++++++ .../widgets/didvan/time_sky_animation.dart | 96 +++ .../widgets/didvan/time_slider_picker.dart | 128 +++ .../widgets/didvan/toggle_button_time.dart | 49 ++ pubspec.yaml | 1 + 21 files changed, 1980 insertions(+), 493 deletions(-) create mode 100644 lib/assets/icons/Didvan.svg create mode 100644 lib/assets/icons/DidvanCircle.svg create mode 100644 lib/assets/icons/Seen.svg create mode 100644 lib/assets/icons/add.svg create mode 100644 lib/assets/icons/sms-notification.svg create mode 100644 lib/assets/images/sky/Moon.png create mode 100644 lib/assets/images/sky/Moon.svg create mode 100644 lib/assets/images/sky/sun.png create mode 100644 lib/views/widgets/ai_voice_chat_dialog.dart create mode 100644 lib/views/widgets/didvan/time_sky_animation.dart create mode 100644 lib/views/widgets/didvan/time_slider_picker.dart create mode 100644 lib/views/widgets/didvan/toggle_button_time.dart diff --git a/lib/assets/icons/Didvan.svg b/lib/assets/icons/Didvan.svg new file mode 100644 index 0000000..bbe6e62 --- /dev/null +++ b/lib/assets/icons/Didvan.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lib/assets/icons/DidvanCircle.svg b/lib/assets/icons/DidvanCircle.svg new file mode 100644 index 0000000..41eb3cb --- /dev/null +++ b/lib/assets/icons/DidvanCircle.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/lib/assets/icons/Seen.svg b/lib/assets/icons/Seen.svg new file mode 100644 index 0000000..82d630a --- /dev/null +++ b/lib/assets/icons/Seen.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/assets/icons/add.svg b/lib/assets/icons/add.svg new file mode 100644 index 0000000..471dda9 --- /dev/null +++ b/lib/assets/icons/add.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/assets/icons/sms-notification.svg b/lib/assets/icons/sms-notification.svg new file mode 100644 index 0000000..b481501 --- /dev/null +++ b/lib/assets/icons/sms-notification.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lib/assets/images/sky/Moon.png b/lib/assets/images/sky/Moon.png new file mode 100644 index 0000000000000000000000000000000000000000..2a46e7e54f0c51c59c71280b0ee56e5068a18c28 GIT binary patch literal 6332 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/assets/images/sky/sun.png b/lib/assets/images/sky/sun.png new file mode 100644 index 0000000000000000000000000000000000000000..cf951e34495ee5c90c7d57407d405c0b7d112cef GIT binary patch literal 5242 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 json) => MessageData( @@ -39,6 +43,8 @@ class MessageData { readed: json['readed'], createdAt: json['createdAt'], duration: json['duration'], + writerName: json['writer']?['name'] ?? json['writerName'], + writerPhoto: json['writer']?['photo'] ?? json['writerPhoto'], audioDuration: json['waveform'] == null ? null : jsonDecode(json['waveform'])['duration'] ?? 0, @@ -57,6 +63,8 @@ class MessageData { 'readed': readed, 'createdAt': createdAt, 'duration': duration, + 'writerName': writerName, + 'writerPhoto': writerPhoto, 'news': news?.toJson(), 'radar': radar?.toJson(), }; @@ -73,6 +81,8 @@ class MessageData { File? audioFile, int? audioDuration, int? duration, + String? writerName, + String? writerPhoto, }) { return MessageData( id: id ?? this.id, @@ -86,6 +96,8 @@ class MessageData { audioFile: audioFile ?? this.audioFile, audioDuration: audioDuration ?? this.audioDuration, duration: duration ?? this.duration, + writerName: writerName ?? this.writerName, + writerPhoto: writerPhoto ?? this.writerPhoto, ); } } diff --git a/lib/views/direct/direct.dart b/lib/views/direct/direct.dart index 804855a..da5fea5 100644 --- a/lib/views/direct/direct.dart +++ b/lib/views/direct/direct.dart @@ -3,7 +3,6 @@ import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/constants/assets.dart'; import 'package:didvan/models/enums.dart'; -import 'package:didvan/models/view/app_bar_data.dart'; import 'package:didvan/providers/server_data.dart'; import 'package:didvan/services/media/voice.dart'; import 'package:didvan/views/direct/direct_state.dart'; @@ -12,10 +11,12 @@ import 'package:didvan/views/direct/widgets/message_box.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/logos/didvan_vertical_logo.dart'; import 'package:didvan/views/widgets/state_handlers/empty_state.dart'; import 'package:didvan/views/widgets/state_handlers/sliver_state_handler.dart'; import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:provider/provider.dart'; class Direct extends StatefulWidget { @@ -61,7 +62,7 @@ class _DirectState extends State { child: Stack( children: [ Positioned( - top: 0, + top: 60 + d.padding.top + 56, bottom: 56, left: 0, right: 0, @@ -70,13 +71,7 @@ class _DirectState extends State { padding: EdgeInsets.zero, reverse: true, backgroundColor: Theme.of(context).colorScheme.surface, - appBarData: AppBarData( - hasBack: true, - subtitle: widget.pageData['type'].contains('پشتیبانی') - ? null - : 'ارتباط با سردبیر', - title: widget.pageData['type'] ?? 'پشتیبانی اپلیکیشن', - ), + appBarData: null, slivers: [ if (state.appState != AppState.busy) SliverPadding( @@ -116,6 +111,53 @@ class _DirectState extends State { ], ), ), + Positioned( + top: 0, + right: 20, + child: Container( + height: 60 + d.padding.top, + padding: EdgeInsets.only(top: d.padding.top), + child: const DidvanHorizontalLogo(), + ), + ), + Positioned( + top: 60 + d.padding.top, + left: 0, + right: 0, + child: Container( + height: 56, + color: Theme.of(context).colorScheme.surface, + child: Row( + children: [ + const SizedBox(width: 16), + Expanded( + child: Text( + widget.pageData['type'] ?? 'پیام به پشتیبانی', + style: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith( + color: const Color.fromARGB(255, 0, 53, 70), + fontWeight: FontWeight.bold, + fontSize: 19), + ), + ), + IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: SvgPicture.asset( + 'lib/assets/icons/arrow-left.svg', + width: 30, + height: 30, + colorFilter: const ColorFilter.mode( + Color.fromARGB(255, 102, 102, 102), + BlendMode.srcIn), + ), + ), + const SizedBox(width: 8), + ], + ), + ), + ), Positioned( bottom: d.viewInsets.bottom, right: 0, diff --git a/lib/views/direct/widgets/message.dart b/lib/views/direct/widgets/message.dart index 5b6e20c..7afb117 100644 --- a/lib/views/direct/widgets/message.dart +++ b/lib/views/direct/widgets/message.dart @@ -1,6 +1,5 @@ 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/message_data/message_data.dart'; import 'package:didvan/utils/date_time.dart'; import 'package:didvan/views/ai/widgets/audio_wave.dart'; @@ -9,6 +8,7 @@ import 'package:didvan/views/widgets/didvan/divider.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/skeleton_image.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:provider/provider.dart'; import 'package:persian_number_utility/persian_number_utility.dart'; @@ -57,35 +57,39 @@ class Message extends StatelessWidget { child: Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.splash, - borderRadius: DesignConfig.lowBorderRadius, + decoration: const BoxDecoration( + color: Color.fromARGB(255, 200, 224, 244), + borderRadius: DesignConfig.mediumBorderRadius, ), - child: DidvanText( - DateTime.parse(message.createdAt).toPersianDateStr(), - style: Theme.of(context).textTheme.labelSmall, - color: DesignConfig.isDark - ? Theme.of(context).colorScheme.white - : Theme.of(context).colorScheme.black, + child: Padding( + padding: const EdgeInsets.all(3.0), + child: DidvanText( + DateTime.parse(message.createdAt).toPersianDateStr(), + style: Theme.of(context).textTheme.labelMedium, + color: DesignConfig.isDark + ? Theme.of(context).colorScheme.white + : Theme.of(context).colorScheme.black, + ), ), ), ), Padding( - padding: EdgeInsets.only( - right: message.writedByAdmin ? 20 : 0, - left: !message.writedByAdmin ? 20 : 0, - ), + padding: const EdgeInsets.all(0), child: Column( crossAxisAlignment: message.writedByAdmin - ? CrossAxisAlignment.start - : CrossAxisAlignment.end, + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, children: [ _MessageContainer( writedByAdmin: message.writedByAdmin, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (message.text != null) DidvanText(message.text!), + if (message.text != null) + DidvanText( + message.text!, + fontWeight: FontWeight.bold, + ), if (message.audio != null) AudioWave( file: message.audio!, @@ -109,18 +113,104 @@ class Message extends StatelessWidget { const SizedBox(height: 4), Row( mainAxisSize: MainAxisSize.min, + mainAxisAlignment: message.writedByAdmin + ? MainAxisAlignment.end + : MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ + if (message.writedByAdmin) + Padding( + padding: const EdgeInsets.only(left: 8), + child: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: const Color.fromARGB( + 255, 184, 184, 184))), + child: Center( + child: SvgPicture.asset( + 'lib/assets/icons/Didvan.svg', + width: 12, + height: 12, + ), + ), + )) + else + Padding( + padding: const EdgeInsets.only(left: 8), + child: message.writerPhoto != null && + message.writerPhoto!.isNotEmpty + ? Container( + width: 20, + height: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: const Color.fromARGB( + 255, 184, 184, 184), + width: 1, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Image.network( + message.writerPhoto!, + fit: BoxFit.cover, + errorBuilder: + (context, error, stackTrace) { + return Container( + width: 20, + height: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: const Color.fromARGB( + 255, 184, 184, 184), + width: 1, + ), + ), + child: const Icon( + Icons.person_rounded, + size: 12, + color: Colors.black, + ), + ); + }, + ), + ), + ) + : Container( + width: 20, + height: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: const Color.fromARGB( + 255, 184, 184, 184), + width: 1, + ), + ), + child: const Icon( + Icons.person_rounded, + size: 12, + color: Colors.black, + ), + ), + ), DidvanText( DateTimeUtils.timeWithAmPm(message.createdAt), style: Theme.of(context).textTheme.labelSmall, color: Theme.of(context).colorScheme.caption, ), + const SizedBox(width: 4), if (!message.writedByAdmin) - Icon( + SvgPicture.asset( message.readed - ? DidvanIcons.check_double_light - : DidvanIcons.check_light, - size: 16, + ? 'lib/assets/icons/Seen.svg' + : 'DidvanIcons.check_light', + height: 10, ) ], ), @@ -162,20 +252,27 @@ class _ReplyRadarOverview extends StatelessWidget { style: Theme.of(context).textTheme.bodyLarge, maxLines: 1, overflow: TextOverflow.ellipsis, - color: Theme.of(context).colorScheme.focusedBorder, + color: const Color.fromARGB(255, 0, 126, 167), ), Row( children: [ - DidvanText( - 'رادار ${message.radar!.categories.first.label}', - style: Theme.of(context).textTheme.labelSmall, - color: Theme.of(context).colorScheme.focusedBorder, + Expanded( + child: DidvanText( + 'رادار ${message.radar!.categories.first.label}', + style: Theme.of(context) + .textTheme + .labelSmall + ?.copyWith(fontWeight: FontWeight.bold), + color: const Color.fromARGB(255, 0, 126, 167), + ), ), - const Spacer(), DidvanText( - '${DateTimeUtils.momentGenerator(message.radar!.createdAt)} | خواندن در ${message.radar!.timeToRead} دقیقه', - color: Theme.of(context).colorScheme.focusedBorder, - style: Theme.of(context).textTheme.labelSmall, + '${DateTimeUtils.momentGenerator(message.radar!.createdAt)} / خواندن در ${message.radar!.timeToRead} دقیقه', + color: const Color.fromARGB(255, 0, 126, 167), + style: Theme.of(context) + .textTheme + .labelSmall + ?.copyWith(fontWeight: FontWeight.bold), ), ], ), @@ -255,18 +352,20 @@ class _MessageContainer extends StatelessWidget { return Container( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), decoration: BoxDecoration( - borderRadius: DesignConfig.mediumBorderRadius.copyWith( + borderRadius: DesignConfig.highBorderRadius.copyWith( bottomLeft: writedByAdmin ? Radius.zero : null, bottomRight: !writedByAdmin ? Radius.zero : null, ), + border: Border.all( + color: writedByAdmin + ? const Color.fromARGB(255, 184, 184, 184) + : Colors.transparent, + width: 0.5, + ), color: (writedByAdmin ? Theme.of(context).colorScheme.surface : Theme.of(context).colorScheme.focused) .withValues(alpha: 0.9), - border: Border.all( - color: Theme.of(context).colorScheme.border, - width: 0.5, - ), ), child: child, ); diff --git a/lib/views/direct/widgets/message_box.dart b/lib/views/direct/widgets/message_box.dart index 8d50579..738bdbe 100644 --- a/lib/views/direct/widgets/message_box.dart +++ b/lib/views/direct/widgets/message_box.dart @@ -1,4 +1,3 @@ - import 'package:didvan/config/design_config.dart'; import 'package:didvan/config/theme_data.dart'; import 'package:didvan/constants/app_icons.dart'; @@ -23,47 +22,46 @@ class _MessageBoxState extends State { return Column( children: [ Consumer( - builder: (context, state, child) => - state.replyRadar != null || state.replyNews != null - ? _MessageBoxContainer( - isMessage: false, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const DidvanText( - 'لینک به مطلب:', - ), - DidvanText( - state.replyRadar != null - ? state.replyRadar!.title - : state.replyNews!.title, - overflow: TextOverflow.ellipsis, - maxLines: 1, - color: - Theme.of(context).colorScheme.primary, - ), - ], + builder: (context, state, child) => state.replyRadar != null || + state.replyNews != null + ? _MessageBoxContainer( + isMessage: false, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const DidvanText( + 'لینک به مطلب:', ), - ), - DidvanIconButton( - icon: DidvanIcons.close_regular, - gestureSize: 24, - onPressed: () { - state.replyRadar = null; - state.replyNews = null; - state.update(); - }, - ), - ], + DidvanText( + state.replyRadar != null + ? state.replyRadar!.title + : state.replyNews!.title, + overflow: TextOverflow.ellipsis, + maxLines: 1, + color: Theme.of(context).colorScheme.primary, + ), + ], + ), ), - ), - ) - : const SizedBox(), + DidvanIconButton( + icon: DidvanIcons.close_regular, + gestureSize: 24, + onPressed: () { + state.replyRadar = null; + state.replyNews = null; + state.update(); + }, + ), + ], + ), + ), + ) + : const SizedBox(), ), _MessageBoxContainer( isMessage: true, @@ -96,15 +94,6 @@ class _MessageBoxContainer extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - height: isMessage ? 68 : null, - decoration: BoxDecoration( - border: Border( - top: BorderSide( - color: Theme.of(context).colorScheme.cardBorder, - ), - ), - color: Theme.of(context).colorScheme.surface, - ), child: child, ); } @@ -140,46 +129,67 @@ class _TypingState extends State<_Typing> { return Row( children: [ Expanded( - flex: 2, - child: AnimatedSwitcher( - duration: DesignConfig.lowAnimationDuration, - transitionBuilder: (child, animation) => ScaleTransition( - scale: animation, - child: child, - ), - child: state.textController.text.isNotEmpty - ? DidvanIconButton( - icon: DidvanIcons.send_solid, - onPressed: () async { - await state.sendMessage(); - }, - size: 32, - color: Theme.of(context).colorScheme.focusedBorder, - ) - : DidvanIconButton( - icon: DidvanIcons.mic_solid, - onPressed: state.startRecording, - size: 32, - color: Theme.of(context).colorScheme.focusedBorder, + child: Padding( + padding: const EdgeInsets.all(20.0), + child: TextFormField( + controller: state.textController, + textInputAction: TextInputAction.send, + style: Theme.of(context).textTheme.bodyMedium, + decoration: InputDecoration( + filled: true, + fillColor: const Color.fromARGB(255, 234, 235, 235), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(32.0), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(32.0), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(32.0), + borderSide: BorderSide.none, + ), + hintText: 'پیام خود را بنویسید...', + hintStyle: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: const Color.fromARGB(255, 102, 102, 102)), + contentPadding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + prefixIcon: Padding( + padding: const EdgeInsets.only(left: 5.0, right: 5.0), + child: AnimatedSwitcher( + duration: DesignConfig.lowAnimationDuration, + transitionBuilder: (child, animation) => ScaleTransition( + scale: animation, + child: child, + ), + child: state.textController.text.isNotEmpty + ? DidvanIconButton( + key: const ValueKey('sendBtn'), + isSvg: true, + icon: 'lib/assets/icons/send.svg', + onPressed: () async { + await state.sendMessage(); + }, + size: 28, + color: Theme.of(context).colorScheme.primary, + ) + : DidvanIconButton( + key: const ValueKey('micBtn'), + isSvg: true, + icon: 'lib/assets/icons/microphone-2.svg', + onPressed: state.startRecording, + size: 28, + color: Theme.of(context).colorScheme.primary, + ), ), - ), - ), - Expanded( - flex: 15, - child: TextFormField( - controller: state.textController, - textInputAction: TextInputAction.send, - style: Theme.of(context).textTheme.bodyMedium, - decoration: InputDecoration( - border: InputBorder.none, - hintText: 'بنویسید یا پیام صوتی بگذارید...', - hintStyle: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(color: Theme.of(context).colorScheme.disabledText), + ), + ), ), ), - ), + ) ], ); } @@ -256,4 +266,4 @@ class _RecordChecking extends StatelessWidget { ], ); } -} \ No newline at end of file +} diff --git a/lib/views/notification_settings/notification_settings.dart b/lib/views/notification_settings/notification_settings.dart index 7d1abe5..94a40f6 100644 --- a/lib/views/notification_settings/notification_settings.dart +++ b/lib/views/notification_settings/notification_settings.dart @@ -1,20 +1,17 @@ import 'package:didvan/config/design_config.dart'; -import 'package:didvan/config/theme_data.dart'; -import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/constants/assets.dart'; +import 'package:didvan/models/day_time.dart'; import 'package:didvan/models/enums.dart'; import 'package:didvan/routes/routes.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/notification_time/notification_time_state.dart'; -import 'package:didvan/views/notification_time/widgets/custom_cupertino_date_picker.dart'; import 'package:didvan/views/widgets/didvan/button.dart'; import 'package:didvan/views/widgets/didvan/divider.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:didvan/views/widgets/item_title.dart'; +import 'package:didvan/views/widgets/didvan/time_sky_animation.dart'; +import 'package:didvan/views/widgets/didvan/time_slider_picker.dart'; import 'package:didvan/views/widgets/shimmer_placeholder.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:provider/provider.dart'; @@ -177,75 +174,98 @@ class _NotificationSettingsState extends State { ? Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ItemTitle( - title: 'زمان دریافت اعلان‌ها', - icon: DidvanIcons.notification_regular, - style: Theme.of(context).textTheme.titleMedium, - ), - const SizedBox(height: 12), - DidvanText( - "لطفا زمان دریافت اعلانات خود را مشخص کنید", - style: Theme.of(context).textTheme.bodyMedium, - ), - Container( - margin: const EdgeInsets.all(24), - height: 210, - child: CustomCupertinoDatePicker( - disable: state.isAnytime, - itemExtent: 64, - selectedTime: state.selectedTime, - selectedStyle: Theme.of(context) - .textTheme - .titleMedium! - .copyWith( - color: state.isAnytime - ? const Color(0xFFC8E0F4) - : 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; - }, + const Padding( + padding: EdgeInsets.all(18.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 10), + Text( + 'زمان‌بندی دریافت اعلان‌ها', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.black), + ), + SizedBox(height: 10), + Text( + 'در این بخش می‌توانید تعیین کنید اعلان‌های دسته‌بندی‌هایی که انتخاب کرده‌اید، در چه بازه‌های زمانی برای شما ارسال شوند..', + style: TextStyle( + fontSize: 14, + color: Color.fromARGB(255, 128, 128, 128), + )), + SizedBox( + height: 15, + ) + ], ), ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - border: Border.all( - color: Theme.of(context).colorScheme.border, - width: 1), - borderRadius: BorderRadius.circular(18)), - child: DidvanSwitch( - value: state.isAnytime, - title: "دریافت آنی اعلانات", - onChanged: (val) { - state.isAnytime = val; - state.update(); - }, - ), - ), - ), - const SizedBox(height: 16), - DidvanButton( - onPressed: () { - context - .read() - .putFavourites(context); - context - .read() - .putTime(context); + Builder( + builder: (context) { + int hour24 = + int.tryParse(state.selectedTime.hour) ?? 12; + if (state.selectedTime.meridiem == Meridiem.PM && + hour24 != 12) { + hour24 += 12; + } + if (state.selectedTime.meridiem == Meridiem.AM && + hour24 == 12) { + hour24 = 0; + } + return Padding( + padding: + const EdgeInsets.symmetric(horizontal: 24), + child: TimeSkyAnimation( + hour: hour24, + ), + ); }, - title: 'ذخیره تغییرات', + ), + TimeSliderPicker( + selectedTime: state.selectedTime, + isDisabled: state.isAnytime, + onTimeChanged: (newTime) { + state.selectedTime = newTime; + state + .update(); // به‌روزرسانی state برای اعمال تغییرات در انیمیشن و UI + }, + ), + // Padding( + // padding: const EdgeInsets.symmetric(horizontal: 16), + // child: Container( + // padding: const EdgeInsets.all(16), + // decoration: BoxDecoration( + // color: Theme.of(context).colorScheme.surface, + // border: Border.all( + // color: Theme.of(context).colorScheme.border, + // width: 1), + // borderRadius: BorderRadius.circular(18)), + // child: DidvanSwitch( + // value: state.isAnytime, + // title: "دریافت آنی اعلانات", + // onChanged: (val) { + // state.isAnytime = val; + // state.update(); + // }, + // ), + // ), + // ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.all(15.0), + child: DidvanButton( + onPressed: () { + context + .read() + .putFavourites(context); + context + .read() + .putTime(context); + }, + title: 'ذخیره تغییرات', + imagepath: 'lib/assets/icons/verify.svg', + ), ), ], ) @@ -280,6 +300,7 @@ class _CategoryExpansionGroupState extends State<_CategoryExpansionGroup> { @override Widget build(BuildContext context) { const expansionColor = Color.fromARGB(255, 230, 243, 250); + const grayTextColor = Color.fromARGB(255, 102, 102, 102); return Padding( padding: const EdgeInsets.all(16.0), @@ -336,26 +357,59 @@ class _CategoryExpansionGroupState extends State<_CategoryExpansionGroup> { return Padding( padding: const EdgeInsets.only(bottom: 12), child: Container( - height: 48, + height: 70, + padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), - color: item.selected! - ? Theme.of(context).colorScheme.focused - : Theme.of(context).colorScheme.surface, - border: Border.all( - width: 1, - color: item.selected! - ? Theme.of(context).colorScheme.focusedBorder - : Theme.of(context).colorScheme.cardBorder, - ), + color: Theme.of(context).colorScheme.surface, ), - child: CustomizeCategoryCheckbox( - title: item.name!, - value: item.selected, - onChanged: (value) { - item.selected = value; - widget.state.update(); - }, + child: Column( + children: [ + Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + item.name!, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: grayTextColor, + fontWeight: FontWeight.w500, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + Directionality( + textDirection: TextDirection.ltr, + child: CupertinoSwitch( + value: item.selected ?? false, + activeColor: + Theme.of(context).colorScheme.primary, + onChanged: (value) { + setState(() { + item.selected = value; + }); + widget.state.update(); + }, + ), + ), + ], + ), + ), + const SizedBox( + height: 15, + ), + const DidvanDivider( + verticalPadding: 5, + ) + ], ), ), ); diff --git a/lib/views/profile/direct_list/direct_list.dart b/lib/views/profile/direct_list/direct_list.dart index c8245e4..e0ff464 100644 --- a/lib/views/profile/direct_list/direct_list.dart +++ b/lib/views/profile/direct_list/direct_list.dart @@ -1,15 +1,14 @@ import 'package:didvan/constants/assets.dart'; -import 'package:didvan/models/view/app_bar_data.dart'; import 'package:didvan/routes/routes.dart'; import 'package:didvan/views/profile/direct_list/direct_list_state.dart'; import 'package:didvan/views/profile/direct_list/widgets/direct_item.dart'; -import 'package:didvan/views/widgets/didvan/badge.dart'; import 'package:didvan/views/widgets/didvan/divider.dart'; import 'package:didvan/views/widgets/didvan/scaffold.dart'; import 'package:didvan/views/widgets/shimmer_placeholder.dart'; import 'package:didvan/views/widgets/state_handlers/empty_state.dart'; import 'package:didvan/views/widgets/state_handlers/sliver_state_handler.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:provider/provider.dart'; class DirectList extends StatefulWidget { @@ -33,48 +32,97 @@ class _DirectListState extends State { return Consumer( builder: (context, state, child) => DidvanScaffold( padding: const EdgeInsets.symmetric(vertical: 16), - appBarData: AppBarData( - hasBack: true, - title: 'پیام‌ها', - // تغییرات در این بخش انجام شد - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - TextButton.icon( - onPressed: () { - Navigator.of(context).pushNamed( - Routes.direct, - arguments: {'type': 'پشتیبانی اپلیکیشن'}, - ).then((value) { - if (mounted) { - context.read().getDirectsList(); - } - }); - }, - icon: Icon( - Icons.add_circle_outline_rounded, - size: 20, - color: Theme.of(context).colorScheme.primary, - ), - label: Text( - 'تیکت جدید', - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.bold, - ), - ), + appBarData: null, + showSliversFirst: true, + slivers: [ + SliverAppBar( + pinned: true, + backgroundColor: Theme.of(context).colorScheme.surface, + automaticallyImplyLeading: false, + leadingWidth: 200, + leading: Padding( + padding: const EdgeInsetsDirectional.only(start: 0.0), + child: SvgPicture.asset( + Assets.horizontalLogoWithText, + fit: BoxFit.contain, + height: 80, ), - if (state.unreadCount > 0) - Padding( - padding: const EdgeInsetsDirectional.only(start: 8), - child: DidvanBadge( - text: state.unreadCount.toString(), - ), - ), + ), + actions: [ + IconButton( + onPressed: () { + Navigator.of(context).pushNamed(Routes.bookmarks); + }, + icon: SvgPicture.asset( + 'lib/assets/icons/hugeicons_telescope-01.svg')), + IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: SvgPicture.asset( + 'lib/assets/icons/arrow-left.svg', + color: const Color.fromARGB(255, 102, 102, 102), + )), + const SizedBox(width: 8), ], ), - ), - slivers: [ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + TextButton.icon( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + const Color.fromARGB(255, 0, 126, 167), + ), + shape: + MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + ), + onPressed: () { + Navigator.of(context).pushNamed( + Routes.direct, + arguments: {'type': 'پشتیبانی اپلیکیشن'}, + ).then((value) { + if (mounted) { + context.read().getDirectsList(); + } + }); + }, + icon: SvgPicture.asset( + 'lib/assets/icons/add.svg', + height: 20, + color: Theme.of(context).colorScheme.surface, + ), + label: Text( + 'تیکت جدید', + style: TextStyle( + color: Theme.of(context).colorScheme.surface, + fontWeight: FontWeight.bold, + ), + ), + ), + // if (state.unreadCount > 0) + // Padding( + // padding: const EdgeInsetsDirectional.only(start: 8), + // child: DidvanBadge( + // text: state.unreadCount.toString(), + // ), + // ), + ], + ), + ], + ), + ), + ), SliverStateHandler( onRetry: state.getDirectsList, itemPadding: const EdgeInsets.symmetric(horizontal: 16), diff --git a/lib/views/profile/direct_list/widgets/direct_item.dart b/lib/views/profile/direct_list/widgets/direct_item.dart index 5665763..d3f8dfb 100644 --- a/lib/views/profile/direct_list/widgets/direct_item.dart +++ b/lib/views/profile/direct_list/widgets/direct_item.dart @@ -3,12 +3,12 @@ import 'package:didvan/constants/app_icons.dart'; import 'package:didvan/models/chat_room/chat_room.dart'; import 'package:didvan/providers/user.dart'; import 'package:didvan/routes/routes.dart'; -import 'package:didvan/utils/date_time.dart'; import 'package:didvan/views/profile/direct_list/direct_list_state.dart'; -import 'package:didvan/views/widgets/didvan/badge.dart'; import 'package:didvan/views/widgets/didvan/divider.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:persian_number_utility/persian_number_utility.dart'; import 'package:provider/provider.dart'; class ChatRoomItem extends StatelessWidget { @@ -42,58 +42,117 @@ class ChatRoomItem extends StatelessWidget { children: [ Row( children: [ - const Icon( - DidvanIcons.avatar_light, - size: 32, - ), - const SizedBox(width: 12), + SvgPicture.asset( + chatRoom.unread != 0 + ? 'lib/assets/icons/sms-notification.svg' + : 'lib/assets/icons/sms.svg', + height: 24, + width: 24, + color: chatRoom.unread != 0 + ? null + : const Color.fromARGB(255, 0, 126, 167)), + const SizedBox(width: 6), Expanded( child: DidvanText( chatRoom.type, - style: Theme.of(context).textTheme.bodyLarge, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: const Color.fromARGB(255, 102, 102, 102)), ), ), - if (chatRoom.unread != 0) - DidvanBadge(text: chatRoom.unread.toString()), + // if (chatRoom.unread != 0) + // DidvanBadge(text: chatRoom.unread.toString()), ], ), - Row( + const SizedBox( + height: 10, + ), + Column( + mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(width: 40), - if (!chatRoom.lastMessage.writedByAdmin) - Icon( - chatRoom.lastMessage.readed - ? DidvanIcons.check_double_light - : DidvanIcons.check_light, - size: 16, - ), - const SizedBox(width: 4), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(width: 2), + Container( + decoration: BoxDecoration( + color: chatRoom.lastMessage.readed + ? chatRoom.lastMessage.writedByAdmin + ? const Color.fromARGB(255, 230, 243, 250) + : const Color.fromARGB(255, 235, 235, 235) + : const Color.fromARGB(255, 255, 200, 215), + borderRadius: BorderRadius.circular(8)), + child: Padding( + padding: const EdgeInsets.fromLTRB(10, 8, 10, 8), + child: Text( + chatRoom.lastMessage.readed + ? chatRoom.lastMessage.writedByAdmin + ? 'پاسخ داده شده' + : 'در انتظار پاسخ' + : 'خوانده نشده', + ), + ), + ), + // Icon( + // chatRoom.lastMessage.readed + // ? DidvanIcons.check_double_light + // : DidvanIcons.check_light, + // size: 16, + // ), + const SizedBox(width: 12), + Expanded( + child: Row( children: [ - if (chatRoom.lastMessage.text == null) - const Icon( - DidvanIcons.mic_light, - size: 18, - ), - Expanded( - child: DidvanText( - chatRoom.lastMessage.text ?? 'پیام صوتی', - maxLines: 1, - overflow: TextOverflow.ellipsis, + Center( + child: Column( + children: [ + const SizedBox( + height: 7, + ), + DidvanText( + DateTime.parse(chatRoom.updatedAt) + .toPersianDateStr(), + style: Theme.of(context).textTheme.bodySmall, + color: Theme.of(context).colorScheme.caption, + ), + ], ), ), ], ), - DidvanText( - DateTimeUtils.momentGenerator(chatRoom.updatedAt), - style: Theme.of(context).textTheme.bodySmall, - color: Theme.of(context).colorScheme.caption, - ) + ), + ], + ), + const SizedBox( + height: 5, + ), + if (chatRoom.lastMessage.text == null) + const Icon( + DidvanIcons.mic_light, + size: 18, + ), + Padding( + padding: const EdgeInsets.only(right: 8), + child: Row( + children: [ + Text( + chatRoom.lastMessage.writedByAdmin + ? 'پیام پشتیبانی: ' + : 'پیام شما: ', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.bold, + color: const Color.fromARGB(255, 0, 126, 167), + ), + ), + Expanded( + child: DidvanText( + chatRoom.lastMessage.text ?? 'پیام صوتی', + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + color: const Color.fromARGB(255, 102, 102, 102), + ), + ), ], ), ), diff --git a/lib/views/widgets/ai_chat_dialog.dart b/lib/views/widgets/ai_chat_dialog.dart index acb5960..3e602f2 100644 --- a/lib/views/widgets/ai_chat_dialog.dart +++ b/lib/views/widgets/ai_chat_dialog.dart @@ -4,6 +4,7 @@ import 'package:didvan/services/ai_rag_service.dart'; import 'package:didvan/services/ai_voice_service.dart'; import 'package:didvan/services/network/request.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:didvan/views/widgets/ai_voice_chat_dialog.dart'; import 'package:didvan/providers/user.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -42,7 +43,7 @@ class _AiChatDialogState extends State super.initState(); _animationController = AnimationController( vsync: this, - duration: const Duration(milliseconds: 400), + duration: const Duration(milliseconds: 600), ); _audioPlayer.playerStateStream.listen((state) { @@ -338,7 +339,7 @@ class _AiChatDialogState extends State child: _buildMessageList(), ), if (_isLoading) _buildLoadingIndicator(), - if (_isRecording) _buildRecordingIndicator(), + // if (_isRecording) _buildRecordingIndicator(), _buildInputField(), ], ), @@ -417,51 +418,45 @@ class _AiChatDialogState extends State ), ), ), - Positioned( - bottom: 1, - left: 1, - child: Container( - width: 11, - height: 11, - decoration: BoxDecoration( - color: const Color(0xFF00FF88), - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 1.5), - boxShadow: [ - BoxShadow( - color: const Color(0xFF00FF88).withOpacity(0.4), - blurRadius: 6, - spreadRadius: 0.5, - ), - ], - ), - ), - ), ], ), const SizedBox(width: 12), const Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - DidvanText( - 'دستیار هوشمند دیدوان', - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - SizedBox(height: 3), - Row( - children: [ - SizedBox(width: 4), - DidvanText( - 'آنلاین', - fontSize: 11, - color: Colors.white70, - ), - ], - ), - ], + child: DidvanText( + 'دستیار هوشمند دیدوان', + fontSize: 13, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + Container( + margin: const EdgeInsets.only(left: 6), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: AlignmentGeometry.topLeft, + end: AlignmentGeometry.bottomRight, + colors: [ + Color.fromARGB(255, 1, 35, 72), + Color.fromARGB(255, 27, 60, 89), + Color.fromARGB(255, 25, 93, 128), + Color.fromARGB(255, 0, 126, 167), + ], + ), + shape: BoxShape.circle, + ), + child: IconButton( + icon: const Icon(Icons.headset_mic_rounded, + color: Colors.white, size: 20), + onPressed: () { + Navigator.pop(context); + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const AiVoiceChatDialog(), + ); + }, + splashRadius: 20, + tooltip: 'گفتگوی صوتی', ), ), Container( @@ -632,67 +627,67 @@ class _AiChatDialogState extends State ), ), ], - if (!message.isUser && - message.audioUrl != null && - message.audioUrl!.isNotEmpty) ...[ - const SizedBox(height: 8), - InkWell( - onTap: () => _toggleAudioPlayback(message.audioUrl!), - borderRadius: BorderRadius.circular(20), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 8), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - const Color(0xFF0066AA).withOpacity(0.1), - const Color(0xFF0088DD).withOpacity(0.08), - ], - ), - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: const Color(0xFF0066AA).withOpacity(0.3), - width: 1, - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.all(4), - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [ - Color(0xFF0066AA), - Color(0xFF0088DD), - ], - ), - shape: BoxShape.circle, - ), - child: Icon( - (_isPlaying && - _currentPlayingUrl == message.audioUrl) - ? Icons.pause_rounded - : Icons.play_arrow_rounded, - size: 16, - color: Colors.white, - ), - ), - const SizedBox(width: 8), - DidvanText( - (_isPlaying && - _currentPlayingUrl == message.audioUrl) - ? 'در حال پخش...' - : 'پخش صوتی پاسخ', - fontSize: 12, - color: const Color(0xFF0066AA), - fontWeight: FontWeight.w600, - ), - ], - ), - ), - ), - ], + // if (!message.isUser && + // message.audioUrl != null && + // message.audioUrl!.isNotEmpty) ...[ + // const SizedBox(height: 8), + // InkWell( + // onTap: () => _toggleAudioPlayback(message.audioUrl!), + // borderRadius: BorderRadius.circular(20), + // child: Container( + // padding: const EdgeInsets.symmetric( + // horizontal: 12, vertical: 8), + // decoration: BoxDecoration( + // gradient: LinearGradient( + // colors: [ + // const Color(0xFF0066AA).withOpacity(0.1), + // const Color(0xFF0088DD).withOpacity(0.08), + // ], + // ), + // borderRadius: BorderRadius.circular(20), + // border: Border.all( + // color: const Color(0xFF0066AA).withOpacity(0.3), + // width: 1, + // ), + // ), + // child: Row( + // mainAxisSize: MainAxisSize.min, + // children: [ + // Container( + // padding: const EdgeInsets.all(4), + // decoration: const BoxDecoration( + // gradient: LinearGradient( + // colors: [ + // Color(0xFF0066AA), + // Color(0xFF0088DD), + // ], + // ), + // shape: BoxShape.circle, + // ), + // child: Icon( + // (_isPlaying && + // _currentPlayingUrl == message.audioUrl) + // ? Icons.pause_rounded + // : Icons.play_arrow_rounded, + // size: 16, + // color: Colors.white, + // ), + // ), + // const SizedBox(width: 8), + // DidvanText( + // (_isPlaying && + // _currentPlayingUrl == message.audioUrl) + // ? 'در حال پخش...' + // : 'پخش صوتی پاسخ', + // fontSize: 12, + // color: const Color(0xFF0066AA), + // fontWeight: FontWeight.w600, + // ), + // ], + // ), + // ), + // ), + // ], ], ), ), @@ -921,6 +916,51 @@ class _AiChatDialogState extends State child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ + GestureDetector( + onTap: _isLoading || _isRecording ? null : _sendMessage, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 44, + height: 55, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: (_isLoading || _isRecording) + ? [Colors.grey.shade300, Colors.grey.shade400] + : [const Color(0xFF0066AA), const Color(0xFF00AAFF)], + ), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: ((_isLoading || _isRecording) + ? Colors.grey.shade400 + : const Color(0xFF0066AA)) + .withOpacity(0.35), + blurRadius: 12, + offset: const Offset(0, 3), + ), + ], + ), + child: _isLoading + ? const Padding( + padding: EdgeInsets.all(11), + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + 'lib/assets/icons/send.svg', + color: Colors.white, + height: 5, + ), + ), + ), + ), + const SizedBox(width: 10), Expanded( child: Container( constraints: const BoxConstraints(maxHeight: 100), @@ -956,93 +996,48 @@ class _AiChatDialogState extends State ), ), ), - const SizedBox(width: 10), - GestureDetector( - onTap: () { - if (!_isLoading) { - if (_isRecording) { - _stopRecording(); - } else { - _startRecording(); - } - } - }, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - width: 44, - height: 44, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: _isRecording - ? [const Color(0xFFFF3366), const Color(0xFFFF6699)] - : [const Color(0xFF6B7280), const Color(0xFF9CA3AF)], - ), - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: (_isRecording - ? const Color(0xFFFF3366) - : const Color(0xFF6B7280)) - // ignore: deprecated_member_use - .withOpacity(0.35), - blurRadius: _isRecording ? 16 : 10, - offset: const Offset(0, 3), - ), - ], - ), - child: Icon( - _isRecording ? Icons.stop_rounded : Icons.mic_rounded, - color: Colors.white, - size: 20, - ), - ), - ), - const SizedBox(width: 10), - // دکمه ارسال - GestureDetector( - onTap: _isLoading || _isRecording ? null : _sendMessage, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - width: 44, - height: 44, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: (_isLoading || _isRecording) - ? [Colors.grey.shade300, Colors.grey.shade400] - : [const Color(0xFF0066AA), const Color(0xFF00AAFF)], - ), - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: ((_isLoading || _isRecording) - ? Colors.grey.shade400 - : const Color(0xFF0066AA)) - // ignore: deprecated_member_use - .withOpacity(0.35), - blurRadius: 12, - offset: const Offset(0, 3), - ), - ], - ), - child: _isLoading - ? const Padding( - padding: EdgeInsets.all(11), - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ) - : const Icon( - Icons.arrow_upward_rounded, - color: Colors.white, - size: 20, - ), - ), - ), + + // GestureDetector( + // onTap: () { + // if (!_isLoading) { + // if (_isRecording) { + // _stopRecording(); + // } else { + // _startRecording(); + // } + // } + // }, + // child: AnimatedContainer( + // duration: const Duration(milliseconds: 200), + // width: 44, + // height: 44, + // decoration: BoxDecoration( + // gradient: LinearGradient( + // begin: Alignment.topLeft, + // end: Alignment.bottomRight, + // colors: _isRecording + // ? [const Color(0xFFFF3366), const Color(0xFFFF6699)] + // : [const Color(0xFF6B7280), const Color(0xFF9CA3AF)], + // ), + // shape: BoxShape.circle, + // boxShadow: [ + // BoxShadow( + // color: (_isRecording + // ? const Color(0xFFFF3366) + // : const Color(0xFF6B7280)) + // .withOpacity(0.35), + // blurRadius: _isRecording ? 16 : 10, + // offset: const Offset(0, 3), + // ), + // ], + // ), + // child: Icon( + // _isRecording ? Icons.stop_rounded : Icons.mic_rounded, + // color: Colors.white, + // size: 20, + // ), + // ), + // ), ], ), ); diff --git a/lib/views/widgets/ai_voice_chat_dialog.dart b/lib/views/widgets/ai_voice_chat_dialog.dart new file mode 100644 index 0000000..728f088 --- /dev/null +++ b/lib/views/widgets/ai_voice_chat_dialog.dart @@ -0,0 +1,815 @@ +// ignore_for_file: deprecated_member_use + +import 'package:didvan/services/ai_rag_service.dart'; +import 'package:didvan/services/ai_voice_service.dart'; +import 'package:didvan/views/widgets/didvan/text.dart'; +import 'package:flutter/material.dart'; +import 'package:record/record.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:just_audio/just_audio.dart'; +import 'dart:ui'; +import 'dart:io'; +import 'dart:math' as math; + +class AiVoiceChatDialog extends StatefulWidget { + const AiVoiceChatDialog({super.key}); + + @override + State createState() => _AiVoiceChatDialogState(); +} + +class _AiVoiceChatDialogState extends State + with TickerProviderStateMixin { + bool _isRecording = false; + bool _isPreparing = false; + bool _isProcessing = false; + bool _isAiSpeaking = false; + String _statusText = 'برای شروع مکالمه، دکمه میکروفون را نگه دارید'; + + late AnimationController _waveController; + late AnimationController _pulseController; + late AnimationController _glowController; + late AnimationController _particleController; + late AnimationController _preparingController; + + final AudioRecorder _audioRecorder = AudioRecorder(); + final AudioPlayer _audioPlayer = AudioPlayer(); + String? _recordingPath; + + final List _audioWaveHeights = List.generate(40, (_) => 0.3); + int _currentWaveIndex = 0; + + final List _thinkingMessages = [ + 'در حال فکر کردن...', + 'در حال بررسی اطلاعات...', + 'در حال تحلیل سوال شما...', + 'در حال جستجو در دانش...', + 'در حال پردازش درخواست...', + 'در حال یافتن بهترین پاسخ...', + 'در حال بررسی جزئیات...', + 'در حال تحلیل داده‌های مرتبط...', + 'در حال مقایسه‌ی نتایج ممکن...', + 'در حال جمع‌آوری اطلاعات موردنیاز...', + 'در حال تشخیص الگوها...' + ]; + + final List _analyzingMessages = [ + 'در حال تجزیه و تحلیل...', + 'در حال پردازش داده‌ها...', + 'در حال بررسی محتوا...', + 'در حال ترکیب اطلاعات...', + 'در حال درک منظور شما...', + 'در حال آماده‌سازی پاسخ...', + 'در حال محاسبه‌ی بهترین گزینه...', + 'در حال درک مفهوم پرسش شما...', + 'در حال به‌روزرسانی اطلاعات...', + 'در حال استخراج پاسخ مناسب...', + 'در حال هماهنگ‌سازی با پایگاه دانش...' + ]; + + int _currentThinkingIndex = 0; + int _currentAnalyzingIndex = 0; + + @override + void initState() { + super.initState(); + + _waveController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 100), + )..addListener(() { + if (_isRecording || _isAiSpeaking) { + setState(() { + _currentWaveIndex = + (_currentWaveIndex + 1) % _audioWaveHeights.length; + for (int i = 0; i < _audioWaveHeights.length; i++) { + _audioWaveHeights[i] = 0.3 + (math.Random().nextDouble() * 0.7); + } + }); + } + }); + + _pulseController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1500), + )..repeat(reverse: true); + + _glowController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 2000), + )..repeat(reverse: true); + + _particleController = AnimationController( + vsync: this, + duration: const Duration(seconds: 3), + )..repeat(); + + _preparingController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 800), + )..repeat(reverse: true); + + _audioPlayer.playerStateStream.listen((state) { + if (mounted) { + setState(() { + _isAiSpeaking = state.playing; + if (state.processingState == ProcessingState.completed) { + _isAiSpeaking = false; + _statusText = 'برای ادامه مکالمه، دکمه میکروفون را نگه دارید'; + _waveController.stop(); + } else if (state.playing) { + _statusText = 'دستیار در حال پاسخ دادن است...'; + _waveController.repeat(); + } + }); + } + }); + } + + @override + void dispose() { + _waveController.dispose(); + _pulseController.dispose(); + _glowController.dispose(); + _particleController.dispose(); + _preparingController.dispose(); + _audioRecorder.dispose(); + _audioPlayer.dispose(); + super.dispose(); + } + + Future _startRecording() async { + try { + if (await _audioRecorder.hasPermission()) { + if (_isAiSpeaking) { + await _audioPlayer.stop(); + setState(() { + _isAiSpeaking = false; + }); + } + + setState(() { + _isPreparing = true; + _statusText = '⏳ در حال آماده سازی...'; + }); + + _preparingController.repeat(reverse: true); + + final directory = await getTemporaryDirectory(); + final timestamp = DateTime.now().millisecondsSinceEpoch; + _recordingPath = '${directory.path}/voice_$timestamp.m4a'; + + await _audioRecorder.start( + const RecordConfig( + encoder: AudioEncoder.aacLc, + ), + path: _recordingPath!, + ); + + await Future.delayed(const Duration(seconds: 2)); + + if (mounted && _isPreparing) { + setState(() { + _isPreparing = false; + _isRecording = true; + _statusText = '🎙️ در حال گوش دادن...'; + }); + _preparingController.stop(); + _waveController.repeat(); + } + } + } catch (e) { + setState(() { + _isPreparing = false; + _isRecording = false; + _statusText = 'خطا در شروع ضبط صدا'; + }); + _preparingController.stop(); + _waveController.stop(); + debugPrint('Error starting recording: $e'); + } + } + + Future _stopRecording() async { + if (!_isRecording) return; + + try { + setState(() { + _isRecording = false; + _isProcessing = true; + _currentThinkingIndex = 0; + _statusText = _thinkingMessages[_currentThinkingIndex]; + }); + + _waveController.stop(); + + _startThinkingMessageRotation(); + + await Future.delayed(const Duration(seconds: 2)); + + if (!mounted) return; + + final path = await _audioRecorder.stop(); + + if (path != null) { + final response = await AiVoiceService.uploadVoice(path); + + if (response.isSuccess && response.text.isNotEmpty) { + setState(() { + _currentAnalyzingIndex = 0; + _statusText = _analyzingMessages[_currentAnalyzingIndex]; + }); + + _startAnalyzingMessageRotation(); + + final ragResponse = await AiRagService.sendMessage(response.text); + + if (ragResponse.audioUrl != null && + ragResponse.audioUrl!.isNotEmpty) { + setState(() { + _statusText = '🔊 دستیار در حال پاسخ دادن است...'; + _isProcessing = false; + _isAiSpeaking = true; + }); + + await _audioPlayer.setUrl(ragResponse.audioUrl!); + await _audioPlayer.play(); + _waveController.repeat(); + } else { + setState(() { + _statusText = 'خطا در پردازش'; + _isProcessing = false; + }); + } + } else { + setState(() { + _statusText = 'خطا در پردازش پیام صوتی'; + _isProcessing = false; + }); + } + + try { + await File(path).delete(); + } catch (e) { + debugPrint('Error deleting temp file: $e'); + } + } else { + setState(() { + _isProcessing = false; + _statusText = 'خطا در ضبط صدا'; + }); + } + } catch (e) { + setState(() { + _isRecording = false; + _isProcessing = false; + _statusText = 'خطا در پردازش: ${e.toString()}'; + }); + _waveController.stop(); + debugPrint('Error stopping recording: $e'); + } + } + + void _startThinkingMessageRotation() { + Future.delayed(const Duration(seconds: 10), () { + if (mounted && _isProcessing && !_isAiSpeaking) { + setState(() { + _currentThinkingIndex = + (_currentThinkingIndex + 1) % _thinkingMessages.length; + _statusText = _thinkingMessages[_currentThinkingIndex]; + }); + _startThinkingMessageRotation(); + } + }); + } + + void _startAnalyzingMessageRotation() { + Future.delayed(const Duration(seconds: 10), () { + if (mounted && _isProcessing && !_isAiSpeaking) { + setState(() { + _currentAnalyzingIndex = + (_currentAnalyzingIndex + 1) % _analyzingMessages.length; + _statusText = _analyzingMessages[_currentAnalyzingIndex]; + }); + _startAnalyzingMessageRotation(); + } + }); + } + + Color _getMainColor() { + if (_isPreparing) return const Color(0xFFFFA500); + if (_isRecording) return const Color.fromARGB(255, 178, 4, 54); + if (_isAiSpeaking) return const Color(0xFF00AAFF); + if (_isProcessing) return const Color(0xFFFFAA00); + return const Color(0xFF6B7280); + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 40), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), + child: Container( + constraints: const BoxConstraints(maxWidth: 400, maxHeight: 700), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + const Color(0xFF0A0E27).withOpacity(0.95), + const Color(0xFF1A1F3A).withOpacity(0.95), + ], + ), + borderRadius: BorderRadius.circular(32), + border: Border.all( + color: Colors.white.withOpacity(0.1), + width: 1.5, + ), + boxShadow: [ + BoxShadow( + color: _getMainColor().withOpacity(0.3), + blurRadius: 40, + spreadRadius: 0, + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(32), + child: Stack( + children: [ + _buildAnimatedBackground(), + Column( + children: [ + _buildHeader(), + Expanded( + child: _buildMainContent(), + ), + _buildControls(), + ], + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildAnimatedBackground() { + return AnimatedBuilder( + animation: _particleController, + builder: (context, child) { + return CustomPaint( + painter: ParticlePainter( + animation: _particleController, + color: _getMainColor(), + ), + size: Size.infinite, + ); + }, + ); + } + + Widget _buildHeader() { + return Container( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 16), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.white.withOpacity(0.05), + Colors.transparent, + ], + ), + ), + child: Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [_getMainColor(), _getMainColor().withOpacity(0.6)], + ), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: _getMainColor().withOpacity(0.5), + blurRadius: 20, + spreadRadius: 2, + ), + ], + ), + child: Icon( + _isRecording + ? Icons.mic_rounded + : _isAiSpeaking + ? Icons.volume_up_rounded + : Icons.headset_mic_rounded, + color: Colors.white, + size: 24, + ), + ), + const SizedBox(width: 16), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DidvanText( + 'گفتگوی صوتی', + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + SizedBox(height: 4), + DidvanText( + 'دستیار هوشمند دیدوان', + fontSize: 12, + color: Colors.white60, + ), + ], + ), + ), + IconButton( + icon: const Icon(Icons.close_rounded, color: Colors.white70), + onPressed: () => Navigator.pop(context), + ), + ], + ), + ); + } + + Widget _buildMainContent() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildVisualization(), + const SizedBox(height: 40), + _buildStatusText(), + ], + ), + ); + } + + Widget _buildVisualization() { + return AnimatedBuilder( + animation: Listenable.merge( + [_pulseController, _glowController, _preparingController]), + builder: (context, child) { + final pulseValue = _pulseController.value; + final glowValue = _glowController.value; + final preparingValue = _preparingController.value; + + return Stack( + alignment: Alignment.center, + children: [ + if (_isPreparing) ...[ + for (int i = 0; i < 4; i++) + Transform.scale( + scale: 1.0 + (preparingValue * 0.3) + (i * 0.15), + child: Container( + width: 200.0, + height: 200.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: const Color(0xFFFFA500) + .withOpacity(0.4 - (i * 0.1)), + width: 3, + ), + ), + ), + ), + ], + if (!_isPreparing) + for (int i = 0; i < 3; i++) + Container( + width: 280 + (i * 40) + (pulseValue * 20), + height: 280 + (i * 40) + (pulseValue * 20), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: _getMainColor().withOpacity(0.1 - (i * 0.03)), + width: 2, + ), + ), + ), + Container( + width: 260 + (glowValue * 20), + height: 260 + (glowValue * 20), + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [ + _getMainColor().withOpacity(0.3), + _getMainColor().withOpacity(0.1), + Colors.transparent, + ], + ), + ), + ), + if (_isPreparing) + SizedBox( + width: 240, + height: 240, + child: CustomPaint( + painter: PreparingSpinnerPainter( + animation: preparingValue, + color: const Color(0xFFFFA500), + ), + ), + ) + else + SizedBox( + width: 240, + height: 240, + child: CustomPaint( + painter: WaveVisualizerPainter( + waveHeights: _audioWaveHeights, + color: _getMainColor(), + isActive: _isRecording || _isAiSpeaking, + ), + ), + ), + Container( + width: 120, + height: 120, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + _getMainColor(), + _getMainColor().withOpacity(0.7), + ], + ), + boxShadow: [ + BoxShadow( + color: _getMainColor().withOpacity(0.6), + blurRadius: 30, + spreadRadius: 5, + ), + ], + ), + child: Icon( + _isPreparing + ? Icons.settings_rounded + : _isRecording + ? Icons.mic_rounded + : _isAiSpeaking + ? Icons.graphic_eq_rounded + : _isProcessing + ? Icons.hourglass_empty_rounded + : Icons.headphones_rounded, + color: Colors.white, + size: 50, + ), + ), + ], + ); + }, + ); + } + + Widget _buildStatusText() { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 32), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.05), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: _getMainColor().withOpacity(0.3), + width: 1, + ), + ), + child: DidvanText( + _statusText, + fontSize: 14, + color: Colors.white, + textAlign: TextAlign.center, + ), + ); + } + + Widget _buildControls() { + return Container( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (_isAiSpeaking) + const Padding( + padding: EdgeInsets.only(bottom: 16), + child: DidvanText( + '', + fontSize: 12, + color: Colors.white70, + textAlign: TextAlign.center, + ), + ), + GestureDetector( + onLongPressStart: (_) { + if (!_isProcessing && !_isRecording && !_isPreparing) { + _startRecording(); + } + }, + onLongPressEnd: (_) { + if (_isRecording) { + _stopRecording(); + } + }, + child: AnimatedBuilder( + animation: _pulseController, + builder: (context, child) { + return Container( + width: 80, + height: 80, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: _isPreparing + ? [const Color(0xFFFFA500), const Color(0xFFFFCC00)] + : _isRecording + ? [ + const Color.fromARGB(255, 178, 4, 54), + const Color.fromARGB(255, 255, 200, 215) + ] + : _isProcessing + ? [Colors.grey.shade600, Colors.grey.shade700] + : [ + const Color(0xFF0066AA), + const Color(0xFF00AAFF) + ], + ), + boxShadow: [ + BoxShadow( + color: _getMainColor().withOpacity(0.5), + blurRadius: 20 + (_pulseController.value * 10), + spreadRadius: 5 + (_pulseController.value * 5), + ), + ], + ), + child: Icon( + _isPreparing || _isRecording + ? Icons.stop_rounded + : Icons.mic_rounded, + color: Colors.white, + size: 36, + ), + ); + }, + ), + ), + ], + ), + ); + } +} + +class WaveVisualizerPainter extends CustomPainter { + final List waveHeights; + final Color color; + final bool isActive; + + WaveVisualizerPainter({ + required this.waveHeights, + required this.color, + required this.isActive, + }); + + @override + void paint(Canvas canvas, Size size) { + if (!isActive) return; + + final paint = Paint() + ..color = color + ..style = PaintingStyle.fill; + + final center = Offset(size.width / 2, size.height / 2); + final radius = size.width / 2; + final barCount = waveHeights.length; + final angleStep = (2 * math.pi) / barCount; + + for (int i = 0; i < barCount; i++) { + final angle = i * angleStep; + final height = waveHeights[i] * 40; + + final startX = center.dx + (radius - 10) * math.cos(angle); + final startY = center.dy + (radius - 10) * math.sin(angle); + final endX = center.dx + (radius - 10 + height) * math.cos(angle); + final endY = center.dy + (radius - 10 + height) * math.sin(angle); + + paint.strokeWidth = 3; + paint.strokeCap = StrokeCap.round; + paint.color = color.withOpacity(0.6 + (waveHeights[i] * 0.4)); + + canvas.drawLine( + Offset(startX, startY), + Offset(endX, endY), + paint, + ); + } + } + + @override + bool shouldRepaint(covariant WaveVisualizerPainter oldDelegate) { + return true; + } +} + +class ParticlePainter extends CustomPainter { + final Animation animation; + final Color color; + + ParticlePainter({required this.animation, required this.color}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color.withOpacity(0.1) + ..style = PaintingStyle.fill; + + final random = math.Random(42); + for (int i = 0; i < 30; i++) { + final x = random.nextDouble() * size.width; + final baseY = random.nextDouble() * size.height; + final y = baseY + (animation.value * 100) % size.height; + final radius = 1 + random.nextDouble() * 2; + + canvas.drawCircle(Offset(x, y), radius, paint); + } + } + + @override + bool shouldRepaint(covariant ParticlePainter oldDelegate) { + return true; + } +} + +class PreparingSpinnerPainter extends CustomPainter { + final double animation; + final Color color; + + PreparingSpinnerPainter({required this.animation, required this.color}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 4 + ..strokeCap = StrokeCap.round; + + final center = Offset(size.width / 2, size.height / 2); + final radius = size.width / 2 - 20; + const dotCount = 12; + const angleStep = (2 * math.pi) / dotCount; + + for (int i = 0; i < dotCount; i++) { + final angle = (i * angleStep) + (animation * 2 * math.pi); + final opacity = (1.0 - (i / dotCount)) * 0.8; + + final x = center.dx + radius * math.cos(angle); + final y = center.dy + radius * math.sin(angle); + + paint.color = color.withOpacity(opacity); + + canvas.drawCircle( + Offset(x, y), + 3 + (animation * 2), + paint..style = PaintingStyle.fill, + ); + } + + for (int i = 0; i < 3; i++) { + final arcRadius = radius - (i * 25); + final startAngle = (animation * 2 * math.pi) + (i * math.pi / 3); + final sweepAngle = math.pi / 2 + (animation * math.pi / 4); + + paint + ..style = PaintingStyle.stroke + ..strokeWidth = 3.0 - i + ..color = color.withOpacity(0.5 - (i * 0.1)); + + canvas.drawArc( + Rect.fromCircle(center: center, radius: arcRadius), + startAngle, + sweepAngle, + false, + paint, + ); + } + } + + @override + bool shouldRepaint(covariant PreparingSpinnerPainter oldDelegate) { + return true; + } +} diff --git a/lib/views/widgets/didvan/time_sky_animation.dart b/lib/views/widgets/didvan/time_sky_animation.dart new file mode 100644 index 0000000..cbc8dab --- /dev/null +++ b/lib/views/widgets/didvan/time_sky_animation.dart @@ -0,0 +1,96 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; + +class TimeSkyAnimation extends StatelessWidget { + final int hour; // ساعت بین ۰ تا ۲۳ + + const TimeSkyAnimation({Key? key, required this.hour}) : super(key: key); + + bool get _isDay => hour >= 6 && hour < 18; // فرض می‌کنیم روز بین ۶ صبح تا ۶ عصر است + + double _getYofMoon(double x) { + // فرمول حرکت قوسی شکل + double r = 12; + double a = -12; + return sqrt(max(0, r * r - (x + a) * (x + a))); + } + + @override + Widget build(BuildContext context) { + // نرمال‌سازی ساعت برای حرکت نرم‌تر در انیمیشن (۰ تا ۲۴) + final double hValue = hour.toDouble(); + + return Container( + height: 120, // ارتفاع کانتینر آسمان + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), // گردی گوشه‌ها مشابه دیزاین دیدوان + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ) + ], + ), + clipBehavior: Clip.antiAlias, + child: Stack( + children: [ + // پس‌زمینه گرادیانت متحرک آسمان + AnimatedPositioned( + duration: const Duration(milliseconds: 500), + top: hValue * -84, // حرکت عمودی گرادیانت بر اساس ساعت + left: hValue * -84, // حرکت افقی گرادیانت + child: Container( + width: 2400, // عرض زیاد برای حرکت روان گرادیانت + height: 2400, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xff0d0221), // نیمه شب (تاریک‌ترین) + Color(0xff292929), // بامداد + Color(0xffFAD7A3), // طلوع + Color(0xff2ED4F9), // ظهر (روشن‌ترین) + Color(0xff2ED4F9), // بعد از ظهر + Color(0xffFAD7A3), // غروب + Color(0xff292929), // اوایل شب + Color(0xff0d0221), // نیمه شب + ], + ), + ), + ), + ), + // خورشید یا ماه + AnimatedPositioned( + duration: const Duration(milliseconds: 500), + bottom: _getYofMoon(hValue > 12 ? hValue - 12 : hValue) * 4, + left: (hValue + 1) * 12, // حرکت افقی + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + switchInCurve: Curves.easeInOut, + switchOutCurve: Curves.easeInOut, + transitionBuilder: (Widget child, Animation animation) { + return FadeTransition(opacity: animation, child: ScaleTransition(scale: animation, child: child)); + }, + child: _isDay + ? Image.asset( + 'lib/assets/images/sky/sun.png', + key: const ValueKey(1), + width: 80, + height: 80, + ) + : Image.asset( + 'lib/assets/images/sky/Moon.png', + key: const ValueKey(2), + width: 80, + height: 80, + ), + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/views/widgets/didvan/time_slider_picker.dart b/lib/views/widgets/didvan/time_slider_picker.dart new file mode 100644 index 0000000..1c751bb --- /dev/null +++ b/lib/views/widgets/didvan/time_slider_picker.dart @@ -0,0 +1,128 @@ +import 'package:didvan/models/day_time.dart'; +import 'package:didvan/views/widgets/didvan/toggle_button_time.dart'; +import 'package:flutter/material.dart'; + +class TimeSliderPicker extends StatefulWidget { + final DayTime selectedTime; + final ValueChanged onTimeChanged; + final bool isDisabled; + + const TimeSliderPicker({ + Key? key, + required this.selectedTime, + required this.onTimeChanged, + this.isDisabled = false, + }) : super(key: key); + + @override + State createState() => _TimeSliderPickerState(); +} + +class _TimeSliderPickerState extends State { + bool _isHourMode = true; + + int get _currentHour24 { + int hour = int.tryParse(widget.selectedTime.hour) ?? 12; + if (widget.selectedTime.meridiem == Meridiem.PM && hour != 12) hour += 12; + if (widget.selectedTime.meridiem == Meridiem.AM && hour == 12) hour = 0; + return hour; + } + + int get _currentMinute => int.tryParse(widget.selectedTime.minute) ?? 0; + + void _updateTime(int newHour, int newMinute) { + Meridiem newMeridiem = newHour >= 12 ? Meridiem.PM : Meridiem.AM; + int hour12 = newHour > 12 ? newHour - 12 : (newHour == 0 ? 12 : newHour); + + widget.onTimeChanged(DayTime( + hour: hour12.toString().padLeft(2, '0'), + minute: newMinute.toString().padLeft(2, '0'), + meridiem: newMeridiem, + )); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Opacity( + opacity: widget.isDisabled ? 0.5 : 1.0, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Text("دقیقه", style: theme.textTheme.bodySmall), + Text("ساعت", style: theme.textTheme.bodySmall), + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + DidvanToggleButtonTime( + title: _currentMinute.toString().padLeft(2, '0'), + active: !_isHourMode, + onTap: widget.isDisabled + ? () {} + : () => setState(() => _isHourMode = false), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text(":", + style: theme.textTheme.headlineMedium + ?.copyWith(fontWeight: FontWeight.bold)), + ), + DidvanToggleButtonTime( + title: _currentHour24.toString().padLeft(2, '0'), + active: _isHourMode, + onTap: widget.isDisabled + ? () {} + : () => setState(() => _isHourMode = true), + ), + ], + ), + const SizedBox(height: 24), + Directionality( + textDirection: TextDirection.ltr, + child: SliderTheme( + data: SliderThemeData( + trackHeight: 8, + activeTrackColor: theme.colorScheme.primary, + inactiveTrackColor: theme.colorScheme.outline.withOpacity(0.2), + thumbColor: theme.colorScheme.primary, + overlayColor: theme.colorScheme.primary.withOpacity(0.2), + thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 12), + valueIndicatorTextStyle: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onPrimary, + ), + ), + child: Slider( + value: _isHourMode + ? _currentHour24.toDouble() + : _currentMinute.toDouble(), + min: 0, + max: _isHourMode ? 23 : 59, + divisions: _isHourMode ? 23 : 59, + label: (_isHourMode ? _currentHour24 : _currentMinute) + .toString() + .padLeft(2, '0'), + onChanged: widget.isDisabled + ? null + : (value) { + if (_isHourMode) { + _updateTime(value.round(), _currentMinute); + } else { + _updateTime(_currentHour24, value.round()); + } + }, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/views/widgets/didvan/toggle_button_time.dart b/lib/views/widgets/didvan/toggle_button_time.dart new file mode 100644 index 0000000..2c9a5b4 --- /dev/null +++ b/lib/views/widgets/didvan/toggle_button_time.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; + +class DidvanToggleButtonTime extends StatelessWidget { + final bool active; + final String title; + final VoidCallback onTap; + + const DidvanToggleButtonTime({ + Key? key, + required this.title, + required this.active, + required this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(16), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 80, + height: 48, + decoration: BoxDecoration( + color: active ? colorScheme.primary : colorScheme.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: active + ? colorScheme.primary + : colorScheme.outline.withOpacity(0.2), + width: 2, + ), + ), + child: Center( + child: Text( + title, + style: theme.textTheme.titleLarge?.copyWith( + color: active ? colorScheme.onPrimary : colorScheme.onSurface, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 19770a4..0bf40a0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -151,6 +151,7 @@ flutter: - lib/assets/animations/ - lib/assets/js/ - lib/assets/icons/houshanNav/ + - lib/assets/images/sky/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware.