diff --git a/lib/assets/icons/Economic_NoBG.svg b/lib/assets/icons/Economic_NoBG.svg
new file mode 100644
index 0000000..2362653
--- /dev/null
+++ b/lib/assets/icons/Economic_NoBG.svg
@@ -0,0 +1,5 @@
+
diff --git a/lib/assets/icons/Ejtemaei.svg b/lib/assets/icons/Ejtemaei.svg
new file mode 100644
index 0000000..90a0905
--- /dev/null
+++ b/lib/assets/icons/Ejtemaei.svg
@@ -0,0 +1,5 @@
+
diff --git a/lib/assets/icons/EjtemaeiNoBg.svg b/lib/assets/icons/EjtemaeiNoBg.svg
new file mode 100644
index 0000000..d33078e
--- /dev/null
+++ b/lib/assets/icons/EjtemaeiNoBg.svg
@@ -0,0 +1,4 @@
+
diff --git a/lib/assets/icons/Fanavari.svg b/lib/assets/icons/Fanavari.svg
new file mode 100644
index 0000000..b4f92d5
--- /dev/null
+++ b/lib/assets/icons/Fanavari.svg
@@ -0,0 +1,15 @@
+
diff --git a/lib/assets/icons/FanavariNoBG.svg b/lib/assets/icons/FanavariNoBG.svg
new file mode 100644
index 0000000..fb28f17
--- /dev/null
+++ b/lib/assets/icons/FanavariNoBG.svg
@@ -0,0 +1,14 @@
+
diff --git a/lib/assets/icons/KasboKar.svg b/lib/assets/icons/KasboKar.svg
new file mode 100644
index 0000000..7f564ff
--- /dev/null
+++ b/lib/assets/icons/KasboKar.svg
@@ -0,0 +1,5 @@
+
diff --git a/lib/assets/icons/KasbokarNoBG.svg b/lib/assets/icons/KasbokarNoBG.svg
new file mode 100644
index 0000000..bda4780
--- /dev/null
+++ b/lib/assets/icons/KasbokarNoBG.svg
@@ -0,0 +1,3 @@
+
diff --git a/lib/assets/icons/New_Economic.svg b/lib/assets/icons/New_Economic.svg
new file mode 100644
index 0000000..9c502c9
--- /dev/null
+++ b/lib/assets/icons/New_Economic.svg
@@ -0,0 +1,13 @@
+
diff --git a/lib/assets/icons/New_Ejtemaei.svg b/lib/assets/icons/New_Ejtemaei.svg
new file mode 100644
index 0000000..dc1758e
--- /dev/null
+++ b/lib/assets/icons/New_Ejtemaei.svg
@@ -0,0 +1,12 @@
+
diff --git a/lib/assets/icons/New_Fanavari.svg b/lib/assets/icons/New_Fanavari.svg
new file mode 100644
index 0000000..75e3c84
--- /dev/null
+++ b/lib/assets/icons/New_Fanavari.svg
@@ -0,0 +1,22 @@
+
diff --git a/lib/assets/icons/New_Kasbokar.svg b/lib/assets/icons/New_Kasbokar.svg
new file mode 100644
index 0000000..4196b15
--- /dev/null
+++ b/lib/assets/icons/New_Kasbokar.svg
@@ -0,0 +1,11 @@
+
diff --git a/lib/assets/icons/New_Siasi.svg b/lib/assets/icons/New_Siasi.svg
new file mode 100644
index 0000000..f1d5df9
--- /dev/null
+++ b/lib/assets/icons/New_Siasi.svg
@@ -0,0 +1,15 @@
+
diff --git a/lib/assets/icons/New_ZistMohit.svg b/lib/assets/icons/New_ZistMohit.svg
new file mode 100644
index 0000000..b88cf47
--- /dev/null
+++ b/lib/assets/icons/New_ZistMohit.svg
@@ -0,0 +1,13 @@
+
diff --git a/lib/assets/icons/Siasi.svg b/lib/assets/icons/Siasi.svg
new file mode 100644
index 0000000..92ec1cb
--- /dev/null
+++ b/lib/assets/icons/Siasi.svg
@@ -0,0 +1,7 @@
+
diff --git a/lib/assets/icons/SiasiNoBG.svg b/lib/assets/icons/SiasiNoBG.svg
new file mode 100644
index 0000000..034f223
--- /dev/null
+++ b/lib/assets/icons/SiasiNoBG.svg
@@ -0,0 +1,7 @@
+
diff --git a/lib/assets/icons/ZistMohit.svg b/lib/assets/icons/ZistMohit.svg
new file mode 100644
index 0000000..2302fdd
--- /dev/null
+++ b/lib/assets/icons/ZistMohit.svg
@@ -0,0 +1,12 @@
+
diff --git a/lib/assets/icons/ZistMohitNoBG.svg b/lib/assets/icons/ZistMohitNoBG.svg
new file mode 100644
index 0000000..4956d2e
--- /dev/null
+++ b/lib/assets/icons/ZistMohitNoBG.svg
@@ -0,0 +1,3 @@
+
diff --git a/lib/assets/icons/Zist_Mohit.svg b/lib/assets/icons/Zist_Mohit.svg
new file mode 100644
index 0000000..90dd33b
--- /dev/null
+++ b/lib/assets/icons/Zist_Mohit.svg
@@ -0,0 +1,4 @@
+
diff --git a/lib/assets/icons/cpu-setting.svg b/lib/assets/icons/cpu-setting.svg
new file mode 100644
index 0000000..f8c57ab
--- /dev/null
+++ b/lib/assets/icons/cpu-setting.svg
@@ -0,0 +1,14 @@
+
diff --git a/lib/assets/icons/economic.svg b/lib/assets/icons/economic.svg
new file mode 100644
index 0000000..faaacc2
--- /dev/null
+++ b/lib/assets/icons/economic.svg
@@ -0,0 +1,4 @@
+
diff --git a/lib/assets/icons/economic_bg.svg b/lib/assets/icons/economic_bg.svg
new file mode 100644
index 0000000..161612a
--- /dev/null
+++ b/lib/assets/icons/economic_bg.svg
@@ -0,0 +1,5 @@
+
diff --git a/lib/assets/icons/fluent_mention-32-regular.svg b/lib/assets/icons/fluent_mention-32-regular.svg
new file mode 100644
index 0000000..d41c359
--- /dev/null
+++ b/lib/assets/icons/fluent_mention-32-regular.svg
@@ -0,0 +1,3 @@
+
diff --git a/lib/assets/icons/icon-park-outline_earth.svg b/lib/assets/icons/icon-park-outline_earth.svg
new file mode 100644
index 0000000..bad0e4d
--- /dev/null
+++ b/lib/assets/icons/icon-park-outline_earth.svg
@@ -0,0 +1,6 @@
+
diff --git a/lib/assets/icons/message-text.svg b/lib/assets/icons/message-text.svg
new file mode 100644
index 0000000..64030ae
--- /dev/null
+++ b/lib/assets/icons/message-text.svg
@@ -0,0 +1,5 @@
+
diff --git a/lib/assets/icons/solar_users-group-two-rounded-broken.svg b/lib/assets/icons/solar_users-group-two-rounded-broken.svg
new file mode 100644
index 0000000..bfbf923
--- /dev/null
+++ b/lib/assets/icons/solar_users-group-two-rounded-broken.svg
@@ -0,0 +1,4 @@
+
diff --git a/lib/models/home_page_content/content.dart b/lib/models/home_page_content/content.dart
index e32a62a..dc89c3d 100644
--- a/lib/models/home_page_content/content.dart
+++ b/lib/models/home_page_content/content.dart
@@ -1,13 +1,13 @@
-
class MainPageContentType {
final int id;
final String title;
final String image;
final String link;
bool marked;
- bool isViewed;
+ bool isViewed;
final List subtitles;
final int? duration;
+ final String? description;
MainPageContentType({
required this.id,
@@ -15,9 +15,10 @@ class MainPageContentType {
required this.image,
required this.link,
required this.marked,
- required this.isViewed,
+ required this.isViewed,
required this.subtitles,
this.duration,
+ this.description,
});
factory MainPageContentType.fromJson(Map json) =>
@@ -30,5 +31,6 @@ class MainPageContentType {
isViewed: json['isViewed'] ?? false,
subtitles: List.from(json['subtitles']),
duration: json['duration'],
+ description: json['description'],
);
-}
\ No newline at end of file
+}
diff --git a/lib/models/news_details_data.dart b/lib/models/news_details_data.dart
index cfab02d..24fe67e 100644
--- a/lib/models/news_details_data.dart
+++ b/lib/models/news_details_data.dart
@@ -17,6 +17,7 @@ class NewsDetailsData {
final List contents;
final List relatedContents = [];
bool relatedContentsIsEmpty;
+ String? description;
NewsDetailsData({
required this.id,
@@ -32,6 +33,7 @@ class NewsDetailsData {
required this.contents,
required this.order,
this.relatedContentsIsEmpty = false,
+ this.description,
});
factory NewsDetailsData.fromJson(Map json) {
@@ -46,6 +48,7 @@ class NewsDetailsData {
likes: json['likes'],
comments: json['comments'],
order: json['order'],
+ description: json['description'],
tags: List.from(json['tags'].map((tag) => Tag.fromJson(tag))),
contents: List.from(json['contents'].map(
(content) => Content.fromJson(content),
@@ -64,6 +67,7 @@ class NewsDetailsData {
'liked': liked,
'likes': likes,
'comments': comments,
+ 'description': description,
'tags': tags.map((e) => e.toJson()).toList(),
'contents': contents.map((e) => e.toJson()).toList(),
};
diff --git a/lib/models/radar_details_data.dart b/lib/models/radar_details_data.dart
index 839bebb..76e8e11 100644
--- a/lib/models/radar_details_data.dart
+++ b/lib/models/radar_details_data.dart
@@ -21,6 +21,7 @@ class RadarDetailsData {
final int order;
final List relatedContents = [];
bool relatedContentsIsEmpty;
+ String? description;
RadarDetailsData({
required this.id,
@@ -38,6 +39,7 @@ class RadarDetailsData {
required this.order,
required this.duration,
this.relatedContentsIsEmpty = false,
+ this.description,
});
factory RadarDetailsData.fromJson(Map json) {
@@ -64,6 +66,7 @@ class RadarDetailsData {
),
),
duration: json['duration'],
+ description: json['description'],
);
}
@@ -81,5 +84,6 @@ class RadarDetailsData {
'tags': tags.map((e) => e.toJson()).toList(),
'contents': contents.map((e) => e.toJson()).toList(),
'categories': categories.map((e) => e.toJson()).toList(),
+ 'description': description,
};
}
diff --git a/lib/services/ai_rag_service.dart b/lib/services/ai_rag_service.dart
index 9124455..cf24e9a 100644
--- a/lib/services/ai_rag_service.dart
+++ b/lib/services/ai_rag_service.dart
@@ -6,6 +6,21 @@ class AiRagService {
'https://n8n-didvan.liara.run/webhook/didvan-rag';
static const String _authHeader =
'Basic dXNlcjpkYXNqZHBuM3BjdTQzcDM0aWpyaA==';
+ static const String _didvanBaseUrl = 'https://api.didvan.app';
+
+ static String _getFullAudioUrl(String? url) {
+ if (url == null || url.isEmpty) return '';
+
+ if (url.startsWith('http://') || url.startsWith('https://')) {
+ return url;
+ }
+
+ if (url.startsWith('/')) {
+ return '$_didvanBaseUrl$url';
+ }
+
+ return '$_didvanBaseUrl/$url';
+ }
static Future sendMessage(String message) async {
try {
@@ -23,9 +38,11 @@ class AiRagService {
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
+ final rawAudioUrl = data['url'] as String?;
return AiRagResponse(
output: data['output'] ?? '',
sources: List.from(data['sources'] ?? []),
+ audioUrl: _getFullAudioUrl(rawAudioUrl),
isSuccess: true,
);
} else {
@@ -48,11 +65,13 @@ class AiRagService {
class AiRagResponse {
final String output;
final List sources;
+ final String? audioUrl;
final bool isSuccess;
AiRagResponse({
required this.output,
required this.sources,
+ this.audioUrl,
required this.isSuccess,
});
}
diff --git a/lib/views/comments/comments.dart b/lib/views/comments/comments.dart
index 59596bf..3f9af75 100644
--- a/lib/views/comments/comments.dart
+++ b/lib/views/comments/comments.dart
@@ -55,46 +55,65 @@ class _CommentsState extends State {
}
}
_bottomPadding = bottomViewInset;
- return Material(
- child: DidvanScaffold(
- hidePlayer: true,
- physics: const BouncingScrollPhysics(),
- backgroundColor: Theme.of(context).colorScheme.surface,
- appBarData: _isPage
- ? AppBarData(
- hasBack: true,
- title: 'نظرات',
- subtitle: widget.pageData['title'],
- )
- : null,
- padding: _isPage ? const EdgeInsets.only(bottom: 92) : EdgeInsets.zero,
- slivers: [
- Consumer(
- builder: (context, state, child) =>
- SliverStateHandler(
- onRetry: state.getComments,
- state: state,
- itemPadding: const EdgeInsets.only(top: 16, bottom: 16),
- childCount: state.comments.length,
- placeholder: const _CommentPlaceholder(),
- centerEmptyState: _isPage,
- enableEmptyState: state.comments.isEmpty,
- emptyState: EmptyState(
- asset: Assets.emptyChat,
- title: 'اولین نظر را بنویسید...',
- ),
- builder: (context, state, index) => Comment(
- key: ValueKey(
- state.comments[index].id.toString() +
- state.comments[index].text,
- ),
- focusNode: _focusNode,
- comment: state.comments[index],
+
+ final scaffold = DidvanScaffold(
+ hidePlayer: true,
+ physics: const BouncingScrollPhysics(),
+ backgroundColor: Theme.of(context).colorScheme.surface,
+ appBarData: _isPage
+ ? AppBarData(
+ hasBack: true,
+ title: 'نظرات',
+ subtitle: widget.pageData['title'],
+ )
+ : null,
+ padding: _isPage ? const EdgeInsets.only(bottom: 92) : EdgeInsets.zero,
+ slivers: [
+ Consumer(
+ builder: (context, state, child) => SliverStateHandler(
+ onRetry: state.getComments,
+ state: state,
+ itemPadding: const EdgeInsets.only(top: 16, bottom: 16),
+ childCount: state.comments.length,
+ placeholder: const _CommentPlaceholder(),
+ centerEmptyState: _isPage,
+ enableEmptyState: state.comments.isEmpty,
+ emptyState: EmptyState(
+ asset: Assets.emptyChat,
+ title: 'اولین نظر را بنویسید...',
+ ),
+ builder: (context, state, index) => Comment(
+ key: ValueKey(
+ state.comments[index].id.toString() +
+ state.comments[index].text,
),
+ focusNode: _focusNode,
+ comment: state.comments[index],
),
),
- ],
- ),
+ ),
+ ],
+ );
+
+ if (_isPage) {
+ return Scaffold(
+ resizeToAvoidBottomInset: true,
+ body: Stack(
+ children: [
+ Positioned.fill(child: scaffold),
+ Positioned(
+ left: 20,
+ right: 20,
+ bottom: 20,
+ child: CommentMessageBox(focusNode: _focusNode),
+ ),
+ ],
+ ),
+ );
+ }
+
+ return Material(
+ child: scaffold,
);
}
}
diff --git a/lib/views/direct/widgets/audio_widget.dart b/lib/views/direct/widgets/audio_widget.dart
index 67938ec..3c92884 100644
--- a/lib/views/direct/widgets/audio_widget.dart
+++ b/lib/views/direct/widgets/audio_widget.dart
@@ -5,7 +5,7 @@ import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/requests/studio.dart';
import 'package:didvan/models/studio_details_data.dart';
import 'package:didvan/services/media/media.dart';
-import 'package:didvan/views/widgets/audio/audio_slider.dart';
+import 'package:didvan/views/home/media/widgets/audio_waveform_progress.dart';
import 'package:didvan/views/podcasts/studio_details/studio_details_state.dart';
import 'package:didvan/views/widgets/didvan/icon_button.dart';
import 'package:flutter/material.dart';
@@ -27,41 +27,103 @@ class AudioWidget extends StatelessWidget {
this.duration,
}) : super(key: key);
+ String _formatDuration(Duration duration) {
+ String twoDigits(int n) => n.toString().padLeft(2, "0");
+ String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
+ String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
+ return "$twoDigitMinutes:$twoDigitSeconds";
+ }
+
@override
Widget build(BuildContext context) {
- return StreamBuilder(
- stream: MediaService.audioPlayer.playingStream,
+ final tag =
+ audioMetaData != null ? '${audioMetaData!.type}-$id' : 'message-$id';
+
+ return StreamBuilder(
+ stream: MediaService.audioPlayer.playerStateStream,
builder: (context, snapshot) {
- return Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Expanded(
- child: Padding(
- padding: const EdgeInsets.only(top: 12),
- child: Row(
- children: [
- Expanded(
- child: AudioSlider(
- tag: audioMetaData != null
- ? '${audioMetaData!.type}-$id'
- : 'message-$id',
- duration:
- audioMetaData?.duration ?? duration?.inSeconds,
- showTimer: true,
- ),
- ),
- ],
+ final isCurrentlyPlaying = MediaService.audioPlayerTag == tag;
+
+ return StreamBuilder(
+ stream: MediaService.audioPlayer.positionStream,
+ builder: (context, posSnapshot) {
+ var totalDuration = audioMetaData?.duration != null
+ ? Duration(seconds: audioMetaData!.duration!)
+ : (duration ?? Duration.zero);
+
+ if (isCurrentlyPlaying &&
+ MediaService.audioPlayer.duration != null &&
+ MediaService.audioPlayer.duration! > Duration.zero) {
+ totalDuration = MediaService.audioPlayer.duration!;
+ }
+
+ final currentPosition = isCurrentlyPlaying
+ ? (posSnapshot.data ?? Duration.zero)
+ : Duration.zero;
+
+ final progress = (totalDuration.inMilliseconds > 0)
+ ? currentPosition.inMilliseconds / totalDuration.inMilliseconds
+ : 0.0;
+
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ _AudioControllerButton(
+ audioFile: audioFile,
+ audioUrl: audioUrl,
+ id: id,
+ audioMetaData: audioMetaData,
),
- ),
- ),
- const SizedBox(width: 12),
- _AudioControllerButton(
- audioFile: audioFile,
- audioUrl: audioUrl,
- id: id,
- audioMetaData: audioMetaData,
- ),
- ],
+ const SizedBox(width: 12),
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.only(top: 4),
+ child: Row(
+ children: [
+ SizedBox(
+ width: 45,
+ child: Text(
+ _formatDuration(totalDuration),
+ style: const TextStyle(
+ fontSize: 12,
+ color: Color.fromARGB(255, 102, 102, 102)),
+ textAlign: TextAlign.center,
+ ),
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: AudioWaveformProgress(
+ progress: progress.clamp(0.0, 1.0),
+ isActive: isCurrentlyPlaying,
+ onChanged: isCurrentlyPlaying
+ ? (value) {
+ final seekMillis =
+ (value * totalDuration.inMilliseconds)
+ .toInt();
+ MediaService.audioPlayer.seek(
+ Duration(milliseconds: seekMillis));
+ }
+ : null,
+ ),
+ ),
+ const SizedBox(width: 8),
+ SizedBox(
+ width: 45,
+ child: Text(
+ _formatDuration(currentPosition),
+ style: const TextStyle(
+ fontSize: 12,
+ color: Color.fromARGB(255, 102, 102, 102)),
+ textAlign: TextAlign.center,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ );
+ },
);
},
);
diff --git a/lib/views/home/explore/explore.dart b/lib/views/home/explore/explore.dart
index 681e587..00bc26c 100644
--- a/lib/views/home/explore/explore.dart
+++ b/lib/views/home/explore/explore.dart
@@ -178,7 +178,7 @@ class StaggeredEntry extends StatefulWidget {
super.key,
required this.child,
required this.index,
- this.duration = const Duration(milliseconds: 500),
+ this.duration = const Duration(milliseconds: 250),
this.verticalOffset = 50.0,
});
@@ -268,7 +268,7 @@ class SwotSection extends StatelessWidget {
),
const SizedBox(width: 5),
DidvanText(
- "باید ها و نباید ها",
+ "بایدها و نبایدها",
style: Theme.of(context).textTheme.titleSmall,
color: const Color.fromARGB(255, 0, 89, 119),
),
@@ -529,6 +529,7 @@ class MainPageSection extends StatelessWidget {
list.type,
item.id,
item.link,
+ description: item.description,
),
child: Padding(
padding: const EdgeInsets.all(8.0),
@@ -605,6 +606,7 @@ class MainPageSection extends StatelessWidget {
list.type,
item.id,
item.link,
+ description: item.description,
),
child: Padding(
padding: const EdgeInsets.all(8.0),
diff --git a/lib/views/home/main/main_page_state.dart b/lib/views/home/main/main_page_state.dart
index b8eb775..89ec7ef 100644
--- a/lib/views/home/main/main_page_state.dart
+++ b/lib/views/home/main/main_page_state.dart
@@ -24,27 +24,22 @@ class MainPageState extends CoreProvier {
List swotItems = [];
Future _getMainPageContent() async {
- print("DEBUG: _getMainPageContent started");
final service = RequestService(RequestHelper.mainPageContent);
await service.httpGet();
if (service.isSuccess) {
- print("DEBUG: _getMainPageContent success");
content = MainPageContent.fromJson(service.result);
- print("DEBUG: _getMainPageContent service.result: ${service.result}");
unread = service.result['unread'];
- print("DEBUG: __getMainPageContent unread: $unread, content: $content");
notifyListeners();
} else {
- print("DEBUG: _getMainPageContent failed state");
notifyListeners();
throw Exception('Failed to load main page content');
}
}
int getStoryStartIndex(List stories) {
- final firstUnreadIndex = stories.indexWhere((story) => !story.isViewed);
- return firstUnreadIndex != -1 ? firstUnreadIndex : 0;
-}
+ final firstUnreadIndex = stories.indexWhere((story) => !story.isViewed);
+ return firstUnreadIndex != -1 ? firstUnreadIndex : 0;
+ }
Future _getSwotItems() async {
try {
@@ -94,8 +89,9 @@ class MainPageState extends CoreProvier {
void navigationHandler(
String type,
int id,
- String? link,
- ) {
+ String? link, {
+ String? description,
+ }) {
link = link ?? '';
dynamic args;
switch (type) {
@@ -118,6 +114,7 @@ class MainPageState extends CoreProvier {
'id': id,
'args': const NewsRequestArgs(page: 0),
'hasUnmarkConfirmation': false,
+ 'description': description,
};
break;
}
@@ -129,6 +126,7 @@ class MainPageState extends CoreProvier {
'id': id,
'args': const RadarRequestArgs(page: 0),
'hasUnmarkConfirmation': false,
+ 'description': description,
};
break;
}
@@ -155,4 +153,4 @@ class MainPageState extends CoreProvier {
}
Navigator.of(navigatorKey.currentContext!).pushNamed(link, arguments: args);
}
-}
\ No newline at end of file
+}
diff --git a/lib/views/news/news_details/news_details.dart b/lib/views/news/news_details/news_details.dart
index d6a021e..6329b49 100644
--- a/lib/views/news/news_details/news_details.dart
+++ b/lib/views/news/news_details/news_details.dart
@@ -28,6 +28,7 @@ class _NewsDetailsState extends State {
}
});
state.args = widget.pageData['args'] ?? const NewsRequestArgs(page: 0);
+ state.initialDescription = widget.pageData['description'];
Future.delayed(Duration.zero, () {
state.getNewsDetails(widget.pageData['id']);
});
@@ -42,8 +43,7 @@ class _NewsDetailsState extends State {
return Scaffold(
body: Consumer(
builder: (context, state, child) => StateHandler(
- onRetry: () =>
- state.getNewsDetails(widget.pageData['id']),
+ onRetry: () => state.getNewsDetails(widget.pageData['id']),
state: state,
builder: (context, state) => Stack(
children: [
diff --git a/lib/views/news/news_details/news_details_state.dart b/lib/views/news/news_details/news_details_state.dart
index c4c2dea..65a16a5 100644
--- a/lib/views/news/news_details/news_details_state.dart
+++ b/lib/views/news/news_details/news_details_state.dart
@@ -13,6 +13,7 @@ class NewsDetailsState extends CoreProvier {
final List news = [];
late final int initialIndex;
late final NewsRequestArgs args;
+ String? initialDescription;
late Timer _trackingTimer;
int _trackingTimerCounter = 0;
bool isFetchingNewItem = false;
@@ -26,13 +27,11 @@ class NewsDetailsState extends CoreProvier {
NewsDetailsData get currentNews => news[_currentIndex]!;
Future getNewsDetails(int id, {bool? isForward}) async {
- // Prevent duplicate requests
if (_isRequestInProgress) {
return;
}
_isRequestInProgress = true;
- // Set loading state
if (isForward == null) {
appState = AppState.busy;
} else {
@@ -41,7 +40,6 @@ class NewsDetailsState extends CoreProvier {
}
try {
- // Wait for token to be ready
if (RequestService.token == null || RequestService.token!.isEmpty) {
await Future.delayed(const Duration(milliseconds: 500));
if (RequestService.token == null || RequestService.token!.isEmpty) {
@@ -56,6 +54,15 @@ class NewsDetailsState extends CoreProvier {
if (service.isSuccess) {
final result = service.result;
final newsItem = NewsDetailsData.fromJson(result['news']);
+
+ if (isForward == null && initialDescription != null) {
+ newsItem.description = initialDescription;
+ }
+
+ if (isForward == null && newsItem.description == null) {
+ await _fetchDescriptionFromList(newsItem.id, newsItem);
+ }
+
if (args.page == 0) {
news.add(newsItem);
initialIndex = 0;
@@ -73,7 +80,8 @@ class NewsDetailsState extends CoreProvier {
nextNews = NewsDetailsData.fromJson(result['nextNews']);
}
if (isForward == null) {
- news.addAll(List.generate(max(newsItem.order - 2, 0), (index) => null));
+ news.addAll(
+ List.generate(max(newsItem.order - 2, 0), (index) => null));
if (prevNews != null) {
news.add(prevNews);
}
@@ -107,6 +115,7 @@ class NewsDetailsState extends CoreProvier {
}
}
} catch (e) {
+ // ignore: avoid_print
print('Error fetching news details: $e');
isFetchingNewItem = false;
if (isForward == null) {
@@ -145,6 +154,35 @@ class NewsDetailsState extends CoreProvier {
_trackingTimer.cancel();
}
+ Future _fetchDescriptionFromList(
+ int newsId, NewsDetailsData newsItem) async {
+ try {
+ final service = RequestService(
+ RequestHelper.newsOverviews(
+ args: const NewsRequestArgs(page: 1),
+ ),
+ );
+ await service.httpGet();
+
+ if (service.isSuccess) {
+ final newsList = service.result['news'] as List?;
+ if (newsList != null) {
+ for (var item in newsList) {
+ if (item['id'] == newsId) {
+ final newsData = OverviewData.fromJson(item);
+ if (newsData.description.isNotEmpty) {
+ newsItem.description = newsData.description;
+ }
+ break;
+ }
+ }
+ }
+ }
+ } catch (e) {
+ print('Error fetching description from list: $e');
+ }
+ }
+
Future getRelatedContents() async {
if (currentNews.relatedContents.isNotEmpty &&
currentNews.relatedContentsIsEmpty) {
@@ -187,4 +225,4 @@ class NewsDetailsState extends CoreProvier {
_trackingTimer.cancel();
super.dispose();
}
-}
\ No newline at end of file
+}
diff --git a/lib/views/profile/profile.dart b/lib/views/profile/profile.dart
index 51e7052..6b54610 100644
--- a/lib/views/profile/profile.dart
+++ b/lib/views/profile/profile.dart
@@ -3,7 +3,6 @@ import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/constants/assets.dart';
import 'package:didvan/models/view/action_sheet_data.dart';
-import 'package:didvan/models/view/app_bar_data.dart';
import 'package:didvan/providers/theme.dart';
import 'package:didvan/providers/user.dart';
import 'package:didvan/routes/routes.dart';
@@ -54,164 +53,276 @@ class _ProfilePageState extends State {
@override
Widget build(BuildContext context) {
+ final user = context.watch().user;
+
return Consumer(
builder: (context, state, child) => StateHandler(
onRetry: context.read().init,
state: context.read(),
builder: (context, state) => DidvanScaffold(
- padding: const EdgeInsets.all(16),
- appBarData: AppBarData(title: 'تنظیمات', hasBack: true),
+ padding: const EdgeInsets.fromLTRB(8, 20, 0, 20),
+ 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,
+ ),
+ ),
+ 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),
+ ],
+ ),
+ ],
children: [
- DidvanCard(
+ const SizedBox(height: 24),
+ Center(
child: Column(
+ mainAxisSize: MainAxisSize.min,
children: [
- MenuOption(
- title: 'ویرایش پروفایل',
- icon: DidvanIcons.user_edit_regular,
- suffix: context.watch().user.fullName,
- onTap: () => Navigator.of(context)
- .pushNamed(Routes.editProfile),
+ Container(
+ width: 100,
+ height: 100,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ color: Theme.of(context)
+ .colorScheme
+ .surfaceContainerHighest,
+ border: Border.all(
+ color: const Color.fromARGB(255, 184, 184, 184),
+ width: 1.0,
+ ),
+ image:
+ (user.photo != null && user.photo!.isNotEmpty)
+ ? DecorationImage(
+ image: NetworkImage(user.photo!),
+ fit: BoxFit.cover,
+ )
+ : null,
+ ),
+ alignment: Alignment.center,
+ child: (user.photo == null || user.photo!.isEmpty)
+ ? Icon(
+ Icons.person_rounded,
+ size: 60,
+ color: Theme.of(context)
+ .colorScheme
+ .onSurfaceVariant
+ .withOpacity(0.5),
+ )
+ : null,
),
- const DidvanDivider(),
- MenuOption(
- title: 'تنظیمات',
- icon: DidvanIcons.setting_regular,
- onTap: () {
- state.showSettings = !state.showSettings;
- state.update();
- },
- trailing: Icon(
- state.showSettings
- ? DidvanIcons.angle_up_regular
- : DidvanIcons.angle_down_regular,
- size: 18,
- color: Theme.of(context).colorScheme.title,
- )),
- AnimatedVisibility(
- duration: DesignConfig.lowAnimationDuration,
- isVisible: state.showSettings,
- child: Padding(
- padding: const EdgeInsets.only(right: 8.0, top: 8),
- child: Column(
- children: [
- // Padding(
- // padding: const EdgeInsets.symmetric(
- // vertical: 12.0),
- // child: MenuOption(
- // title: 'زمان دریافت اعلان',
- // onTap: () =>
- // Navigator.of(context).pushNamed(
- // Routes.notificationTime,
- // arguments: {
- // "fromFav": false,
- // 'onTimeChanged': () => Future.delayed(
- // Duration.zero,
- // () => state.getTime(),
- // )
- // },
- // ),
- // icon: DidvanIcons.notification_regular,
- // suffix: state.time,
- // // suffix: 'از${DateTimeUtils.normalizeTimeDuration(
- // // Duration(minutes: state.notificationTimeRange[0]),
- // // )} تا ${DateTimeUtils.normalizeTimeDuration(
- // // Duration(minutes: state.notificationTimeRange[1]),
- // // )}',
- // ),
- // ),
- // Padding(
- // padding: const EdgeInsets.symmetric(
- // vertical: 12.0),
- // child: MenuOption(
- // title: 'شخصی سازی محتوا',
- // onTap: () => Navigator.of(context)
- // .pushNamed(Routes.favouritesStep,
- // arguments: {"toTimer": false}),
- // icon: DidvanIcons.note_regular,
- // // suffix: 'از${DateTimeUtils.normalizeTimeDuration(
- // // Duration(minutes: state.notificationTimeRange[0]),
- // // )} تا ${DateTimeUtils.normalizeTimeDuration(
- // // Duration(minutes: state.notificationTimeRange[1]),
- // // )}',
- // ),
- // ),
- Padding(
- padding: const EdgeInsets.symmetric(
- vertical: 12.0),
- child: MenuOption(
- suffix: state.fontFamily == 'Dana-FA'
- ? 'دانا'
- : state.fontFamily == 'Iransans-FA'
- ? 'ایران سنس'
- : 'ایران یکان',
- title: 'فونت برنامه',
- onTap: _showFontFamilyBottomSheet,
- icon: DidvanIcons.font_regular,
- ),
- ),
- Padding(
+ const SizedBox(height: 4),
+ const SizedBox(height: 12),
+ // ویجت نمایش نام کاربر
+ DidvanText(
+ user.fullName,
+ style: Theme.of(context)
+ .textTheme
+ .titleLarge
+ ?.copyWith(
+ fontWeight: FontWeight.bold,
+ color:
+ const Color.fromARGB(255, 0, 126, 167)),
+ ),
+ // نمایش شماره تلفن (اختیاری)
+ // const SizedBox(height: 4),
+ // Directionality(
+ // textDirection: TextDirection.ltr,
+ // child: DidvanText(
+ // user.phoneNumber,
+ // style: Theme.of(context)
+ // .textTheme
+ // .bodyMedium
+ // ?.copyWith(
+ // color: Theme.of(context)
+ // .colorScheme
+ // .onSurface
+ // .withOpacity(0.6),
+ // ),
+ // ),
+ // ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 32),
+ Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: DidvanCard(
+ child: Column(
+ children: [
+ MenuOption(
+ title: 'ویرایش پروفایل',
+ icon: DidvanIcons.user_edit_regular,
+ // suffix: context.watch().user.fullName,
+ onTap: () => Navigator.of(context)
+ .pushNamed(Routes.editProfile),
+ ),
+ const DidvanDivider(),
+ // ... بقیه کدها بدون تغییر
+ MenuOption(
+ title: 'تنظیمات',
+ icon: DidvanIcons.setting_regular,
+ onTap: () {
+ state.showSettings = !state.showSettings;
+ state.update();
+ },
+ trailing: Icon(
+ state.showSettings
+ ? DidvanIcons.angle_up_regular
+ : DidvanIcons.angle_down_regular,
+ size: 18,
+ color: Theme.of(context).colorScheme.title,
+ )),
+ AnimatedVisibility(
+ duration: DesignConfig.lowAnimationDuration,
+ isVisible: state.showSettings,
+ child: Padding(
+ padding:
+ const EdgeInsets.only(right: 8.0, top: 8),
+ child: Column(
+ children: [
+ Padding(
padding: const EdgeInsets.symmetric(
vertical: 12.0),
child: MenuOption(
- suffix:
- fontScaleSuffix(state.fontSizeScale),
- title: 'اندازه متن',
- onTap: _showFontScaleBottomSheet,
- icon: DidvanIcons.font_size_solid,
- )),
- Padding(
- padding: const EdgeInsets.symmetric(
- vertical: 12.0),
- child: MenuOption(
- title: 'ظاهر برنامه',
- onTap: () {
- state.showThemes = !state.showThemes;
- state.update();
+ title: 'زمان دریافت اعلان',
+ onTap: () =>
+ Navigator.of(context).pushNamed(
+ Routes.notificationTime,
+ arguments: {
+ "fromFav": false,
+ 'onTimeChanged': () => Future.delayed(
+ Duration.zero,
+ () => state.getTime(),
+ )
},
- icon: DidvanIcons.theme_regular,
- trailing: Icon(
- state.showThemes
- ? DidvanIcons.angle_up_regular
- : DidvanIcons.angle_down_regular,
- size: 18,
- color: Theme.of(context)
- .colorScheme
- .title,
- ))),
- AnimatedVisibility(
- duration: DesignConfig.lowAnimationDuration,
- isVisible: state.showThemes,
- child: Padding(
- padding: const EdgeInsets.symmetric(
- vertical: 12.0),
- child: Row(
- mainAxisAlignment:
- MainAxisAlignment.spaceAround,
- children: [
- _themeItem(context, state, 'light'),
- _themeItem(context, state, 'dark'),
- ],
+ ),
+ icon: DidvanIcons.notification_regular,
+ suffix: state.time,
+ // suffix:
+ // 'از${DateTimeUtils.normalizeTimeDuration(
+ // Duration(
+ // minutes:
+ // state.notificationTimeRange[0]),
+ // )} تا ${DateTimeUtils.normalizeTimeDuration(
+ // Duration(
+ // minutes:
+ // state.notificationTimeRange[1]),
+ // )}',
),
),
- ),
- ],
+ Padding(
+ padding: const EdgeInsets.symmetric(
+ vertical: 12.0),
+ child: MenuOption(
+ title: 'شخصی سازی محتوا',
+ onTap: () => Navigator.of(context)
+ .pushNamed(Routes.favouritesStep,
+ arguments: {"toTimer": false}),
+ icon: DidvanIcons.note_regular,
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(
+ vertical: 12.0),
+ child: MenuOption(
+ suffix: state.fontFamily == 'Dana-FA'
+ ? 'دانا'
+ : state.fontFamily == 'Iransans-FA'
+ ? 'ایران سنس'
+ : 'ایران یکان',
+ title: 'فونت برنامه',
+ onTap: _showFontFamilyBottomSheet,
+ icon: DidvanIcons.font_regular,
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(
+ vertical: 12.0),
+ child: MenuOption(
+ suffix: fontScaleSuffix(
+ state.fontSizeScale),
+ title: 'اندازه متن',
+ onTap: _showFontScaleBottomSheet,
+ icon: DidvanIcons.font_size_solid,
+ )),
+ Padding(
+ padding: const EdgeInsets.symmetric(
+ vertical: 12.0),
+ child: MenuOption(
+ title: 'ظاهر برنامه',
+ onTap: () {
+ state.showThemes =
+ !state.showThemes;
+ state.update();
+ },
+ icon: DidvanIcons.theme_regular,
+ trailing: Icon(
+ state.showThemes
+ ? DidvanIcons.angle_up_regular
+ : DidvanIcons
+ .angle_down_regular,
+ size: 18,
+ color: Theme.of(context)
+ .colorScheme
+ .title,
+ ))),
+ AnimatedVisibility(
+ duration: DesignConfig.lowAnimationDuration,
+ isVisible: state.showThemes,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(
+ vertical: 12.0),
+ child: Row(
+ mainAxisAlignment:
+ MainAxisAlignment.spaceAround,
+ children: [
+ _themeItem(context, state, 'light'),
+ _themeItem(context, state, 'dark'),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
),
),
- ),
- const DidvanDivider(),
- MenuOption(
- title: 'خروج از حساب کاربری',
- icon: DidvanIcons.sign_out_regular,
- color: Theme.of(context).colorScheme.secondary,
- onTap: () async {
- StorageService.delete(key: 'token');
- Navigator.of(context).pushNamedAndRemoveUntil(
- Routes.splash,
- (_) => false,
- );
- },
- ),
- ],
+ const DidvanDivider(),
+ MenuOption(
+ title: 'خروج از حساب کاربری',
+ icon: DidvanIcons.sign_out_regular,
+ color: Theme.of(context).colorScheme.secondary,
+ onTap: () async {
+ StorageService.delete(key: 'token');
+ Navigator.of(context).pushNamedAndRemoveUntil(
+ Routes.splash,
+ (_) => false,
+ );
+ },
+ ),
+ ],
+ ),
),
),
const ItemTitle(
diff --git a/lib/views/radar/radar.dart b/lib/views/radar/radar.dart
index 21b93a1..248fafb 100644
--- a/lib/views/radar/radar.dart
+++ b/lib/views/radar/radar.dart
@@ -1,231 +1,341 @@
import 'dart:async';
import 'dart:math';
-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/category.dart';
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/requests/radar.dart';
import 'package:didvan/models/view/action_sheet_data.dart';
-import 'package:didvan/models/view/app_bar_data.dart';
import 'package:didvan/views/radar/radar_state.dart';
-import 'package:didvan/views/widgets/categories_gird.dart';
-import 'package:didvan/views/widgets/categories_list.dart';
import 'package:didvan/views/widgets/date_picker_button.dart';
import 'package:didvan/views/widgets/didvan/scaffold.dart';
import 'package:didvan/utils/action_sheet.dart';
import 'package:didvan/views/widgets/overview/radar.dart';
-import 'package:didvan/views/widgets/search_field.dart';
-import 'package:didvan/views/widgets/animated_visibility.dart';
import 'package:didvan/views/widgets/didvan/checkbox.dart';
-import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/item_title.dart';
import 'package:didvan/views/widgets/state_handlers/empty_result.dart';
import 'package:didvan/views/widgets/state_handlers/sliver_state_handler.dart';
+import 'package:didvan/views/widgets/home_app_bar.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart';
class Radar extends StatefulWidget {
const Radar({Key? key}) : super(key: key);
@override
- State createState() => _RadarState();
+ State createState() => _RadarStateView();
}
-class _RadarState extends State {
- final ScrollController _scrollController = ScrollController();
- final _focusNode = FocusNode();
-
- bool _isAnimating = false;
-
+class _RadarStateView extends State {
Timer? _timer;
+ final _focusNode = FocusNode();
@override
void initState() {
- _scrollController.addListener(() {
- _handleAnimations();
- });
- final state = context.read();
- state.addListener(() {
- if (state.shouldColapse && mounted) {
- _handleAnimations(true);
- state.shouldColapse = false;
- }
- });
- state.init();
+ context.read().init();
super.initState();
}
+ String _getCategoryIcon(int categoryId, bool isDark) {
+ switch (categoryId) {
+ case 1:
+ return 'lib/assets/icons/New_Economic.svg';
+ case 2:
+ return 'lib/assets/icons/New_Siasi.svg';
+ case 3:
+ return 'lib/assets/icons/New_Fanavari.svg';
+ case 4:
+ return 'lib/assets/icons/New_Kasbokar.svg';
+ case 5:
+ return 'lib/assets/icons/New_ZistMohit.svg';
+ case 6:
+ return 'lib/assets/icons/New_Ejtemaei.svg';
+ default:
+ return 'lib/assets/icons/Kasb o Kar.svg';
+ }
+ }
+
@override
Widget build(BuildContext context) {
- return Consumer(
- builder: (context, state, child) => DidvanScaffold(
- padding: EdgeInsets.zero,
- appBarData: AppBarData(
- hasBack: true,
- hasElevation: false,
- title: 'پویشهای افق',
+ final state = context.watch();
+ final theme = Theme.of(context);
+ final colorScheme = theme.colorScheme;
+ final isDark = theme.brightness == Brightness.dark;
+ final showCategories = !state.filtering && !state.searching;
+
+ return DidvanScaffold(
+ padding: EdgeInsets.zero,
+ appBarData: null,
+ slivers: [
+ SliverToBoxAdapter(
+ child: Padding(
+ padding: const EdgeInsets.only(top: 12.0),
+ child: HomeAppBar(
+ showBackButton: false,
+ showSearchField: state.appState != AppState.failed,
+ onSearchChanged: _onChanged,
+ onFilterPressed: _showFilterBottomSheet,
+ searchFocusNode: _focusNode,
+ isFiltered: state.filtering,
+ searchValue: state.search,
+ ),
+ ),
),
- children: [
- SizedBox(
- height: MediaQuery.of(context).size.height,
- child: Stack(
- children: [
- CustomScrollView(
- physics: _isAnimating
- ? const NeverScrollableScrollPhysics()
- : const ClampingScrollPhysics(),
- controller: _scrollController,
- slivers: [
- if (!state.filtering &&
- !state.searching &&
- state.appState != AppState.failed)
- const SliverToBoxAdapter(
- child: SizedBox(height: 320),
- ),
- if (state.appState != AppState.failed)
- SliverPadding(
- padding: const EdgeInsets.only(
- right: 16, bottom: 20, top: 20),
- sliver: SliverToBoxAdapter(
- child: Align(
- alignment: Alignment.centerRight,
- child: AnimatedVisibility(
- isVisible: !state.isColapsed &&
- !state.searching &&
- !state.filtering,
- duration: DesignConfig.lowAnimationDuration,
- child: DidvanText(
- 'آخرین رصد',
- style: Theme.of(context).textTheme.titleMedium,
- color: Theme.of(context).colorScheme.title,
- ),
- ),
- ),
- ),
- ),
- if (state.isColapsed || state.searching || state.filtering)
- const SliverToBoxAdapter(
- child: SizedBox(
- height: 160,
- ),
- ),
- SliverStateHandler(
- onRetry: () => state.getRadars(page: state.page),
- state: state,
- itemPadding: const EdgeInsets.only(
- bottom: 20,
- left: 16,
- right: 16,
- ),
- enableEmptyState: state.radars.isEmpty,
- emptyState: Padding(
- padding: const EdgeInsets.only(bottom: 120),
- child: EmptyResult(
- onNewSearch: () => _focusNode.requestFocus()),
- ),
- placeholder: RadarOverview.placeholder,
- builder: (context, state, index) {
- index += 2;
- if (index % 15 == 0 && state.lastPage != state.page) {
- state.getRadars(page: state.page + 1);
- }
- index -= 2;
- if (index >= state.radars.length) {
- return RadarOverview.placeholder;
- }
- final radar = state.radars[index];
- return RadarOverview(
- radar: radar,
- onMarkChanged: state.changeMark,
- onLikedChanged: state.changeLike,
- onCommentsChanged: (id, count) =>
- state.onCommentsChanged(id, count),
- radarRequestArgs: RadarRequestArgs(
- page: state.page,
- categories: List.from(
- state.selectedCats.map((cat) => cat.id)),
- endDate: state.endDate,
- isSingleItem: false,
- search: state.search,
- startDate: state.startDate,
- ),
- );
- },
- childCount: state.radars.length +
- (state.lastPage == state.page ? 0 : 3),
- ),
- if (state.radars.length == 1)
- const SliverToBoxAdapter(
- child: SizedBox(height: 320),
- ),
- const SliverToBoxAdapter(
- child: SizedBox(
- height: 100,
- ),
- ),
- ],
+ SliverAppBar(
+ pinned: true,
+ automaticallyImplyLeading: false,
+ backgroundColor: colorScheme.surface,
+ elevation: 0,
+ centerTitle: false,
+ titleSpacing: 16,
+ title: Text(
+ 'پویش افق',
+ style: theme.textTheme.headlineSmall?.copyWith(
+ color: isDark
+ ? Colors.white
+ : const Color.fromARGB(255, 0, 53, 70),
+ fontWeight: FontWeight.bold,
+ fontSize: 19),
+ ),
+ actions: [
+ IconButton(
+ onPressed: () => Navigator.of(context).pop(),
+ icon: SvgPicture.asset(
+ 'lib/assets/icons/arrow-left.svg',
+ width: 30,
+ height: 30,
+ colorFilter: ColorFilter.mode(
+ isDark
+ ? Colors.white70
+ : const Color.fromARGB(255, 102, 102, 102),
+ BlendMode.srcIn),
+ ),
+ ),
+ const SizedBox(width: 8),
+ ],
+ ),
+ const SliverToBoxAdapter(
+ child: SizedBox(
+ height: 16,
+ ),
+ ),
+ SliverToBoxAdapter(
+ child: AnimatedSwitcher(
+ duration: const Duration(milliseconds: 500),
+ switchInCurve: Curves.easeOutCubic,
+ switchOutCurve: Curves.easeInCubic,
+ transitionBuilder: (Widget child, Animation animation) {
+ return SizeTransition(
+ sizeFactor: animation,
+ axisAlignment: -1.0,
+ child: FadeTransition(
+ opacity: animation,
+ child: child,
),
- if (state.appState != AppState.failed)
- CategoriesRow1(
- topPadding: 212,
- rightPadding: 124,
- onSelected: _onCategorySelected,
- categories: state.categories,
- isColapsed:
- state.isColapsed || state.searching || state.filtering,
- ),
- if (state.appState != AppState.failed)
- CategoriesRow2(
- categories: state.categories,
- isColapsed:
- state.isColapsed || state.searching || state.filtering,
- onSelected: _onCategorySelected,
- ),
- if (state.appState != AppState.failed &&
- !state.searching &&
- !state.filtering)
- CategoriesList(
- top: 60,
- categories: state.categories,
- isColapsed:
- state.isColapsed || state.searching || state.filtering,
- onSelected: (_) => state.getRadars(page: 1),
- selectedCats: state.selectedCats,
- ),
- if (state.appState != AppState.failed)
- Container(
- padding: const EdgeInsets.all(16),
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.surface,
- borderRadius: const BorderRadius.vertical(
- bottom: Radius.circular(12),
- ),
- ),
- child: SearchField(
- focusNode: _focusNode,
- isFiltered: state.filtering,
- title: 'پویشهای افق',
- onChanged: _onChanged,
- onFilterButtonPressed: _showFilterBottomSheet,
- ),
+ );
+ },
+ child: showCategories
+ ? Column(
+ key: const ValueKey('categories_column'),
+ children: [
+ if (state.categories.length >= 3)
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: state.categories
+ .sublist(0, 3)
+ .map((category) => _buildCoolCategoryItem(
+ category, isDark, state, theme))
+ .toList(),
+ ),
+ ),
+ if (state.categories.length >= 6)
+ Padding(
+ padding: const EdgeInsets.only(
+ top: 12, left: 20, right: 20),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: state.categories
+ .sublist(3, 6)
+ .map((category) => _buildCoolCategoryItem(
+ category, isDark, state, theme))
+ .toList(),
+ ),
+ ),
+ const SizedBox(height: 20),
+ ],
+ )
+ : const SizedBox.shrink(key: ValueKey('empty_categories')),
+ ),
+ ),
+ SliverStateHandler(
+ centerEmptyState: false,
+ onRetry: () => state.getRadars(page: state.page),
+ state: state,
+ childCount: min(state.visibleCount, state.radars.length) +
+ (_hasMoreItems(state) ? 1 : 0),
+ builder: (context, state, index) {
+ final currentDisplayCount =
+ min(state.visibleCount, state.radars.length);
+
+ if (index == currentDisplayCount && _hasMoreItems(state)) {
+ return _buildLoadMoreButton(context, state);
+ }
+
+ if (index >= state.radars.length) {
+ return _SlideFadeTransition(
+ index: index, child: RadarOverview.placeholder);
+ }
+
+ final radar = state.radars[index];
+ return _SlideFadeTransition(
+ index: index,
+ child: RadarOverview(
+ radar: radar,
+ onMarkChanged: state.changeMark,
+ onLikedChanged: state.changeLike,
+ onCommentsChanged: (id, count) =>
+ state.onCommentsChanged(id, count),
+ radarRequestArgs: RadarRequestArgs(
+ page: state.page,
+ categories:
+ List.from(state.selectedCats.map((cat) => cat.id)),
+ endDate: state.endDate,
+ isSingleItem: false,
+ search: state.search,
+ startDate: state.startDate,
+ ),
+ ),
+ );
+ },
+ enableEmptyState:
+ state.radars.isEmpty && state.appState != AppState.busy,
+ emptyState: EmptyResult(
+ onNewSearch: () => _focusNode.requestFocus(),
+ ),
+ itemPadding: const EdgeInsets.only(
+ bottom: 20,
+ left: 16,
+ right: 16,
+ ),
+ placeholder:
+ _SlideFadeTransition(index: 0, child: RadarOverview.placeholder),
+ ),
+ ],
+ );
+ }
+
+ bool _hasMoreItems(RadarState state) {
+ return state.radars.length > state.visibleCount ||
+ state.page < state.lastPage;
+ }
+
+ Widget _buildLoadMoreButton(BuildContext context, RadarState state) {
+ if (state.appState == AppState.busy &&
+ state.radars.length <= state.visibleCount) {
+ return const Padding(
+ padding: EdgeInsets.all(16.0),
+ child: Center(child: CircularProgressIndicator()),
+ );
+ }
+
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0),
+ child: Center(
+ child: GestureDetector(
+ onTap: () {
+ state.loadMore();
+ },
+ child: Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 50.0,
+ vertical: 12.0,
+ ),
+ decoration: BoxDecoration(
+ color: const Color.fromARGB(255, 0, 126, 167),
+ borderRadius: BorderRadius.circular(15),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ SvgPicture.asset(
+ 'lib/assets/icons/element-plus.svg',
+ colorFilter:
+ const ColorFilter.mode(Colors.white, BlendMode.srcIn),
+ width: 20,
+ ),
+ const SizedBox(width: 8),
+ const Text(
+ 'بارگذاری بیشتر',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 14,
+ fontWeight: FontWeight.bold,
),
+ ),
],
),
),
- ],
+ ),
),
);
}
- void _onCategorySelected(CategoryData category) {
- final state = context.read();
- state.selectedCats.clear();
- if (category.id != 0) {
- state.selectedCats.add(category);
- }
- state.getRadars(page: 1);
+ Widget _buildCoolCategoryItem(
+ CategoryData category, bool isDark, RadarState state, ThemeData theme) {
+ final isSelected =
+ state.selectedCats.any((element) => element.id == category.id);
+ category.asset = _getCategoryIcon(category.id, isDark);
+
+ return Expanded(
+ child: GestureDetector(
+ onTap: () => _onCategorySelected(category),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ AnimatedContainer(
+ duration: const Duration(milliseconds: 300),
+ curve: Curves.fastOutSlowIn,
+ padding: EdgeInsets.zero,
+ decoration: BoxDecoration(
+ color: isSelected
+ ? theme.colorScheme.primary.withOpacity(0.1)
+ : Colors.transparent,
+ borderRadius: BorderRadius.circular(30),
+ border: Border.all(
+ color: isSelected
+ ? theme.colorScheme.primary
+ : Colors.transparent,
+ width: 2,
+ ),
+ ),
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(30),
+ child: SvgPicture.asset(
+ category.asset!,
+ width: 100,
+ height: 100,
+ ),
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ category.label,
+ textAlign: TextAlign.center,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ style: theme.textTheme.bodySmall?.copyWith(
+ color: theme.colorScheme.primary,
+ fontWeight: isSelected ? FontWeight.bold : FontWeight.bold,
+ fontSize: 15,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
}
void _onChanged(String value) {
@@ -240,35 +350,19 @@ class _RadarState extends State {
});
}
- void _handleAnimations([bool forceAnimate = false]) async {
+ void _onCategorySelected(CategoryData category) {
final state = context.read();
- if (_isAnimating || state.filtering || state.searching) return;
- final double position = _scrollController.offset;
- if (position > 5 && !state.isColapsed || forceAnimate) {
- state.isScrolled = true;
- _isAnimating = true;
- setState(() {});
- await _scrollController.animateTo(
- 380,
- duration: DesignConfig.mediumAnimationDuration,
- curve: Curves.easeIn,
- );
- _isAnimating = false;
- setState(() {});
- } else if (position <
- min(_scrollController.position.maxScrollExtent, 380) &&
- state.isColapsed) {
- state.isScrolled = false;
- _isAnimating = true;
- setState(() {});
- await _scrollController.animateTo(
- 0,
- duration: DesignConfig.mediumAnimationDuration,
- curve: Curves.easeIn,
- );
- _isAnimating = false;
- setState(() {});
+
+ final isAlreadySelected =
+ state.selectedCats.any((cat) => cat.id == category.id);
+
+ state.selectedCats.clear();
+
+ if (!isAlreadySelected && category.id != 0) {
+ state.selectedCats.add(category);
}
+
+ state.getRadars(page: 1);
}
Future _showFilterBottomSheet() async {
@@ -324,13 +418,15 @@ class _RadarState extends State {
width: (MediaQuery.of(context).size.width - 40) / 2,
child: DidvanCheckbox(
title: state.categories[i].label,
- value: state.selectedCats.contains(state.categories[i]),
+ value: state.selectedCats
+ .any((cat) => cat.id == state.categories[i].id),
onChanged: (value) {
if (value) {
state.selectedCats.add(state.categories[i]);
- return;
+ } else {
+ state.selectedCats.removeWhere(
+ (cat) => cat.id == state.categories[i].id);
}
- state.selectedCats.remove(state.categories[i]);
},
),
),
@@ -345,7 +441,64 @@ class _RadarState extends State {
@override
void dispose() {
_focusNode.dispose();
- _scrollController.dispose();
super.dispose();
}
}
+
+class _SlideFadeTransition extends StatefulWidget {
+ final Widget child;
+ final int index;
+
+ const _SlideFadeTransition({
+ Key? key,
+ required this.child,
+ required this.index,
+ }) : super(key: key);
+
+ @override
+ State<_SlideFadeTransition> createState() => _SlideFadeTransitionState();
+}
+
+class _SlideFadeTransitionState extends State<_SlideFadeTransition>
+ with SingleTickerProviderStateMixin {
+ late final AnimationController _controller;
+ late final Animation _opacity;
+ late final Animation _offset;
+
+ @override
+ void initState() {
+ super.initState();
+ _controller = AnimationController(
+ vsync: this,
+ duration: const Duration(milliseconds: 600),
+ );
+
+ _opacity = CurvedAnimation(parent: _controller, curve: Curves.easeOut);
+ _offset = Tween(begin: const Offset(0, 0.1), end: Offset.zero)
+ .animate(
+ CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic));
+
+ Future.delayed(Duration(milliseconds: 50 * (widget.index % 5)), () {
+ if (mounted) {
+ _controller.forward();
+ }
+ });
+ }
+
+ @override
+ void dispose() {
+ _controller.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return FadeTransition(
+ opacity: _opacity,
+ child: SlideTransition(
+ position: _offset,
+ child: widget.child,
+ ),
+ );
+ }
+}
diff --git a/lib/views/radar/radar_details/radar_details.dart b/lib/views/radar/radar_details/radar_details.dart
index d4c14b5..49e2d9c 100644
--- a/lib/views/radar/radar_details/radar_details.dart
+++ b/lib/views/radar/radar_details/radar_details.dart
@@ -29,6 +29,7 @@ class _RadarDetailsState extends State {
}
});
state.args = widget.pageData['args'] ?? const RadarRequestArgs(page: 0);
+ state.initialDescription = widget.pageData['description'];
Future.delayed(Duration.zero, () {
state.getRadarDetails(widget.pageData['id']);
});
diff --git a/lib/views/radar/radar_details/radar_details_state.dart b/lib/views/radar/radar_details/radar_details_state.dart
index ef3e3f8..705d243 100644
--- a/lib/views/radar/radar_details/radar_details_state.dart
+++ b/lib/views/radar/radar_details/radar_details_state.dart
@@ -15,6 +15,7 @@ class RadarDetailsState extends CoreProvier {
int _trackingTimerCounter = 0;
late final int initialIndex;
late final RadarRequestArgs args;
+ String? initialDescription;
bool isFetchingNewItem = false;
final List relatedQueue = [];
bool openComments = false;
@@ -32,13 +33,11 @@ class RadarDetailsState extends CoreProvier {
}
Future getRadarDetails(int id, {bool? isForward}) async {
- // Prevent duplicate requests
if (_isRequestInProgress) {
return;
}
_isRequestInProgress = true;
- // Set loading state
if (isForward == null) {
appState = AppState.busy;
} else {
@@ -47,7 +46,6 @@ class RadarDetailsState extends CoreProvier {
}
try {
- // Wait for token to be ready
if (RequestService.token == null || RequestService.token!.isEmpty) {
await Future.delayed(const Duration(milliseconds: 500));
if (RequestService.token == null || RequestService.token!.isEmpty) {
@@ -62,6 +60,15 @@ class RadarDetailsState extends CoreProvier {
if (service.isSuccess) {
final result = service.result;
final radar = RadarDetailsData.fromJson(result['radar']);
+
+ if (isForward == null && initialDescription != null) {
+ radar.description = initialDescription;
+ }
+
+ if (isForward == null && radar.description == null) {
+ await _fetchDescriptionFromList(radar.id, radar);
+ }
+
if (args.page == 0) {
radars.add(radar);
initialIndex = 0;
@@ -82,7 +89,8 @@ class RadarDetailsState extends CoreProvier {
}
if (isForward == null) {
- radars.addAll(List.generate(max(radar.order - 2, 0), (index) => null));
+ radars
+ .addAll(List.generate(max(radar.order - 2, 0), (index) => null));
if (prevRadar != null) {
radars.add(prevRadar);
}
@@ -127,6 +135,36 @@ class RadarDetailsState extends CoreProvier {
}
}
+ Future _fetchDescriptionFromList(
+ int radarId, RadarDetailsData radarItem) async {
+ try {
+ final service = RequestService(
+ RequestHelper.radarOverviews(
+ args: const RadarRequestArgs(page: 1),
+ ),
+ );
+ await service.httpGet();
+
+ if (service.isSuccess) {
+ final radarList = service.result['radars'] as List?;
+ if (radarList != null) {
+ for (var item in radarList) {
+ if (item['id'] == radarId) {
+ final radarData = OverviewData.fromJson(item);
+ if (radarData.description.isNotEmpty) {
+ radarItem.description = radarData.description;
+ }
+ break;
+ }
+ }
+ }
+ }
+ } catch (e) {
+ // ignore: avoid_print
+ print('Error fetching description from list: $e');
+ }
+ }
+
Future getRelatedContents() async {
if (currentRadar.relatedContents.isNotEmpty &&
currentRadar.relatedContentsIsEmpty) {
@@ -197,4 +235,4 @@ class RadarDetailsState extends CoreProvier {
_trackingTimer.cancel();
super.dispose();
}
-}
\ No newline at end of file
+}
diff --git a/lib/views/radar/radar_state.dart b/lib/views/radar/radar_state.dart
index d30a0e3..a8b7c84 100644
--- a/lib/views/radar/radar_state.dart
+++ b/lib/views/radar/radar_state.dart
@@ -14,6 +14,8 @@ class RadarState extends CoreProvier {
String? endDate;
int page = 1;
int lastPage = 1;
+ int visibleCount = 4;
+
bool isScrolled = false;
bool shouldColapse = false;
final List selectedCats = [];
@@ -37,11 +39,20 @@ class RadarState extends CoreProvier {
search = '';
lastSearch = '';
isScrolled = false;
+ visibleCount = 4;
if (!isInit) {
getRadars(page: 1);
}
}
+ void loadMore() {
+ visibleCount += 4;
+ if (visibleCount >= radars.length && page < lastPage) {
+ getRadars(page: page + 1);
+ }
+ notifyListeners();
+ }
+
Future getRadars({
required int page,
}) async {
diff --git a/lib/views/widgets/ai_chat_dialog.dart b/lib/views/widgets/ai_chat_dialog.dart
index 736d16e..acb5960 100644
--- a/lib/views/widgets/ai_chat_dialog.dart
+++ b/lib/views/widgets/ai_chat_dialog.dart
@@ -1,5 +1,8 @@
+// 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/services/network/request.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/providers/user.dart';
import 'package:flutter/material.dart';
@@ -8,6 +11,7 @@ import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:provider/provider.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';
@@ -28,7 +32,10 @@ class _AiChatDialogState extends State
late AnimationController _animationController;
late AnimationController _pulseController;
final AudioRecorder _audioRecorder = AudioRecorder();
+ final AudioPlayer _audioPlayer = AudioPlayer();
String? _recordingPath;
+ String? _currentPlayingUrl;
+ bool _isPlaying = false;
@override
void initState() {
@@ -37,6 +44,18 @@ class _AiChatDialogState extends State
vsync: this,
duration: const Duration(milliseconds: 400),
);
+
+ _audioPlayer.playerStateStream.listen((state) {
+ if (mounted) {
+ setState(() {
+ _isPlaying = state.playing;
+ if (state.processingState == ProcessingState.completed) {
+ _isPlaying = false;
+ _currentPlayingUrl = null;
+ }
+ });
+ }
+ });
_pulseController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
@@ -66,6 +85,7 @@ class _AiChatDialogState extends State
_animationController.dispose();
_pulseController.dispose();
_audioRecorder.dispose();
+ _audioPlayer.dispose();
super.dispose();
}
@@ -81,6 +101,68 @@ class _AiChatDialogState extends State
});
}
+ Future _toggleAudioPlayback(String audioUrl) async {
+ try {
+ if (_isPlaying && _currentPlayingUrl == audioUrl) {
+ await _audioPlayer.pause();
+ setState(() {
+ _isPlaying = false;
+ });
+ } else {
+ if (_currentPlayingUrl != audioUrl) {
+ final token = RequestService.token;
+ debugPrint('🎵 Audio URL: $audioUrl');
+ debugPrint('🔑 Token exists: ${token != null && token.isNotEmpty}');
+
+ AudioSource? audioSource;
+
+ try {
+ if (token != null && token.isNotEmpty) {
+ audioSource = AudioSource.uri(
+ Uri.parse(audioUrl),
+ headers: {
+ 'Authorization': 'Bearer $token',
+ },
+ );
+ debugPrint('✅ Trying with token (no Bearer prefix)');
+ } else {
+ audioSource = AudioSource.uri(
+ Uri.parse(audioUrl),
+ headers: {
+ 'Authorization': 'Bearer $token',
+ },
+ );
+ debugPrint('✅ Trying without Authorization header');
+ }
+
+ await _audioPlayer.setAudioSource(audioSource);
+ setState(() {
+ _currentPlayingUrl = audioUrl;
+ });
+ } catch (headerError) {
+ debugPrint(
+ '⚠️ Error with headers, trying simple URL: $headerError');
+ await _audioPlayer.setUrl(audioUrl);
+ setState(() {
+ _currentPlayingUrl = audioUrl;
+ });
+ }
+ }
+ await _audioPlayer.play();
+ setState(() {
+ _isPlaying = true;
+ });
+ }
+ } catch (e) {
+ debugPrint('❌ Error playing audio: $e');
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('خطا در پخش صوت')),
+ );
+ }
+ }
+ }
+
Future _sendMessage() async {
final message = _messageController.text.trim();
if (message.isEmpty) return;
@@ -105,6 +187,7 @@ class _AiChatDialogState extends State
isUser: false,
timestamp: DateTime.now(),
sources: response.sources,
+ audioUrl: response.audioUrl,
));
_isLoading = false;
});
@@ -171,6 +254,7 @@ class _AiChatDialogState extends State
isUser: false,
timestamp: DateTime.now(),
sources: ragResponse.sources,
+ audioUrl: ragResponse.audioUrl,
));
_isLoading = false;
});
@@ -310,7 +394,6 @@ class _AiChatDialogState extends State
);
},
),
- // Avatar
Container(
width: 44,
height: 44,
@@ -334,7 +417,6 @@ class _AiChatDialogState extends State
),
),
),
- // Online indicator
Positioned(
bottom: 1,
left: 1,
@@ -550,6 +632,67 @@ class _AiChatDialogState extends State
),
),
],
+ if (!message.isUser &&
+ message.audioUrl != null &&
+ message.audioUrl!.isNotEmpty) ...[
+ const SizedBox(height: 8),
+ InkWell(
+ onTap: () => _toggleAudioPlayback(message.audioUrl!),
+ borderRadius: BorderRadius.circular(20),
+ child: Container(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12, vertical: 8),
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ colors: [
+ const Color(0xFF0066AA).withOpacity(0.1),
+ const Color(0xFF0088DD).withOpacity(0.08),
+ ],
+ ),
+ borderRadius: BorderRadius.circular(20),
+ border: Border.all(
+ color: const Color(0xFF0066AA).withOpacity(0.3),
+ width: 1,
+ ),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ padding: const EdgeInsets.all(4),
+ decoration: const BoxDecoration(
+ gradient: LinearGradient(
+ colors: [
+ Color(0xFF0066AA),
+ Color(0xFF0088DD),
+ ],
+ ),
+ shape: BoxShape.circle,
+ ),
+ child: Icon(
+ (_isPlaying &&
+ _currentPlayingUrl == message.audioUrl)
+ ? Icons.pause_rounded
+ : Icons.play_arrow_rounded,
+ size: 16,
+ color: Colors.white,
+ ),
+ ),
+ const SizedBox(width: 8),
+ DidvanText(
+ (_isPlaying &&
+ _currentPlayingUrl == message.audioUrl)
+ ? 'در حال پخش...'
+ : 'پخش صوتی پاسخ',
+ fontSize: 12,
+ color: const Color(0xFF0066AA),
+ fontWeight: FontWeight.w600,
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
],
),
),
@@ -686,7 +829,7 @@ class _AiChatDialogState extends State
mainAxisSize: MainAxisSize.min,
children: [
Text(
- 'در حال تایپ...',
+ 'در حال فکر کردن...',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 12,
@@ -917,11 +1060,13 @@ class ChatMessage {
final bool isUser;
final DateTime timestamp;
final List sources;
+ final String? audioUrl;
ChatMessage({
required this.text,
required this.isUser,
required this.timestamp,
this.sources = const [],
+ this.audioUrl,
});
}
diff --git a/lib/views/widgets/category_item.dart b/lib/views/widgets/category_item.dart
index a192a76..c3f7427 100644
--- a/lib/views/widgets/category_item.dart
+++ b/lib/views/widgets/category_item.dart
@@ -1,5 +1,4 @@
import 'package:didvan/config/design_config.dart';
-import 'package:didvan/config/theme_data.dart';
import 'package:didvan/models/category.dart';
import 'package:didvan/views/widgets/animated_visibility.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
@@ -26,7 +25,7 @@ class CategoryItem extends StatelessWidget {
return ds.height / 16 * 9 / 3;
}
}
- return ds.width / 3;
+ return ds.width / 2;
}
bool _useWebMobileLayout(context) {
@@ -44,14 +43,14 @@ class CategoryItem extends StatelessWidget {
child: AnimatedContainer(
duration: DesignConfig.mediumAnimationDuration,
padding: isColapsed ? const EdgeInsets.all(4) : EdgeInsets.zero,
- width: isColapsed ? 100 : _width(context),
+ width: isColapsed ? 200 : _width(context),
alignment: Alignment.center,
- decoration: BoxDecoration(
- borderRadius: DesignConfig.lowBorderRadius,
- border: isColapsed
- ? Border.all(color: Theme.of(context).colorScheme.focusedBorder)
- : null,
- ),
+ // decoration: BoxDecoration(
+ // borderRadius: DesignConfig.lowBorderRadius,
+ // border: isColapsed
+ // ? Border.all(color: Theme.of(context).colorScheme.focusedBorder)
+ // : null,
+ // ),
child: Column(
children: [
AnimatedVisibility(
@@ -64,13 +63,16 @@ class CategoryItem extends StatelessWidget {
height: !_useWebMobileLayout(context)
? _width(context) / 2
: ds.width / 7,
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.surface,
- boxShadow: DesignConfig.defaultShadow,
- borderRadius: DesignConfig.mediumBorderRadius,
- ),
+ // decoration: BoxDecoration(
+ // color: Theme.of(context).colorScheme.surface,
+ // boxShadow: DesignConfig.defaultShadow,
+ // borderRadius: DesignConfig.mediumBorderRadius,
+ // ),
padding: const EdgeInsets.all(8),
- child: SvgPicture.asset(category.asset!),
+ child: SvgPicture.asset(
+ category.asset!,
+ height: 50,
+ ),
),
),
const SizedBox(
@@ -78,7 +80,7 @@ class CategoryItem extends StatelessWidget {
),
DidvanText(
category.label,
- color: Theme.of(context).colorScheme.title,
+ color: const Color.fromARGB(255, 25, 93, 128),
style: Theme.of(context).textTheme.titleSmall,
),
],
diff --git a/lib/views/widgets/didvan/icon_button.dart b/lib/views/widgets/didvan/icon_button.dart
index 774f44a..4064021 100644
--- a/lib/views/widgets/didvan/icon_button.dart
+++ b/lib/views/widgets/didvan/icon_button.dart
@@ -1,13 +1,16 @@
import 'package:didvan/views/widgets/ink_wrapper.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
class DidvanIconButton extends StatelessWidget {
- final IconData icon;
+ final dynamic icon;
final Color? color;
final Color? backgroundColor;
final double? size;
final double? gestureSize;
final VoidCallback onPressed;
+ final bool isSvg;
+
const DidvanIconButton({
Key? key,
required this.icon,
@@ -16,6 +19,7 @@ class DidvanIconButton extends StatelessWidget {
this.size,
this.gestureSize,
this.backgroundColor,
+ this.isSvg = false,
}) : super(key: key);
@override
@@ -30,12 +34,22 @@ class DidvanIconButton extends StatelessWidget {
),
height: gestureSize ?? 48,
width: gestureSize ?? 48,
- child: Icon(
- icon,
- size: size,
- color: color,
+ child: Center(
+ child: isSvg
+ ? SvgPicture.asset(
+ icon as String,
+ width: size,
+ height: size,
+ colorFilter: color != null
+ ? ColorFilter.mode(color!, BlendMode.srcIn)
+ : null,
+ )
+ : Icon(
+ icon as IconData,
+ size: size,
+ color: color,
+ ),
),
-
),
);
}
diff --git a/lib/views/widgets/didvan/page_view.dart b/lib/views/widgets/didvan/page_view.dart
index c762cba..8d20b0a 100644
--- a/lib/views/widgets/didvan/page_view.dart
+++ b/lib/views/widgets/didvan/page_view.dart
@@ -1,5 +1,6 @@
// ignore_for_file: deprecated_member_use
+import 'dart:async';
import 'dart:math';
import 'package:carousel_slider/carousel_slider.dart';
@@ -9,14 +10,11 @@ import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/main.dart';
import 'package:didvan/models/requests/news.dart';
import 'package:didvan/models/requests/radar.dart';
-import 'package:didvan/models/studio_details_data.dart';
import 'package:didvan/routes/routes.dart';
import 'package:didvan/services/app_initalizer.dart';
+import 'package:didvan/services/media/media.dart';
import 'package:didvan/utils/action_sheet.dart';
-import 'package:didvan/utils/date_time.dart';
-import 'package:didvan/views/direct/widgets/audio_widget.dart';
-import 'package:didvan/views/widgets/overview/multitype.dart';
-import 'package:didvan/views/widgets/tag_item.dart';
+import 'package:didvan/views/home/media/widgets/audio_waveform_progress.dart';
import 'package:didvan/views/widgets/animated_visibility.dart';
import 'package:didvan/views/widgets/didvan/button.dart';
import 'package:didvan/views/widgets/didvan/card.dart';
@@ -24,9 +22,14 @@ import 'package:didvan/views/widgets/didvan/divider.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/ink_wrapper.dart';
import 'package:didvan/views/widgets/item_title.dart';
+import 'package:didvan/views/widgets/overview/multitype.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
+import 'package:didvan/views/widgets/tag_item.dart';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
+import 'package:flutter_svg/svg.dart';
+import 'package:just_audio/just_audio.dart';
+import 'package:persian_number_utility/persian_number_utility.dart';
import 'package:url_launcher/url_launcher_string.dart';
class DidvanPageView extends StatefulWidget {
@@ -54,183 +57,662 @@ class DidvanPageView extends StatefulWidget {
}
class _DidvanPageViewState extends State {
+ static String? _lastPlayedUrl;
+
+ String _formatDuration(Duration duration) {
+ String twoDigits(int n) => n.toString().padLeft(2, "0");
+ String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
+ String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
+ return "${duration.inHours > 0 ? '${twoDigits(duration.inHours)}:' : ''}$twoDigitMinutes:$twoDigitSeconds";
+ }
+
+ PopupMenuItem popUpSpeed({required double value}) {
+ return PopupMenuItem(
+ value: value,
+ child: DidvanText('$value X'),
+ );
+ }
+
+ String _getCategoryIcon(String label) {
+ if (label.contains('سیاسی')) {
+ return 'lib/assets/icons/SiasiNoBG.svg';
+ } else if (label.contains('کسب')) {
+ return 'lib/assets/icons/KasbokarNoBG.svg';
+ } else if (label.contains('اقتصادی')) {
+ return 'lib/assets/icons/Economic_NoBG.svg';
+ } else if (label.contains('اجتماعی')) {
+ return 'lib/assets/icons/EjtemaeiNoBg.svg';
+ } else if (label.contains('محیطی') || label.contains('زیست')) {
+ return 'lib/assets/icons/ZistMohitNoBG.svg';
+ } else if (label.contains('فناوری')) {
+ return 'lib/assets/icons/FanavariNoBG.svg';
+ }
+ return 'lib/assets/icons/discovery.svg';
+ }
+
+ @override
+ void dispose() {
+ MediaService.audioPlayer.stop();
+ super.dispose();
+ }
+
@override
Widget build(BuildContext context) {
- final double deviceTopPadding = MediaQuery.of(context).padding.top;
- return Stack(
- children: [
- Directionality(
- textDirection: TextDirection.ltr,
- child: CarouselSlider.builder(
- itemCount: widget.items.length,
- options: CarouselOptions(
- onPageChanged: (index, reason) => widget.onPageChanged(index),
- height: double.infinity,
- initialPage: widget.initialIndex,
- viewportFraction: 0.94,
- enableInfiniteScroll: false,
+ return Scaffold(
+ backgroundColor: Theme.of(context).colorScheme.background,
+ appBar: PreferredSize(
+ preferredSize: const Size.fromHeight(90.0),
+ child: AppBar(
+ backgroundColor: Colors.white,
+ elevation: 0,
+ automaticallyImplyLeading: false,
+ flexibleSpace: SafeArea(
+ child: Container(
+ padding:
+ const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Center(
+ child: SvgPicture.asset(
+ 'lib/assets/images/logos/logo-horizontal-light.svg',
+ height: 55,
+ ),
+ ),
+ IconButton(
+ icon: SvgPicture.asset(
+ 'lib/assets/icons/arrow-left.svg',
+ color: const Color.fromARGB(255, 102, 102, 102),
+ height: 24,
+ ),
+ onPressed: () {
+ Navigator.pop(context);
+ },
+ ),
+ ],
+ ),
+ ),
),
- itemBuilder: (context, index, realIndex) => Directionality(
- textDirection: TextDirection.rtl,
- child: SizedBox(
- height: MediaQuery.of(context).size.height,
- child: SingleChildScrollView(
- controller: index == widget.currentIndex
- ? widget.scrollController
- : null,
- physics: const BouncingScrollPhysics(),
- padding: EdgeInsets.only(
- left: 4,
- right: 4,
- top: 16 + deviceTopPadding,
- bottom: 92,
- ),
- child: DidvanCard(
- padding: EdgeInsets.zero,
- enableBorder: false,
- child: Builder(
- builder: (context) {
- final item = widget.items[index];
- if (item == null) {
- return const SizedBox();
- }
- return Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- SkeletonImage(
- imageUrl: item.image,
- aspectRatio: 16 / 9,
- ),
- const SizedBox(height: 20),
- Padding(
- padding:
- const EdgeInsets.symmetric(horizontal: 16),
- child: DidvanText(
- item.title,
- style: Theme.of(context).textTheme.titleMedium,
- ),
- ),
- const SizedBox(height: 8),
- Padding(
- padding:
- const EdgeInsets.symmetric(horizontal: 16),
- child: _subtitle(item),
- ),
- if (widget.isRadar && item.podcast != null)
- Container(
- padding: const EdgeInsets.all(12)
- .copyWith(left: 0, bottom: 4),
- margin: const EdgeInsets.all(12),
- decoration: BoxDecoration(
- border: Border.all(
- color: Theme.of(context).colorScheme.hint,
- ),
- borderRadius: DesignConfig.lowBorderRadius,
- color:
- Theme.of(context).colorScheme.background,
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
+ )),
+ body: Column(
+ children: [
+ Expanded(
+ child: Stack(
+ children: [
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: CarouselSlider.builder(
+ itemCount: widget.items.length,
+ options: CarouselOptions(
+ onPageChanged: (index, reason) =>
+ widget.onPageChanged(index),
+ height: double.infinity,
+ initialPage: widget.initialIndex,
+ viewportFraction: 1.0,
+ enableInfiniteScroll: false,
+ scrollPhysics: const BouncingScrollPhysics(),
+ ),
+ itemBuilder: (context, index, realIndex) {
+ final item = widget.items[index];
+ if (item == null) {
+ return const SizedBox();
+ }
+ final bool isActive = index == widget.currentIndex;
+
+ return Directionality(
+ textDirection: TextDirection.rtl,
+ child: LayoutBuilder(
+ builder: (context, constraints) {
+ final double imageHeight =
+ constraints.maxWidth / (16 / 9);
+ const double overlap = 24.0;
+
+ return SizedBox(
+ height: constraints.maxHeight,
+ child: SingleChildScrollView(
+ controller:
+ isActive ? widget.scrollController : null,
+ physics: const BouncingScrollPhysics(),
+ padding: const EdgeInsets.only(bottom: 92),
+ child: Stack(
children: [
- const DidvanText('این مطلب را بشنوید:'),
- const SizedBox(height: 8),
- AudioWidget(
- id: item.id,
- audioUrl: item.podcast,
- audioMetaData: StudioDetailsData(
- id: item.id,
- link: item.podcast,
- createdAt: item.createdAt,
- order: 0,
- marked: item.marked,
- comments: 0,
- tags: [],
- iframe: '',
- duration: item.duration,
- title: item.title,
- image: item.image,
- description: 'radar',
- type: 'radar',
+ Positioned(
+ top: 0,
+ left: 0,
+ right: 0,
+ height: imageHeight,
+ child: SkeletonImage(
+ imageUrl: item.image,
+ borderRadius: BorderRadius.zero,
+ width: double.infinity,
+ ),
+ ),
+ if (widget.isRadar &&
+ item.categories != null &&
+ item.categories.isNotEmpty)
+ Positioned(
+ top: 16,
+ left: 16,
+ child: Wrap(
+ spacing: 8.0,
+ runSpacing: 8.0,
+ direction: Axis.horizontal,
+ alignment: WrapAlignment.start,
+ children: [
+ for (var i = 0;
+ i < item.categories.length;
+ i++)
+ Container(
+ padding:
+ const EdgeInsets.symmetric(
+ horizontal: 12,
+ vertical: 6),
+ decoration: BoxDecoration(
+ color: const Color.fromARGB(
+ 255, 230, 242, 246),
+ borderRadius:
+ BorderRadius.circular(16),
+ ),
+ child: Row(
+ mainAxisSize:
+ MainAxisSize.min,
+ children: [
+ SvgPicture.asset(
+ _getCategoryIcon(item
+ .categories[i].label),
+ height: 16,
+ width: 16,
+ color:
+ const Color.fromARGB(
+ 255, 25, 93, 128),
+ ),
+ const SizedBox(width: 6),
+ DidvanText(
+ item.categories[i].label,
+ style: Theme.of(context)
+ .textTheme
+ .bodySmall
+ ?.copyWith(
+ color: const Color
+ .fromARGB(255,
+ 25, 93, 128),
+ fontWeight:
+ FontWeight.bold,
+ fontSize: 12,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ Container(
+ margin: EdgeInsets.only(
+ top: imageHeight - overlap),
+ decoration: BoxDecoration(
+ color: Theme.of(context)
+ .colorScheme
+ .background,
+ borderRadius:
+ const BorderRadius.vertical(
+ top: Radius.circular(24),
+ ),
+ boxShadow: [
+ BoxShadow(
+ color:
+ Colors.black.withOpacity(0.05),
+ blurRadius: 10,
+ offset: const Offset(0, -5),
+ ),
+ ],
+ ),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(
+ vertical: 24),
+ child: Column(
+ crossAxisAlignment:
+ CrossAxisAlignment.start,
+ children: [
+ if (widget.isRadar &&
+ item.podcast != null)
+ _AnimatedEntry(
+ isActive: isActive,
+ delay: 0,
+ child: _buildPodcastStylePlayer(
+ context, item),
+ ),
+ _AnimatedEntry(
+ isActive: isActive,
+ delay: 100,
+ child: Padding(
+ padding:
+ const EdgeInsets.symmetric(
+ horizontal: 16),
+ child: DidvanText(
+ item.title,
+ style: Theme.of(context)
+ .textTheme
+ .titleMedium,
+ ),
+ ),
+ ),
+ const SizedBox(height: 8),
+ if (!widget.isRadar &&
+ item.description != null &&
+ item.description!.isNotEmpty)
+ _AnimatedEntry(
+ isActive: isActive,
+ delay: 200,
+ child: Padding(
+ padding: const EdgeInsets
+ .symmetric(
+ horizontal: 16),
+ child: Column(
+ crossAxisAlignment:
+ CrossAxisAlignment
+ .start,
+ children: [
+ DidvanText(
+ 'خلاصه خبر:',
+ style: Theme.of(context)
+ .textTheme
+ .bodyMedium
+ ?.copyWith(
+ color: Theme.of(
+ context)
+ .colorScheme
+ .primary,
+ fontWeight:
+ FontWeight
+ .bold,
+ ),
+ ),
+ const SizedBox(height: 8),
+ DidvanText(
+ item.description!,
+ style:
+ Theme.of(context)
+ .textTheme
+ .bodyMedium
+ ?.copyWith(
+ color: const Color
+ .fromARGB(
+ 255,
+ 102,
+ 102,
+ 102),
+ )),
+ const SizedBox(height: 2),
+ const DidvanDivider(),
+ ],
+ ),
+ ),
+ ),
+ if (widget.isRadar &&
+ item.description != null &&
+ item.description!.isNotEmpty)
+ _AnimatedEntry(
+ isActive: isActive,
+ delay: 200,
+ child: Padding(
+ padding: const EdgeInsets
+ .symmetric(
+ horizontal: 16),
+ child: Column(
+ crossAxisAlignment:
+ CrossAxisAlignment
+ .start,
+ children: [
+ DidvanText(
+ 'خلاصه پویش افق:',
+ style: Theme.of(context)
+ .textTheme
+ .bodyMedium
+ ?.copyWith(
+ color: Theme.of(
+ context)
+ .colorScheme
+ .primary,
+ fontWeight:
+ FontWeight
+ .bold,
+ ),
+ ),
+ const SizedBox(height: 8),
+ DidvanText(
+ item.description!,
+ style: Theme.of(context)
+ .textTheme
+ .bodyMedium
+ ?.copyWith(
+ color: const Color
+ .fromARGB(
+ 255,
+ 102,
+ 102,
+ 102),
+ ),
+ ),
+ const SizedBox(height: 2),
+ const DidvanDivider(),
+ ],
+ ),
+ ),
+ ),
+ _AnimatedEntry(
+ isActive: isActive,
+ delay: 300,
+ child: Padding(
+ padding:
+ const EdgeInsets.symmetric(
+ horizontal: 16),
+ child: _subtitle(item),
+ ),
+ ),
+ _AnimatedEntry(
+ isActive: isActive,
+ delay: 400,
+ child: Column(
+ children: [
+ for (var i = 0;
+ i < item.contents.length;
+ i++)
+ Padding(
+ padding: const EdgeInsets
+ .symmetric(
+ horizontal: 16,
+ vertical: 4,
+ ),
+ child: _contentBuilder(
+ item, i),
+ ),
+ ],
+ ),
+ ),
+ if (item.tags.isNotEmpty)
+ const SizedBox(height: 20),
+ Padding(
+ padding:
+ const EdgeInsets.symmetric(
+ horizontal: 16),
+ child: Wrap(
+ spacing: 8,
+ runSpacing: 8,
+ children: [
+ for (var i = 0;
+ i < item.tags.length;
+ i++)
+ TagItem(
+ tag: item.tags[i],
+ onMarkChanged:
+ widget.onMarkChanged,
+ type: widget.isRadar
+ ? 'radar'
+ : 'news',
+ ),
+ ],
+ ),
+ ),
+ const Padding(
+ padding: EdgeInsets.only(
+ left: 16,
+ right: 16,
+ top: 16,
+ ),
+ child: DidvanDivider(
+ verticalPadding: 0),
+ ),
+ const Padding(
+ padding: EdgeInsets.symmetric(
+ horizontal: 16.0,
+ vertical: 16.0),
+ child: ItemTitle(
+ title: 'مطالب مرتبط:'),
+ ),
+ if (item.relatedContents.isEmpty &&
+ !item.relatedContentsIsEmpty)
+ for (var i = 0; i < 3; i++)
+ Padding(
+ padding:
+ const EdgeInsets.only(
+ bottom: 8,
+ left: 16,
+ right: 16,
+ ),
+ child: MultitypeOverview
+ .placeholder,
+ ),
+ for (var i = 0;
+ i < item.relatedContents.length;
+ i++)
+ Padding(
+ padding:
+ const EdgeInsets.symmetric(
+ horizontal: 16,
+ vertical: 4),
+ child: MultitypeOverview(
+ item: item.relatedContents[i],
+ onMarkChanged: (id, value) {},
+ ),
+ ),
+ const SizedBox(height: 8),
+ ],
+ ),
),
),
],
),
),
- for (var i = 0; i < item.contents.length; i++)
- Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 16,
- vertical: 4,
- ),
- child: _contentBuilder(item, i),
- ),
- if (item.tags.isNotEmpty)
- const SizedBox(height: 20),
- Padding(
- padding:
- const EdgeInsets.symmetric(horizontal: 16),
- child: Wrap(
- spacing: 8,
- runSpacing: 8,
- children: [
- for (var i = 0; i < item.tags.length; i++)
- TagItem(
- tag: item.tags[i],
- onMarkChanged: widget.onMarkChanged,
- type: widget.isRadar ? 'radar' : 'news',
- ),
- ],
+ );
+ },
+ ),
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildPodcastStylePlayer(BuildContext context, dynamic item) {
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const SizedBox(
+ height: 16,
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: Row(
+ children: [
+ Expanded(
+ child: StreamBuilder(
+ stream: MediaService.audioPlayer.positionStream,
+ builder: (context, snapshot) {
+ final position = snapshot.data ?? Duration.zero;
+
+ return StreamBuilder(
+ stream: MediaService.audioPlayer.durationStream,
+ builder: (context, durationSnapshot) {
+ final totalDuration = durationSnapshot.data ??
+ Duration(
+ minutes:
+ int.tryParse(item.duration.toString()) ??
+ 0);
+
+ final progress = totalDuration.inMilliseconds > 0
+ ? position.inMilliseconds /
+ totalDuration.inMilliseconds
+ : 0.0;
+
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ DidvanText(
+ _formatDuration(totalDuration),
+ style: const TextStyle(
+ fontSize: 12,
+ color: Color.fromARGB(255, 102, 102, 102),
),
),
- const Padding(
- padding: EdgeInsets.only(
- left: 16,
- right: 16,
- top: 16,
+ const SizedBox(width: 8),
+ Expanded(
+ child: AudioWaveformProgress(
+ progress: progress.clamp(0.0, 1.0),
+ isActive: true,
+ onChanged: (value) {
+ final newPosition = Duration(
+ milliseconds:
+ (totalDuration.inMilliseconds * value)
+ .round(),
+ );
+ MediaService.audioPlayer.seek(newPosition);
+ },
),
- child: DidvanDivider(verticalPadding: 0),
),
- const Padding(
- padding: EdgeInsets.symmetric(horizontal: 16.0),
- child: ItemTitle(title: 'مطالب مشابه'),
- ),
- if (item.relatedContents.isEmpty &&
- !item.relatedContentsIsEmpty)
- for (var i = 0; i < 3; i++)
- Padding(
- padding: const EdgeInsets.only(
- bottom: 8,
- left: 16,
- right: 16,
- ),
- child: MultitypeOverview.placeholder,
- ),
- for (var i = 0;
- i < item.relatedContents.length;
- i++)
- Padding(
- padding: const EdgeInsets.only(bottom: 8),
- child: MultitypeOverview(
- item: item.relatedContents[i],
- onMarkChanged: (id, value) {},
- ),
+ const SizedBox(width: 8),
+ DidvanText(
+ _formatDuration(position),
+ style: const TextStyle(
+ fontSize: 12,
+ color: Color.fromARGB(255, 102, 102, 102),
),
- const SizedBox(height: 8),
+ ),
],
);
},
- ),
- ),
+ );
+ },
),
),
- ),
+ ],
),
),
- Positioned(
- right: 24,
- top: 24 + deviceTopPadding,
- child: _BackButton(scrollController: widget.scrollController),
+ const SizedBox(height: 16),
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: [
+ IconButton(
+ icon: SvgPicture.asset(
+ 'lib/assets/icons/timer-pause.svg',
+ width: 24,
+ height: 24,
+ color: const Color.fromARGB(255, 102, 102, 102),
+ ),
+ onPressed: () {
+ ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
+ content: Text("تایمر خواب در این بخش فعال نیست")));
+ },
+ ),
+ IconButton(
+ onPressed: () {
+ MediaService.audioPlayer.seek(
+ Duration(
+ seconds: max(
+ 0, MediaService.audioPlayer.position.inSeconds + 10),
+ ),
+ );
+ },
+ icon: SvgPicture.asset('lib/assets/icons/forward-10-seconds.svg'),
+ ),
+ StreamBuilder(
+ stream: MediaService.audioPlayer.playerStateStream,
+ builder: (context, snapshot) {
+ final playerState = snapshot.data;
+ final processingState = playerState?.processingState;
+ final playing = playerState?.playing;
+
+ if (processingState == ProcessingState.loading ||
+ processingState == ProcessingState.buffering) {
+ return Container(
+ margin: const EdgeInsets.all(8.0),
+ width: 48.0,
+ height: 48.0,
+ child: const CircularProgressIndicator(),
+ );
+ } else if (playing != true) {
+ return IconButton(
+ icon: const Icon(Icons.play_circle_fill, size: 68),
+ color: Theme.of(context).primaryColor,
+ onPressed: () async {
+ if (_lastPlayedUrl != item.podcast) {
+ _lastPlayedUrl = item.podcast;
+ await MediaService.audioPlayer.setUrl(item.podcast);
+ }
+ MediaService.audioPlayer.play();
+ },
+ );
+ } else if (processingState != ProcessingState.completed) {
+ return IconButton(
+ icon: const Icon(Icons.pause_circle_filled, size: 68),
+ color: Theme.of(context).primaryColor,
+ onPressed: MediaService.audioPlayer.pause,
+ );
+ } else {
+ return IconButton(
+ icon: const Icon(Icons.replay_circle_filled, size: 48),
+ color: Theme.of(context).primaryColor,
+ onPressed: () =>
+ MediaService.audioPlayer.seek(Duration.zero),
+ );
+ }
+ },
+ ),
+ IconButton(
+ onPressed: () {
+ MediaService.audioPlayer.seek(
+ Duration(
+ seconds:
+ max(0, MediaService.audioPlayer.position.inSeconds - 5),
+ ),
+ );
+ },
+ icon: SvgPicture.asset('lib/assets/icons/backward-5-seconds.svg'),
+ ),
+ StreamBuilder(
+ stream: MediaService.audioPlayer.speedStream,
+ builder: (context, snapshot) {
+ if (!snapshot.hasData) return const SizedBox();
+ return PopupMenuButton(
+ child: Container(
+ padding:
+ const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
+ decoration: BoxDecoration(
+ borderRadius: DesignConfig.mediumBorderRadius,
+ border: Border.all(
+ color: const Color.fromARGB(255, 102, 102, 102))),
+ child: DidvanText(
+ '${snapshot.data!.toString().replaceAll('.0', '')}X',
+ style: const TextStyle(
+ fontWeight: FontWeight.w900,
+ color: Color.fromARGB(255, 102, 102, 102),
+ fontSize: 12),
+ ),
+ ),
+ onSelected: (value) async {
+ await MediaService.audioPlayer.setSpeed(value);
+ },
+ itemBuilder: (BuildContext context) =>
+ >[
+ popUpSpeed(value: 0.5),
+ popUpSpeed(value: 0.75),
+ popUpSpeed(value: 1.0),
+ popUpSpeed(value: 1.25),
+ popUpSpeed(value: 1.5),
+ popUpSpeed(value: 2.0),
+ ],
+ );
+ },
+ ),
+ ],
),
+ const SizedBox(height: 16),
],
);
}
@@ -324,14 +806,17 @@ class _DidvanPageViewState extends State {
lineHeight: LineHeight.percent(135),
margin: const Margins(),
padding: HtmlPaddings.zero,
+ color: const Color.fromRGBO(102, 102, 102, 1),
),
'a': Style(
textAlign: TextAlign.left,
+ color: const Color.fromARGB(255, 0, 126, 167),
+ textDecoration: TextDecoration.none,
+ fontWeight: FontWeight.normal,
),
},
);
}
-
if (content.image != null) {
return Column(
children: [
@@ -385,32 +870,30 @@ class _DidvanPageViewState extends State {
return Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
- DidvanText(
- 'رادار ',
- style: Theme.of(context).textTheme.bodySmall,
+ SvgPicture.asset('lib/assets/icons/calendar.svg'),
+ const SizedBox(
+ width: 5,
),
- for (var i = 0; i < item.categories.length; i++)
- DidvanText(
- item.categories[i].label +
- '${i != item.categories.length - 1 ? '،' : ''} ',
- style: Theme.of(context).textTheme.bodySmall,
- ),
DidvanText(
- ' - ${DateTimeUtils.momentGenerator(item.createdAt)}',
- style: Theme.of(context).textTheme.bodySmall,
+ DateTime.parse(item.createdAt!).toPersianDateStr(),
+ style: Theme.of(context).textTheme.bodySmall?.copyWith(
+ color: const Color.fromARGB(255, 102, 102, 102),
+ ),
),
],
);
} else {
return Row(
children: [
- DidvanText(
- item.reference,
- style: Theme.of(context).textTheme.bodySmall,
+ SvgPicture.asset('lib/assets/icons/calendar.svg'),
+ const SizedBox(
+ width: 5,
),
DidvanText(
- ' - ${DateTimeUtils.momentGenerator(item.createdAt)}',
- style: Theme.of(context).textTheme.bodySmall,
+ DateTime.parse(item.createdAt!).toPersianDateStr(),
+ style: Theme.of(context).textTheme.bodySmall?.copyWith(
+ color: const Color.fromARGB(255, 102, 102, 102),
+ ),
),
],
);
@@ -420,6 +903,7 @@ class _DidvanPageViewState extends State {
class _BackButton extends StatefulWidget {
final ScrollController? scrollController;
+ // ignore: unused_element_parameter
const _BackButton({Key? key, this.scrollController}) : super(key: key);
@override
@@ -488,3 +972,79 @@ class __BackButtonState extends State<_BackButton> {
);
}
}
+
+class _AnimatedEntry extends StatefulWidget {
+ final Widget child;
+ final int delay;
+ final bool isActive;
+
+ const _AnimatedEntry({
+ Key? key,
+ required this.child,
+ required this.delay,
+ required this.isActive,
+ }) : super(key: key);
+
+ @override
+ State<_AnimatedEntry> createState() => _AnimatedEntryState();
+}
+
+class _AnimatedEntryState extends State<_AnimatedEntry> {
+ bool _shouldAnimate = false;
+ Timer? _timer;
+
+ @override
+ void initState() {
+ super.initState();
+ _checkAnimation();
+ }
+
+ @override
+ void didUpdateWidget(covariant _AnimatedEntry oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (widget.isActive != oldWidget.isActive) {
+ _checkAnimation();
+ }
+ }
+
+ @override
+ void dispose() {
+ _timer?.cancel();
+ super.dispose();
+ }
+
+ void _checkAnimation() {
+ if (widget.isActive) {
+ _timer?.cancel();
+ _timer = Timer(Duration(milliseconds: widget.delay), () {
+ if (mounted) {
+ setState(() {
+ _shouldAnimate = true;
+ });
+ }
+ });
+ } else {
+ _timer?.cancel();
+ if (_shouldAnimate) {
+ setState(() {
+ _shouldAnimate = false;
+ });
+ }
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return AnimatedOpacity(
+ duration: const Duration(milliseconds: 500),
+ curve: Curves.easeOut,
+ opacity: _shouldAnimate ? 1.0 : 0.0,
+ child: AnimatedPadding(
+ duration: const Duration(milliseconds: 600),
+ curve: Curves.easeOutQuart,
+ padding: EdgeInsets.only(top: _shouldAnimate ? 0 : 50),
+ child: widget.child,
+ ),
+ );
+ }
+}
diff --git a/lib/views/widgets/floating_navigation_bar.dart b/lib/views/widgets/floating_navigation_bar.dart
index b348212..27f5f29 100644
--- a/lib/views/widgets/floating_navigation_bar.dart
+++ b/lib/views/widgets/floating_navigation_bar.dart
@@ -101,10 +101,7 @@ class _FloatingNavigationBarState extends State {
height: 48,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.navigation,
- borderRadius: const BorderRadius.only(
- bottomLeft: Radius.circular(24),
- bottomRight: Radius.circular(24),
- ),
+ borderRadius: BorderRadius.circular(24),
border: DesignConfig.isDark
? Border.all(
color: Theme.of(context).colorScheme.cardBorder,
@@ -138,28 +135,28 @@ class _FloatingNavigationBarState extends State {
),
),
const Spacer(),
- DidvanIconButton(
- gestureSize: 32,
- onPressed: () => Navigator.of(context).pushNamed(
- Routes.mentions,
- arguments: {
- 'id': widget.item.id,
- 'type': widget.isRadar ? 'radar' : 'news',
- 'title': widget.item.title,
- },
+ SizedBox(
+ width: 60,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ DidvanIconButton(
+ gestureSize: 32,
+ onPressed: () => Navigator.of(context).pushNamed(
+ Routes.mentions,
+ arguments: {
+ 'id': widget.item.id,
+ 'type': widget.isRadar ? 'radar' : 'news',
+ 'title': widget.item.title,
+ },
+ ),
+ icon: 'lib/assets/icons/fluent_mention-32-regular.svg',
+ isSvg: true,
+ size: 25,
+ ),
+ ],
),
- icon: DidvanIcons.mention_icon,
),
- const Spacer(),
- DidvanIconButton(
- gestureSize: 32,
- onPressed: () => Navigator.of(context).pushNamedAndRemoveUntil(
- Routes.home,
- (route) => false,
- ),
- icon: DidvanIcons.house_regular,
- ),
- const Spacer(),
BookmarkButton(
itemId: widget.item.id,
type: widget.isRadar ? 'radar' : 'news',
@@ -174,9 +171,10 @@ class _FloatingNavigationBarState extends State {
Navigator.of(context).pop();
}
},
- gestureSize: 32,
+ gestureSize: 22,
+ svgIconOn: 'lib/assets/icons/bookmark_fill.svg',
+ svgIconOff: 'lib/assets/icons/archive-tick.svg',
),
- const Spacer(),
SizedBox(
width: 60,
child: Row(
@@ -198,12 +196,13 @@ class _FloatingNavigationBarState extends State {
'onCommentsChanged': widget.onCommentsChanged,
},
),
- icon: DidvanIcons.chats_regular,
+ icon: 'lib/assets/icons/message-text.svg',
+ isSvg: true,
+ size: 25,
),
],
),
),
- const Spacer(),
DidvanIconButton(
gestureSize: 32,
onPressed:
diff --git a/lib/views/widgets/overview/news.dart b/lib/views/widgets/overview/news.dart
index 9b64b04..5ed5e27 100644
--- a/lib/views/widgets/overview/news.dart
+++ b/lib/views/widgets/overview/news.dart
@@ -38,6 +38,7 @@ class NewsOverview extends StatelessWidget {
'id': news.id,
'args': newsRequestArgs,
'hasUnmarkConfirmation': hasUnmarkConfirmation,
+ 'description': news.description,
},
),
child: Column(
@@ -79,28 +80,38 @@ class NewsOverview extends StatelessWidget {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- Row(
- children: [
- SvgPicture.asset('lib/assets/icons/calendar.svg'),
- const SizedBox(
- width: 5,
- ),
- DidvanText(
- DateTime.parse(news.createdAt).toPersianDateStr(),
- style: Theme.of(context).textTheme.bodySmall,
- ),
- const SizedBox(
- width: 20,
- ),
- SvgPicture.asset('lib/assets/icons/global.svg'),
- const SizedBox(
- width: 5,
- ),
- DidvanText(
- news.reference!,
- style: Theme.of(context).textTheme.bodySmall,
- ),
- ],
+ Expanded(
+ child: Row(
+ children: [
+ SvgPicture.asset('lib/assets/icons/calendar.svg'),
+ const SizedBox(
+ width: 5,
+ ),
+ Flexible(
+ child: DidvanText(
+ DateTime.parse(news.createdAt).toPersianDateStr(),
+ style: Theme.of(context).textTheme.bodySmall,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ const SizedBox(
+ width: 20,
+ ),
+ SvgPicture.asset('lib/assets/icons/global.svg'),
+ const SizedBox(
+ width: 5,
+ ),
+ Flexible(
+ child: DidvanText(
+ news.reference!,
+ style: Theme.of(context).textTheme.bodySmall,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ],
+ ),
),
Row(
children: [
@@ -185,4 +196,4 @@ class NewsOverview extends StatelessWidget {
],
),
);
-}
+}
\ No newline at end of file
diff --git a/lib/views/widgets/overview/radar.dart b/lib/views/widgets/overview/radar.dart
index df13ec9..f02b89e 100644
--- a/lib/views/widgets/overview/radar.dart
+++ b/lib/views/widgets/overview/radar.dart
@@ -1,17 +1,16 @@
import 'package:didvan/config/theme_data.dart';
-import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/models/overview_data.dart';
import 'package:didvan/models/requests/radar.dart';
import 'package:didvan/routes/routes.dart';
import 'package:didvan/views/widgets/bookmark_button.dart';
import 'package:didvan/views/widgets/didvan/card.dart';
import 'package:didvan/views/widgets/didvan/divider.dart';
-import 'package:didvan/views/widgets/didvan/icon_button.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/liked_button.dart';
import 'package:didvan/views/widgets/shimmer_placeholder.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
import 'package:persian_number_utility/persian_number_utility.dart';
class RadarOverview extends StatelessWidget {
@@ -34,6 +33,8 @@ class RadarOverview extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DidvanCard(
+ padding: const EdgeInsets.all(0),
+ enableBorder: true,
onTap: () => Navigator.of(context).pushNamed(
Routes.radarDetails,
arguments: {
@@ -42,22 +43,33 @@ class RadarOverview extends StatelessWidget {
'id': radar.id,
'args': radarRequestArgs,
'hasUnmarkConfirmation': hasUnmarkConfirmation,
+ 'description': radar.description,
},
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- DidvanText(
- radar.title,
- fontWeight: FontWeight.w600,
+ SkeletonImage(
+ imageUrl: radar.image,
+ aspectRatio: 16 / 8,
+ borderRadius: const BorderRadius.only(
+ topLeft: Radius.circular(16),
+ topRight: Radius.circular(16),
+ bottomLeft: Radius.circular(0),
+ bottomRight: Radius.circular(0),
+ )),
+ const SizedBox(
+ height: 10,
+ ),
+ Padding(
+ padding: const EdgeInsets.only(left: 8.0, right: 8.0),
+ child: DidvanText(
+ radar.title,
+ fontWeight: FontWeight.w600,
+ ),
),
- const SizedBox(height: 8),
Stack(
children: [
- SkeletonImage(
- imageUrl: radar.image,
- aspectRatio: 16 / 9,
- ),
if (radar.forManagers)
Positioned(
top: 0,
@@ -80,73 +92,95 @@ class RadarOverview extends StatelessWidget {
),
],
),
- const SizedBox(height: 8),
- Row(
- children: [
- DidvanText(
- radar.categories!.first.label,
- style: Theme.of(context).textTheme.labelSmall,
- color: Theme.of(context).colorScheme.caption,
- ),
- const Spacer(),
- DidvanText(
- '${DateTime.parse(radar.createdAt).toPersianDateStr()} | خواندن در ${radar.timeToRead} دقیقه',
- style: Theme.of(context).textTheme.labelSmall,
- color: Theme.of(context).colorScheme.caption,
- ),
- ],
+ const SizedBox(height: 5),
+ Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: DidvanText(
+ radar.description,
+ maxLines: 3,
+ overflow: TextOverflow.ellipsis,
+ ),
),
- const SizedBox(height: 8),
- DidvanText(
- radar.description,
- maxLines: 3,
- overflow: TextOverflow.ellipsis,
+ const Padding(
+ padding: EdgeInsets.only(left: 8, right: 8),
+ child: DidvanDivider(),
),
- const DidvanDivider(),
Row(
children: [
if (radar.comments != 0) DidvanText(radar.comments.toString()),
const SizedBox(width: 4),
- DidvanIconButton(
- icon: DidvanIcons.chats_regular,
- gestureSize: 24,
- onPressed: () => Navigator.of(context).pushNamed(
- Routes.comments,
- arguments: {
- 'type': 'radar',
- 'title': radar.title,
- 'id': radar.id,
- 'onCommentsChanged': (count) =>
- onCommentsChanged(radar.id, count)
- },
+ Padding(
+ padding: const EdgeInsets.fromLTRB(8, 8, 8, 20),
+ child: Row(
+ children: [
+ SvgPicture.asset('lib/assets/icons/calendar.svg'),
+ const SizedBox(
+ width: 5,
+ ),
+ DidvanText(
+ DateTime.parse(radar.createdAt).toPersianDateStr(),
+ style: Theme.of(context)
+ .textTheme
+ .labelSmall
+ ?.copyWith(fontWeight: FontWeight.bold),
+ color: Theme.of(context).colorScheme.caption,
+ ),
+ const SizedBox(
+ width: 15,
+ ),
+ SvgPicture.asset(
+ 'lib/assets/icons/ion_extension-puzzle-outline.svg',
+ height: 17,
+ ),
+ const SizedBox(
+ width: 5,
+ ),
+ DidvanText(
+ radar.categories!.first.label,
+ style: Theme.of(context)
+ .textTheme
+ .labelSmall
+ ?.copyWith(fontWeight: FontWeight.bold),
+ color: Theme.of(context).colorScheme.caption,
+ ),
+ ],
),
),
const Spacer(),
- Row(
- children: [
- LikedButton(
- itemId: radar.id,
- type: 'radar',
- gestureSize: 32,
- value: radar.liked,
- onMarkChanged: (value) =>
- onLikedChanged(radar.id, value, false),
- askForConfirmation: hasUnmarkConfirmation,
- likes: radar.likes,
- ),
- const SizedBox(
- width: 4.0,
- ),
- BookmarkButton(
- itemId: radar.id,
- type: 'radar',
- gestureSize: 32,
- value: radar.marked,
- onMarkChanged: (value) =>
- onMarkChanged(radar.id, value, false),
- askForConfirmation: hasUnmarkConfirmation,
- ),
- ],
+ Padding(
+ padding: const EdgeInsets.fromLTRB(8, 0, 8, 5),
+ child: Row(
+ children: [
+ LikedButton(
+ itemId: radar.id,
+ type: 'radar',
+ gestureSize: 32,
+ value: radar.liked,
+ onMarkChanged: (value) =>
+ onLikedChanged(radar.id, value, false),
+ askForConfirmation: hasUnmarkConfirmation,
+ likes: radar.likes,
+ unlikedColor: const Color.fromARGB(255, 102, 102, 102),
+ ),
+ const SizedBox(
+ width: 4.0,
+ ),
+ BookmarkButton(
+ itemId: radar.id,
+ type: 'radar',
+ gestureSize: 22,
+ value: radar.marked,
+ onMarkChanged: (value) =>
+ onMarkChanged(radar.id, value, false),
+ askForConfirmation: hasUnmarkConfirmation,
+ svgIconOn: 'lib/assets/icons/bookmark_fill.svg',
+ svgIconOff: 'lib/assets/icons/archive-tick.svg',
+ color: const Color.fromARGB(255, 102, 102, 102),
+ unbookmarkedColor:
+ const Color.fromARGB(255, 102, 102, 102),
+ ),
+ ],
+ ),
),
],
),