redesign support and add fake live voice chat

This commit is contained in:
Mr.Jebelli 2025-11-10 16:09:11 +03:30
parent 12349222d9
commit 6b2509ab89
21 changed files with 1980 additions and 493 deletions

View File

@ -0,0 +1,5 @@
<svg width="27" height="26" viewBox="0 0 27 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M23.2397 5.54306C24.7713 5.54306 26.0128 4.30221 26.0128 2.77153C26.0128 1.24086 24.7713 0 23.2397 0C21.7082 0 20.4666 1.24086 20.4666 2.77153C20.4666 4.30221 21.7082 5.54306 23.2397 5.54306Z" fill="#195D80"/>
<path d="M17.2652 13.463C19.818 13.463 21.8875 11.3947 21.8875 8.84329C21.8875 6.29192 19.818 4.22363 17.2652 4.22363C14.7123 4.22363 12.6428 6.29192 12.6428 8.84329C12.6428 11.3947 14.7123 13.463 17.2652 13.463Z" fill="#1B3C59"/>
<path d="M7.70366 25.998C11.9583 25.998 15.4074 22.551 15.4074 18.2988C15.4074 14.0467 11.9583 10.5996 7.70366 10.5996C3.44903 10.5996 0 14.0467 0 18.2988C0 22.551 3.44903 25.998 7.70366 25.998Z" fill="#012348"/>
</svg>

After

Width:  |  Height:  |  Size: 766 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -0,0 +1,3 @@
<svg width="20" height="11" viewBox="0 0 20 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 5.242L10.243 9.485L18.727 1M1 5.242L5.243 9.485M13.728 1L10.5 4.257" stroke="#007EA7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 267 B

4
lib/assets/icons/add.svg Normal file
View File

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 12H18" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 18V6" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 307 B

View File

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 10.5V15.5C22 19 20 20.5 17 20.5H7C4 20.5 2 19 2 15.5V8.5C2 5 4 3.5 7 3.5H14" stroke="#007EA7" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 9L10.13 11.5C11.16 12.32 12.85 12.32 13.88 11.5L15.06 10.56" stroke="#007EA7" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.5 8C20.8807 8 22 6.88071 22 5.5C22 4.11929 20.8807 3 19.5 3C18.1193 3 17 4.11929 17 5.5C17 6.88071 18.1193 8 19.5 8Z" fill="#B30436" stroke="#B30436" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 736 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -0,0 +1,52 @@
<svg width="94" height="67" viewBox="0 0 94 67" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="44.5" cy="33.5" r="28.5" fill="url(#paint0_linear_11774_25455)"/>
<circle cx="31" cy="43" r="3" fill="url(#paint1_linear_11774_25455)"/>
<circle cx="50.5" cy="23.5" r="4.5" fill="url(#paint2_linear_11774_25455)"/>
<circle cx="44" cy="13" r="3" fill="url(#paint3_linear_11774_25455)"/>
<circle cx="30.5" cy="20.5" r="2.5" fill="url(#paint4_linear_11774_25455)"/>
<circle cx="52.5" cy="42.5" r="6.5" fill="url(#paint5_linear_11774_25455)"/>
<circle cx="46" cy="56" r="2" fill="url(#paint6_linear_11774_25455)"/>
<circle cx="66.5" cy="32.5" r="3.5" fill="url(#paint7_linear_11774_25455)"/>
<circle cx="25.5" cy="32.5" r="2.5" fill="url(#paint8_linear_11774_25455)"/>
<circle opacity="0.2" cx="44.5" cy="33.5" r="33.5" fill="white"/>
<path opacity="0.4" d="M16 39C19.866 39 23 42.134 23 46C23 46.3396 22.9745 46.6733 22.9277 47H25.5C29.0899 47 32 49.9101 32 53.5C32 57.0899 29.0899 60 25.5 60H6.5C2.91015 60 0 57.0899 0 53.5C0 49.9101 2.91015 47 6.5 47H9.07227C9.02553 46.6733 9 46.3396 9 46C9 42.134 12.134 39 16 39Z" fill="#D9D9D9"/>
<path opacity="0.4" d="M78 3C81.866 3 85 6.13401 85 10C85 10.3396 84.9745 10.6733 84.9277 11H87.5C91.0899 11 94 13.9101 94 17.5C94 21.0899 91.0899 24 87.5 24H68.5C64.9101 24 62 21.0899 62 17.5C62 13.9101 64.9101 11 68.5 11H71.0723C71.0255 10.6733 71 10.3396 71 10C71 6.13401 74.134 3 78 3Z" fill="#D9D9D9"/>
<defs>
<linearGradient id="paint0_linear_11774_25455" x1="73" y1="2.5" x2="27.5" y2="57" gradientUnits="userSpaceOnUse">
<stop offset="0.264667" stop-color="#D9D9D9"/>
<stop offset="0.916299" stop-color="#747980"/>
</linearGradient>
<linearGradient id="paint1_linear_11774_25455" x1="34" y1="40" x2="27" y2="47.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#A99B9B" stop-opacity="0.77"/>
<stop offset="0.96237" stop-color="white" stop-opacity="0.58"/>
</linearGradient>
<linearGradient id="paint2_linear_11774_25455" x1="55" y1="19" x2="44.5" y2="30.25" gradientUnits="userSpaceOnUse">
<stop stop-color="#A99B9B" stop-opacity="0.77"/>
<stop offset="0.96237" stop-color="white" stop-opacity="0.58"/>
</linearGradient>
<linearGradient id="paint3_linear_11774_25455" x1="47" y1="10" x2="40" y2="17.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#A99B9B" stop-opacity="0.77"/>
<stop offset="0.96237" stop-color="white" stop-opacity="0.58"/>
</linearGradient>
<linearGradient id="paint4_linear_11774_25455" x1="33" y1="18" x2="27.1667" y2="24.25" gradientUnits="userSpaceOnUse">
<stop stop-color="#A99B9B" stop-opacity="0.77"/>
<stop offset="0.96237" stop-color="white" stop-opacity="0.58"/>
</linearGradient>
<linearGradient id="paint5_linear_11774_25455" x1="59" y1="36" x2="43.8333" y2="52.25" gradientUnits="userSpaceOnUse">
<stop stop-color="#A99B9B" stop-opacity="0.77"/>
<stop offset="0.96237" stop-color="white" stop-opacity="0.58"/>
</linearGradient>
<linearGradient id="paint6_linear_11774_25455" x1="48" y1="54" x2="43.3333" y2="59" gradientUnits="userSpaceOnUse">
<stop stop-color="#A99B9B" stop-opacity="0.77"/>
<stop offset="0.96237" stop-color="white" stop-opacity="0.58"/>
</linearGradient>
<linearGradient id="paint7_linear_11774_25455" x1="70" y1="29" x2="61.8333" y2="37.75" gradientUnits="userSpaceOnUse">
<stop stop-color="#A99B9B" stop-opacity="0.77"/>
<stop offset="0.96237" stop-color="white" stop-opacity="0.58"/>
</linearGradient>
<linearGradient id="paint8_linear_11774_25455" x1="28" y1="30" x2="22.1667" y2="36.25" gradientUnits="userSpaceOnUse">
<stop stop-color="#A99B9B" stop-opacity="0.77"/>
<stop offset="0.96237" stop-color="white" stop-opacity="0.58"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@ -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<String, dynamic> 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,
);
}
}

