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 0000000..2a46e7e
Binary files /dev/null and b/lib/assets/images/sky/Moon.png differ
diff --git a/lib/assets/images/sky/Moon.svg b/lib/assets/images/sky/Moon.svg
new file mode 100644
index 0000000..fad5f8d
--- /dev/null
+++ b/lib/assets/images/sky/Moon.svg
@@ -0,0 +1,52 @@
+
diff --git a/lib/assets/images/sky/sun.png b/lib/assets/images/sky/sun.png
new file mode 100644
index 0000000..cf951e3
Binary files /dev/null and b/lib/assets/images/sky/sun.png differ
diff --git a/lib/models/message_data/message_data.dart b/lib/models/message_data/message_data.dart
index dfc3413..a23ffd1 100644
--- a/lib/models/message_data/message_data.dart
+++ b/lib/models/message_data/message_data.dart
@@ -16,6 +16,8 @@ class MessageData {
final File? audioFile;
final int? audioDuration;
final int? duration;
+ final String? writerName;
+ final String? writerPhoto;
MessageData({
required this.id,
@@ -29,6 +31,8 @@ class MessageData {
this.audioFile,
this.audioDuration,
this.duration,
+ this.writerName,
+ this.writerPhoto,
});
factory MessageData.fromJson(Map 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.