redesign support and add fake live voice chat
|
|
@ -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 |
|
After Width: | Height: | Size: 46 KiB |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
After Width: | Height: | Size: 6.2 KiB |
|
|
@ -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 |
|
After Width: | Height: | Size: 5.1 KiB |
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||