View File

@ -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<Direct> {
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<Direct> {
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<Direct> {
],
),
),
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,

View File

@ -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,
);

View File

@ -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<MessageBox> {
return Column(
children: [
Consumer<DirectState>(
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 {
],
);
}
}
}

View File

@ -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<NotificationSettings> {
? 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<CustomizeCategoryState>()
.putFavourites(context);
context
.read<NotificationTimeState>()
.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<CustomizeCategoryState>()
.putFavourites(context);
context
.read<NotificationTimeState>()
.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,
)
],
),
),
);

View File

@ -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<DirectList> {
return Consumer<DirectListState>(
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<DirectListState>().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<Color>(
const Color.fromARGB(255, 0, 126, 167),
),
shape:
MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
),
),
onPressed: () {
Navigator.of(context).pushNamed(
Routes.direct,
arguments: {'type': 'پشتیبانی اپلیکیشن'},
).then((value) {
if (mounted) {
context.read<DirectListState>().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<DirectListState>(
onRetry: state.getDirectsList,
itemPadding: const EdgeInsets.symmetric(horizontal: 16),

View File

@ -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),
),
),
],
),
),

View File

@ -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<AiChatDialog>
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<AiChatDialog>
child: _buildMessageList(),
),
if (_isLoading) _buildLoadingIndicator(),
if (_isRecording) _buildRecordingIndicator(),
// if (_isRecording) _buildRecordingIndicator(),
_buildInputField(),
],
),
@ -417,51 +418,45 @@ class _AiChatDialogState extends State<AiChatDialog>
),
),
),
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<AiChatDialog>
),
),
],
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<AiChatDialog>
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<Color>(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<AiChatDialog>
),
),
),
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<Color>(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,
// ),
// ),
// ),
],
),
);

View File

@ -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<AiVoiceChatDialog> createState() => _AiVoiceChatDialogState();
}
class _AiVoiceChatDialogState extends State<AiVoiceChatDialog>
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<double> _audioWaveHeights = List.generate(40, (_) => 0.3);
int _currentWaveIndex = 0;
final List<String> _thinkingMessages = [
'در حال فکر کردن...',
'در حال بررسی اطلاعات...',
'در حال تحلیل سوال شما...',
'در حال جستجو در دانش...',
'در حال پردازش درخواست...',
'در حال یافتن بهترین پاسخ...',
'در حال بررسی جزئیات...',
'در حال تحلیل داده‌های مرتبط...',
'در حال مقایسه‌ی نتایج ممکن...',
'در حال جمع‌آوری اطلاعات موردنیاز...',
'در حال تشخیص الگوها...'
];
final List<String> _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<void> _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<void> _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<double> 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<double> 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;
}
}

View File

@ -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<double> 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,
),
),
),
],
),
);
}
}

View File

@ -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<DayTime> onTimeChanged;
final bool isDisabled;
const TimeSliderPicker({
Key? key,
required this.selectedTime,
required this.onTimeChanged,
this.isDisabled = false,
}) : super(key: key);
@override
State<TimeSliderPicker> createState() => _TimeSliderPickerState();
}
class _TimeSliderPickerState extends State<TimeSliderPicker> {
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());
}
},
),
),
),
],
),
);
}
}

View File

@ -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,
),
),
),
),
);
}
}

View File

@ -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.