This commit is contained in:
OkaykOrhmn 2024-09-15 15:20:09 +03:30
parent 6c3117a918
commit 9ea80f21fb
32 changed files with 480 additions and 199 deletions

View File

@ -1,5 +1,7 @@
// ignore_for_file: deprecated_member_use
import 'dart:async';
import 'package:android_intent_plus/android_intent.dart';
import 'package:bot_toast/bot_toast.dart';
import 'package:didvan/config/theme_data.dart';
@ -12,9 +14,11 @@ import 'package:didvan/providers/user.dart';
import 'package:didvan/routes/route_generator.dart';
import 'package:didvan/routes/routes.dart';
import 'package:didvan/services/app_home_widget/home_widget_repository.dart';
import 'package:didvan/services/app_initalizer.dart';
import 'package:didvan/services/media/media.dart';
import 'package:didvan/services/notification/firebase_api.dart';
import 'package:didvan/services/notification/notification_service.dart';
import 'package:didvan/utils/my_custom_scroll_behavior.dart';
import 'package:didvan/views/ai/history_ai_chat_state.dart';
import 'package:didvan/views/podcasts/podcasts_state.dart';
import 'package:didvan/views/podcasts/studio_details/studio_details_state.dart';
@ -23,6 +27,7 @@ import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart';
import 'package:home_widget/home_widget.dart';
import 'package:provider/provider.dart';
@ -56,21 +61,28 @@ Future<void> _backgroundCallbackHomeWidget(Uri? uri) async {
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
try {
HomeWidget.registerBackgroundCallback(_backgroundCallbackHomeWidget);
HomeWidget.registerInteractivityCallback(_backgroundCallbackHomeWidget);
await runZonedGuarded(() async {
WidgetsFlutterBinding.ensureInitialized();
await NotificationService.initializeNotification();
FirebaseMessaging.onBackgroundMessage(_initPushNotification);
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform);
await FirebaseApi().initNotification();
} catch (e) {
debugPrint;
}
try {
if (!kIsWeb) {
HomeWidget.registerBackgroundCallback(_backgroundCallbackHomeWidget);
HomeWidget.registerInteractivityCallback(_backgroundCallbackHomeWidget);
await NotificationService.initializeNotification();
}
runApp(const Didvan());
FirebaseMessaging.onBackgroundMessage(_initPushNotification);
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform);
await FirebaseApi().initNotification();
} catch (e) {
debugPrint;
}
runApp(const Didvan());
}, (error, stack) async {
error.printError();
});
}
class Didvan extends StatefulWidget {
@ -88,10 +100,12 @@ class _DidvanState extends State<Didvan> with WidgetsBindingObserver {
@override
void initState() {
NotificationService.startListeningNotificationEvents();
if (!kIsWeb) {
NotificationService.startListeningNotificationEvents();
}
WidgetsBinding.instance.addObserver(this);
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
@ -108,15 +122,17 @@ class _DidvanState extends State<Didvan> with WidgetsBindingObserver {
@override
void didChangeAppLifecycleState(AppLifecycleState state) async {
if (state == AppLifecycleState.resumed) {
var r = await HomeWidget.getWidgetData("cRoute", defaultValue: '');
if (r!.toString() != Routes.splash) {
await HomeWidgetRepository.decideWhereToGo();
if (!kIsWeb) {
if (state == AppLifecycleState.resumed) {
var r = await HomeWidget.getWidgetData("cRoute", defaultValue: '');
if (r!.toString() != Routes.splash) {
await HomeWidgetRepository.decideWhereToGo();
NotificationMessage? data = HomeWidgetRepository.data;
NotificationMessage? data = HomeWidgetRepository.data;
if (data != null) {
await HomeWidgetRepository.decideWhereToGoNotif();
if (data != null) {
await HomeWidgetRepository.decideWhereToGoNotif();
}
}
}
}
@ -150,6 +166,7 @@ class _DidvanState extends State<Didvan> with WidgetsBindingObserver {
color: Theme.of(context).colorScheme.surface,
child: SafeArea(
child: MaterialApp(
scrollBehavior: MyCustomScrollBehavior(),
navigatorKey: navigatorKey,
debugShowCheckedModeBanner: false,
title: 'Didvan',

View File

@ -1,4 +1,5 @@
import 'package:didvan/models/ai/bots_model.dart';
import 'package:didvan/models/ai/files_model.dart';
class ChatsModel {
int? id;
@ -97,6 +98,7 @@ class Prompts {
bool? finished;
bool? error;
bool? audio;
FilesModel? fileLocal;
Prompts({
this.id,
@ -108,8 +110,9 @@ class Prompts {
this.createdAt,
this.finished,
this.error,
this.fileLocal,
this.audio,
});
}) {}
Prompts.fromJson(Map<String, dynamic> json) {
id = json['id'];
@ -141,6 +144,7 @@ class Prompts {
String? text,
String? file,
String? fileName,
FilesModel? fileLocal,
String? role,
String? createdAt,
bool? finished,
@ -152,6 +156,7 @@ class Prompts {
text: text ?? this.text,
file: file ?? this.file,
fileName: fileName ?? this.fileName,
fileLocal: fileLocal ?? this.fileLocal,
role: role ?? this.role,
createdAt: createdAt ?? this.createdAt,
finished: finished ?? this.finished,

View File

@ -1,4 +1,5 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:mime/mime.dart';
import 'package:path/path.dart' as p;
@ -9,14 +10,38 @@ class FilesModel {
late String extname;
late File main;
final bool isRecorded;
late bool isAudio;
late bool isImage;
final bool? audio;
final bool? image;
final bool? network;
final Uint8List? bytes;
FilesModel(this.path, {this.isRecorded = false}) {
basename = p.basename(path);
FilesModel(
this.path, {
final String? name,
this.isRecorded = false,
this.audio,
this.image,
this.network,
this.bytes,
}) {
basename = name ?? p.basename(path);
extname = p.extension(path);
main = File(path);
isAudio = lookupMimeType(path)?.startsWith('audio/') ?? false;
isImage = lookupMimeType(path)?.startsWith('image/') ?? false;
print(isAudio());
print(isImage());
print(isNetwork());
print(path);
}
bool isAudio() {
return audio ?? lookupMimeType(path)?.startsWith('audio/') ?? false;
}
bool isImage() {
return image ?? lookupMimeType(path)?.startsWith('image/') ?? false;
}
bool isNetwork() {
return network ?? path.startsWith('blob:') || path.startsWith('/uploads');
}
}

View File

@ -67,7 +67,9 @@ import '../views/notification_time/notification_time.dart';
class RouteGenerator {
static Route<dynamic> generateRoute(RouteSettings settings) {
HomeWidget.saveWidgetData("cRoute", settings.name!);
if (!kIsWeb) {
HomeWidget.saveWidgetData("cRoute", settings.name!);
}
switch (settings.name) {
case Routes.splash:
return _createRoute(

View File

@ -4,7 +4,10 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:didvan/models/ai/files_model.dart';
import 'package:didvan/services/storage/storage.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get_connect/http/src/multipart/multipart_file.dart';
import 'package:http/http.dart' as http;
import 'package:mime/mime.dart';
import 'package:path/path.dart' as p;
@ -17,7 +20,7 @@ class AiApiService {
{required final String url,
required final String message,
final int? chatId,
final File? file,
final FilesModel? file,
final bool? edite}) async {
final headers = {
"Authorization": "Bearer ${await StorageService.getValue(key: 'token')}",
@ -35,26 +38,69 @@ class AiApiService {
request.fields['edit'] = edite.toString().toLowerCase();
}
if (file != null) {
int length = 0;
try {
length = await file.length();
// ...
} catch (e) {
// Handle the error or return an error response
// final file = file;
// final filePath = file.path;
// final mimeType = filePath != null ? lookupMimeType(filePath) : null;
// final contentType = mimeType != null ? MediaType.parse(mimeType) : null;
// final fileReadStream = file.readStream;
// if (fileReadStream == null) {
// throw Exception('Cannot read file from null stream');
// }
// final stream = http.ByteStream(fileReadStream);
// final multipartFile = http.MultipartFile(
// 'file',
// stream,
// file.size,
// filename: file.name,
// contentType: contentType,
// );
// request.files.add(multipartFile);
Uint8List bytes;
if (kIsWeb) {
if (file.bytes != null) {
bytes = file.bytes!;
} else {
final Uri audioUri = Uri.parse(file.path);
final http.Response audioResponse = await http.get(audioUri);
bytes = audioResponse.bodyBytes;
}
request.files.add(http.MultipartFile.fromBytes(
'file',
bytes,
filename: file.isAudio()
? 'wav'
: file.isImage()
? 'png'
: 'pdf', // You can set a filename here
contentType: parser.MediaType.parse(file.isAudio()
? 'audio/mpeg'
: file.isImage()
? 'image/png'
: 'application/pdf'), // Set the MIME type
) // Use MediaType.parse to parse the MIME type
);
} else {
int length = 0;
try {
length = await file.main.length();
// ...
} catch (e) {
// Handle the error or return an error response
}
String? mimeType = lookupMimeType(
file.path); // Use MIME type instead of file extension
mimeType ??= 'application/octet-stream';
if (mimeType.startsWith('audio')) {
mimeType = 'audio/${p.extension(file.path).replaceAll('.', '')}';
}
request.files.add(
http.MultipartFile('file', file.main.readAsBytes().asStream(), length,
filename: file.basename,
contentType: parser.MediaType.parse(
mimeType)), // Use MediaType.parse to parse the MIME type
);
}
String basename = p.basename(file.path);
String? mimeType =
lookupMimeType(file.path); // Use MIME type instead of file extension
mimeType ??= 'application/octet-stream';
if (mimeType.startsWith('audio')) {
mimeType = 'audio/${p.extension(file.path).replaceAll('.', '')}';
}
request.files.add(
http.MultipartFile('file', file.readAsBytes().asStream(), length,
filename: basename,
contentType: parser.MediaType.parse(
mimeType)), // Use MediaType.parse to parse the MIME type
);
}
// print("req: ${request.files}");
@ -76,7 +122,7 @@ class AiApiService {
// Handle other non-200 responses
throw Exception('Failed to load data');
} else {
return response.stream;
return response.stream.asBroadcastStream();
}
} catch (e) {
// Handle any other errors

View File

@ -17,7 +17,7 @@ import '../network/request_helper.dart';
class HomeWidgetRepository {
static Future<void> fetchWidget() async {
RequestService.token = await StorageService.getValue(key: 'token');
// RequestService.token = await StorageService.getValue(key: 'token');
final service = RequestService(
RequestHelper.widgetNews(),
);
@ -90,7 +90,7 @@ class HomeWidgetRepository {
);
if (data.link!.startsWith('http')) {
launchUrlString(
openInWebView(
'${data.link}',
mode: LaunchMode.inAppWebView,
);
@ -219,7 +219,7 @@ class HomeWidgetRepository {
}
} else {
if (data.link!.startsWith('http')) {
launchUrlString(
openInWebView(
'${data.link}',
mode: LaunchMode.inAppWebView,
);

View File

@ -13,10 +13,15 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher_string.dart';
enum LaunchMode { inAppWebView }
void launchUrlString(String src, {dynamic mode}) {
void openInWebView(String src, {dynamic mode}) {
if (kIsWeb) {
launchUrlString(src);
return;
}
navigatorKey.currentState!.pushNamed(Routes.web, arguments: src);
}

View File

@ -1,8 +1,10 @@
import 'package:didvan/constants/assets.dart';
import 'package:didvan/main.dart';
import 'package:didvan/models/requests/studio.dart';
import 'package:didvan/models/studio_details_data.dart';
import 'package:didvan/models/view/action_sheet_data.dart';
import 'package:didvan/providers/media.dart';
import 'package:didvan/services/app_initalizer.dart';
import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart';
import 'package:didvan/services/storage/storage.dart';
@ -121,6 +123,7 @@ class MediaService {
return result;
} catch (e) {
e.printError(info: 'Pick PDF Fail');
// navigatorKey.currentState!.pop();
return null;
}
}

View File

@ -410,11 +410,13 @@ class ActionSheetUtils {
? ClipRRect(
borderRadius: DesignConfig.lowBorderRadius,
child: Image.file(File(image)))
: SkeletonImage(
width: min(MediaQuery.of(context).size.width,
MediaQuery.of(context).size.height),
imageUrl: image,
),
: image.startsWith('blob:')
? ClipRRect(
borderRadius: DesignConfig.lowBorderRadius,
child: Image.network(image))
: SkeletonImage(
imageUrl: image,
),
),
),
),

View File

@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
/// Get screen media.
@ -7,9 +8,9 @@ final MediaQueryData media =
/// This extention help us to make widget responsive.
extension NumberParsing on num {
double w() => this * media.size.width / 100;
double w() => this * media.size.width / (kIsWeb ? 250 : 100);
double h() => this * media.size.height / 100;
double h() => this * media.size.height / (kIsWeb ? 550 : 100);
}
///

View File

@ -0,0 +1,11 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
class MyCustomScrollBehavior extends MaterialScrollBehavior {
// Override behavior methods and getters like dragDevices
@override
Set<PointerDeviceKind> get dragDevices => {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
};
}

View File

@ -63,8 +63,12 @@ class _AiState extends State<Ai> {
Icon(
DidvanIcons.ai_solid,
size: MediaQuery.sizeOf(context).width / 5,
color: Theme.of(context).colorScheme.title,
),
DidvanText(
'هوشان',
color: Theme.of(context).colorScheme.title,
),
const DidvanText('هوشان'),
const SizedBox(
height: 24,
),
@ -74,8 +78,12 @@ class _AiState extends State<Ai> {
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(DidvanIcons.caret_down_solid),
Text(bot.name.toString()),
Icon(
DidvanIcons.caret_down_solid,
color: Theme.of(context).colorScheme.title,
),
DidvanText(bot.name.toString(),
color: Theme.of(context).colorScheme.title),
],
),
),
@ -177,7 +185,10 @@ class _AiState extends State<Ai> {
topLeft: Radius.circular(12),
bottomLeft: Radius.circular(12)),
boxShadow: DesignConfig.defaultShadow),
child: const Icon(DidvanIcons.angle_left_light),
child: Icon(
DidvanIcons.angle_left_light,
color: Theme.of(context).colorScheme.title,
),
)),
)
],

View File

@ -21,6 +21,7 @@ import 'package:didvan/views/widgets/didvan/icon_button.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:didvan/views/widgets/marquee_text.dart';
import 'package:didvan/views/widgets/skeleton_image.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
@ -324,8 +325,8 @@ class _AiChatPageState extends State<AiChatPage> {
Widget messageBubble(Prompts message, BuildContext context, AiChatState state,
int index, int mIndex) {
FilesModel? file =
message.file == null ? null : FilesModel(message.file.toString());
FilesModel? file = message.fileLocal ??
(message.file == null ? null : FilesModel(message.file.toString()));
MarkdownStyleSheet defaultMarkdownStyleSheet = MarkdownStyleSheet(
pPadding: const EdgeInsets.all(0.8),
@ -418,13 +419,13 @@ class _AiChatPageState extends State<AiChatPage> {
children: [
if (message.role.toString().contains('user') &&
file != null)
file.isAudio
file.isAudio()
? Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 8),
child: AudioWave(file: file.path),
)
: file.isImage
: file.isImage()
? Padding(
padding: const EdgeInsets.all(8.0),
child: messageImage(file),
@ -605,7 +606,7 @@ class _AiChatPageState extends State<AiChatPage> {
stop: const Duration(seconds: 3),
),
),
if (state.file != null)
if (state.file != null && !kIsWeb)
FutureBuilder(
future: state.file!.main.length(),
builder: (context, snapshot) {
@ -627,14 +628,18 @@ class _AiChatPageState extends State<AiChatPage> {
Widget messageImage(FilesModel file) {
return GestureDetector(
onTap: () => ActionSheetUtils(context).openInteractiveViewer(
context, file.path, !file.path.startsWith('/uploads')),
child: file.path.startsWith('/uploads')
? SkeletonImage(
pWidth: MediaQuery.sizeOf(context).width / 1,
pHeight: MediaQuery.sizeOf(context).height / 6,
imageUrl: file.path,
)
onTap: () => ActionSheetUtils(context)
.openInteractiveViewer(context, file.path, !file.isNetwork()),
child: file.isNetwork()
? file.path.startsWith('blob:')
? ClipRRect(
borderRadius: DesignConfig.lowBorderRadius,
child: Image.network(file.path))
: SkeletonImage(
pWidth: MediaQuery.sizeOf(context).width / 1,
pHeight: MediaQuery.sizeOf(context).height / 6,
imageUrl: file.path,
)
: ClipRRect(
borderRadius: DesignConfig.lowBorderRadius,
child: Image.file(file.main)),

View File

@ -1,6 +1,11 @@
import 'dart:async';
import 'dart:convert';
import 'package:didvan/main.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:persian_number_utility/persian_number_utility.dart';
import 'package:provider/provider.dart';
import 'package:didvan/models/ai/bots_model.dart';
import 'package:didvan/models/ai/chats_model.dart';
import 'package:didvan/models/ai/files_model.dart';
@ -14,15 +19,10 @@ import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart';
import 'package:didvan/utils/action_sheet.dart';
import 'package:didvan/views/ai/history_ai_chat_state.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:persian_number_utility/persian_number_utility.dart';
import 'package:provider/provider.dart';
class AiChatState extends CoreProvier {
ValueNotifier<Stream<String>> messageOnstream =
ValueNotifier(const Stream.empty());
ValueNotifier(const Stream<String>.empty().asBroadcastStream());
List<MessageModel> messages = [];
bool onResponsing = false;
bool loading = false;
@ -34,6 +34,11 @@ class AiChatState extends CoreProvier {
FilesModel? file;
TextEditingController message = TextEditingController();
@override
void dispose() {
super.dispose();
}
Future<void> _scrolledEnd() async {
WidgetsBinding.instance.addPostFrameCallback((_) async {
await scrollController.animateTo(
@ -53,7 +58,6 @@ class AiChatState extends CoreProvier {
await ActionSheetUtils(navigatorKey.currentContext!).showAlert(AlertData(
message: 'خطا در برقراری ارتباط', aLertType: ALertType.error));
update();
}
@ -94,21 +98,16 @@ class AiChatState extends CoreProvier {
messages.add(MessageModel(
dateTime: prompt.createdAt.toString(), prompts: [prompt]));
}
// chats.add("chats: $chats");
}
appState = AppState.idle;
loading = false;
onResponsing = false;
// Add this code to scroll to maxScrollExtent after the ListView is built
update();
return;
}
appState = AppState.failed;
loading = false;
onResponsing = false;
update();
}
@ -122,28 +121,22 @@ class AiChatState extends CoreProvier {
await service.put();
if (service.isSuccess) {
appState = AppState.idle;
// Add this code to scroll to maxScrollExtent after the ListView is built
if (chat == null) {
chat = ChatsModel(id: chatId, placeholder: placeholder);
} else {
chat = chat!.copyWith(placeholder: placeholder);
}
changingPlaceHolder.value = false;
update();
return;
}
appState = AppState.failed;
changingPlaceHolder.value = false;
update();
}
Future<void> postMessage(BotsModel bot) async {
onResponsing = true;
update();
String message = messages.last.prompts.last.text!;
@ -161,27 +154,50 @@ class AiChatState extends CoreProvier {
url: '/${bot.id}/${bot.name}'.toLowerCase(),
message: message,
chatId: chatId,
file: file?.main,
file: file,
edite: isEdite);
final res = await AiApiService.getResponse(req).catchError((e) {
_onError(e);
return e;
// return e;
});
String responseMessgae = '';
String dataMessgae = '';
file = null;
update();
var r = res.listen((value) async {
var str = utf8.decode(value);
if (str.contains('{{{')) {
dataMessgae += str;
update();
return;
}
responseMessgae += str;
messageOnstream.value = Stream.value(responseMessgae);
final stream = res
.transform(utf8.decoder)
.transform(const LineSplitter()); // <--- Add this line
final r = stream.listen((str) {
// var str = utf8.decode(value);
if (!kIsWeb) {
if (str.contains('{{{')) {
dataMessgae += str;
update();
return;
}
}
responseMessgae += str;
if (kIsWeb) {
try {
int startIndex = responseMessgae.indexOf('{{{');
// + 3 to include the }}} characters
String slicedText =
responseMessgae.substring(startIndex, responseMessgae.length);
dataMessgae = slicedText;
responseMessgae = responseMessgae.replaceAll(dataMessgae, '');
print(
"slicedText: $slicedText"); // Output: {{{ HUMAN_MESSAGE_ID: 2028, AI_MESSAGE_ID: 2029 }}}
} catch (e) {
e.printError();
}
}
messageOnstream.value = Stream.value(responseMessgae);
print("responseMessgae: $str");
// update();
});
@ -189,7 +205,6 @@ class AiChatState extends CoreProvier {
if (chatId == null) {
final service = await getChatId();
navigatorKey.currentContext!.read<HistoryAiChatState>().getChats();
if (!service.isSuccess) {
_onError(null);
return;
@ -201,7 +216,6 @@ class AiChatState extends CoreProvier {
messages.last.prompts.last =
messages.last.prompts.last.copyWith(error: true);
messageOnstream.value = const Stream.empty();
update();
_scrolledEnd();
return;
@ -214,10 +228,8 @@ class AiChatState extends CoreProvier {
aiMessageId = data['AI_MESSAGE_ID'];
} catch (e) {
e.printError();
return;
}
// Access the values
messages.last.prompts.last = messages.last.prompts.last
.copyWith(finished: true, text: responseMessgae, id: aiMessageId);
if (messages.last.prompts.length > 2) {
@ -241,7 +253,7 @@ class AiChatState extends CoreProvier {
_scrolledEnd();
});
r.onError(_onError);
r.onError((e) => _onError(e));
}
Future<void> deleteMessage(int id, int mIndex, int index) async {
@ -253,17 +265,13 @@ class AiChatState extends CoreProvier {
} else {
messages[mIndex].prompts.removeAt(index);
}
appState = AppState.idle;
update();
return;
}
appState = AppState.failed;
await ActionSheetUtils(navigatorKey.currentContext!).showAlert(AlertData(
message: 'خطا در برقراری ارتباط', aLertType: ALertType.error));
update();
}
}

View File

@ -27,6 +27,7 @@ import 'package:get/get.dart';
import 'package:image_cropper/image_cropper.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:persian_number_utility/persian_number_utility.dart';
import 'package:provider/provider.dart';
import 'package:record/record.dart';
@ -80,6 +81,7 @@ class AiMessageBar extends StatefulWidget {
class _AiMessageBarState extends State<AiMessageBar> {
final ValueNotifier<String> messageText = ValueNotifier('');
bool openAttach = false;
String? path;
@override
void initState() {
@ -159,8 +161,24 @@ class _AiMessageBarState extends State<AiMessageBar> {
FilePickerResult? result =
await MediaService.pickPdfFile();
if (result != null) {
state.file =
FilesModel(result.files.single.path!);
// if (kIsWeb) {
// Uint8List bytes = result.files.first
// .bytes!; // Access the bytes property
// File file = File.fromRawPath(bytes);
// state.file = FilesModel(file.path,
// name: result.files.first.name,
// bytes: bytes,
// audio: false,
// image: false);
// print(result.files.first.name);
// } else {
state.file = FilesModel(
result.files.single.path!,
audio: false,
image: false);
// }
openAttach = false;
}
Future.delayed(
@ -214,8 +232,10 @@ class _AiMessageBarState extends State<AiMessageBar> {
return;
}
state.file = kIsWeb
? FilesModel(pickedFile.path)
: FilesModel(file!.path);
? FilesModel(pickedFile.path,
image: true, audio: false)
: FilesModel(file!.path,
image: true, audio: false);
openAttach = false;
await Future.delayed(
Duration.zero,
@ -231,11 +251,28 @@ class _AiMessageBarState extends State<AiMessageBar> {
color: Colors.indigoAccent,
click: () async {
MediaService.onLoadingPickFile(context);
FilePickerResult? result =
await MediaService.pickAudioFile();
if (result != null) {
state.file =
FilesModel(result.files.single.path!);
if (kIsWeb) {
Uint8List bytes = result.files.first
.bytes!; // Access the bytes property
File file = File.fromRawPath(bytes);
state.file = FilesModel(file.path,
name: result.files.first.name,
bytes: bytes,
audio: true,
image: false);
print(result.files.first.name);
} else {
state.file = FilesModel(
result.files.single.path!,
audio: true,
image: false);
}
openAttach = false;
}
await Future.delayed(
@ -290,13 +327,16 @@ class _AiMessageBarState extends State<AiMessageBar> {
icon: DidvanIcons
.stop_circle_solid,
click: () async {
final path =
await record
.stop();
path = await record
.stop();
print(
"pathhhhhhh is: $path");
state.file = FilesModel(
path.toString(),
isRecorded: true);
isRecorded: true,
audio: true,
image: false);
_timer.cancel();
_countTimer.value = 0;
state.update();
@ -318,15 +358,29 @@ class _AiMessageBarState extends State<AiMessageBar> {
click: () async {
if (await record
.hasPermission()) {
Directory?
downloadDir =
await getApplicationDocumentsDirectory();
try {
String path =
'';
//
if (!kIsWeb) {
Directory?
downloadDir =
await getApplicationDocumentsDirectory();
path = p.join(
downloadDir
.path,
'${DateTime.now().millisecondsSinceEpoch ~/ 1000}.wav');
}
record.start(
const RecordConfig(),
path:
'${downloadDir.path}/${DateTime.now().millisecondsSinceEpoch ~/ 1000}.m4a');
startTimer();
record.start(
const RecordConfig(),
path:
path);
startTimer();
} catch (e) {
print(
'Error starting recording: $e');
}
}
},
)
@ -381,12 +435,8 @@ class _AiMessageBarState extends State<AiMessageBar> {
.file
?.path,
fileName: state
.file ==
null
? null
: p.basename(state
.file!
.path),
.file
?.basename,
finished:
true,
role: 'user',
@ -417,10 +467,11 @@ class _AiMessageBarState extends State<AiMessageBar> {
file: state
.file
?.path,
fileName: state.file ==
null
? null
: p.basename(state.file!.path),
fileName: state
.file
?.basename,
fileLocal:
state.file,
role:
'user',
createdAt: DateTime.now()
@ -715,10 +766,6 @@ class _AiMessageBarState extends State<AiMessageBar> {
AnimatedVisibility fileContainer() {
final state = context.watch<AiChatState>();
String basename = '';
if (state.file != null) {
basename = p.basename(state.file!.path);
}
return AnimatedVisibility(
isVisible: state.file != null && !state.file!.isRecorded,
duration: DesignConfig.lowAnimationDuration,
@ -731,16 +778,21 @@ class _AiMessageBarState extends State<AiMessageBar> {
margin: const EdgeInsets.only(bottom: 8, left: 12, right: 12),
child: Row(
children: [
state.file != null && state.file!.isImage
state.file != null && state.file!.isImage()
? SizedBox(
width: 32,
height: 42,
child: ClipRRect(
borderRadius: DesignConfig.lowBorderRadius,
child: Image.file(
state.file!.main,
fit: BoxFit.cover,
)))
child: state.file!.isNetwork()
? Image.network(
state.file!.path,
fit: BoxFit.cover,
)
: Image.file(
state.file!.main,
fit: BoxFit.cover,
)))
: const Icon(Icons.file_copy),
const SizedBox(
width: 12,
@ -752,12 +804,12 @@ class _AiMessageBarState extends State<AiMessageBar> {
SizedBox(
height: 24,
child: MarqueeText(
text: basename,
text: state.file != null ? state.file!.basename : '',
style: const TextStyle(fontSize: 14),
stop: const Duration(seconds: 3),
),
),
if (state.file != null)
if (state.file != null && !kIsWeb)
FutureBuilder(
future: state.file!.main.length(),
builder: (context, snapshot) {

View File

@ -27,7 +27,7 @@ class AudioWave extends StatefulWidget {
class _AudioWaveState extends State<AudioWave> {
final int itemCount = 35;
final AudioPlayer audioPlayer = AudioPlayer();
final AudioPlayer audioPlayer = AudioPlayer()..setPitch(1);
final ValueNotifier<List<double>> randoms = ValueNotifier([]);
final ValueNotifier<List<double>> randomsDisable = ValueNotifier([]);
@ -40,7 +40,9 @@ class _AudioWaveState extends State<AudioWave> {
void initState() {
super.initState();
try {
init();
WidgetsBinding.instance.addPostFrameCallback((_) async {
await init();
});
listeners();
} catch (e) {
if (kDebugMode) {
@ -54,19 +56,23 @@ class _AudioWaveState extends State<AudioWave> {
void setRandoms() {
for (var i = 0; i < itemCount; i++) {
randoms.value.add(0);
randomsDisable.value.add(5.74.w() * Random().nextDouble() + .26.w());
randomsDisable.value.add(2 + Random().nextDouble() * (42 - 2));
}
}
Future<void> init() async {
try {
final path = widget.file;
if (widget.file.startsWith('/uploads')) {
final audioSource = LockCachingAudioSource(Uri.parse(
if (widget.file.startsWith('blob:')) {
totalDuration = await audioPlayer
.setAudioSource(AudioSource.uri(Uri.parse(path))) ??
Duration.zero;
} else if (widget.file.startsWith('/uploads')) {
final audioSource = AudioSource.uri(Uri.parse(
'${RequestHelper.baseUrl + path}?accessToken=${RequestService.token}'));
await audioSource.request();
totalDuration =
await audioPlayer.setAudioSource(audioSource) ?? Duration.zero;
totalDuration = await audioPlayer.setUrl(
'${RequestHelper.baseUrl + path}?accessToken=${RequestService.token}') ??
Duration.zero;
} else {
totalDuration = await audioPlayer.setFilePath(path) ?? Duration.zero;
}
@ -114,7 +120,8 @@ class _AudioWaveState extends State<AudioWave> {
}
@override
void dispose() {
void dispose() async {
await audioPlayer.stop();
audioPlayer.dispose();
super.dispose();
}
@ -215,7 +222,7 @@ class _AudioWaveState extends State<AudioWave> {
Opacity(
opacity: 0,
child: Container(
width: 50.5.w(),
width: 12,
color: Colors.transparent
.withOpacity(1),
child: Theme(
@ -302,8 +309,8 @@ class _AudioWaveState extends State<AudioWave> {
children: values
.map(
(e) => Container(
margin: EdgeInsets.symmetric(horizontal: .2.w()),
width: .56.w(),
margin: const EdgeInsets.symmetric(horizontal: 1),
width: 2,
height: e,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(1000),

View File

@ -6,6 +6,7 @@ import 'package:didvan/providers/user.dart';
import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart';
import 'package:didvan/utils/action_sheet.dart';
import 'package:flutter/foundation.dart';
import 'package:home_widget/home_widget.dart';
class AuthenticationState extends CoreProvier {
@ -52,7 +53,9 @@ class AuthenticationState extends CoreProvier {
final token = service.result['token'];
await userProvider.setAndGetToken(newToken: token);
RequestService.token = token;
HomeWidget.saveWidgetData("token", token.toString());
if (!kIsWeb) {
HomeWidget.saveWidgetData("token", token.toString());
}
await userProvider.getUserInfo();
appState = AppState.idle;
return token;

View File

@ -79,7 +79,7 @@ class _UsernameInputState extends State<UsernameInput> {
.bodySmall!
.copyWith(color: Theme.of(context).colorScheme.primary),
recognizer: TapGestureRecognizer()
..onTap = () => launchUrlString(
..onTap = () => openInWebView(
'https://didvan.app/terms-of-use#conditions',
),
),
@ -91,7 +91,7 @@ class _UsernameInputState extends State<UsernameInput> {
.bodySmall!
.copyWith(color: Theme.of(context).colorScheme.primary),
recognizer: TapGestureRecognizer()
..onTap = () => launchUrlString(
..onTap = () => openInWebView(
'https://didvan.app/terms-of-use#privacy',
),
),

View File

@ -1,6 +1,7 @@
import 'package:didvan/models/customize_categories/favorites_response.dart';
import 'package:didvan/models/enums.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import '../../models/view/alert_data.dart';
import '../../providers/core.dart';
@ -100,7 +101,9 @@ class CustomizeCategoryState extends CoreProvier {
);
appState = AppState.idle;
await HomeWidgetRepository.fetchWidget();
if (!kIsWeb) {
await HomeWidgetRepository.fetchWidget();
}
return;
}
@ -134,7 +137,9 @@ class CustomizeCategoryState extends CoreProvier {
);
appState = AppState.idle;
await HomeWidgetRepository.fetchWidget();
if (!kIsWeb) {
await HomeWidgetRepository.fetchWidget();
}
return;
}

View File

@ -32,6 +32,7 @@ import 'package:didvan/views/widgets/didvan/bnb.dart';
import 'package:didvan/views/widgets/shimmer_placeholder.dart';
import 'package:didvan/views/widgets/state_handlers/empty_state.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
@ -52,7 +53,9 @@ class _HomeState extends State<Home>
@override
void initState() {
NotificationService.startListeningNotificationEvents();
if (!kIsWeb) {
NotificationService.startListeningNotificationEvents();
}
final state = context.read<HomeState>();
DesignConfig.updateSystemUiOverlayStyle();
@ -67,16 +70,17 @@ class _HomeState extends State<Home>
context.read<HistoryAiChatState>().getBots();
}
});
Future.delayed(Duration.zero, () {
HomeWidgetRepository.fetchWidget();
HomeWidgetRepository.decideWhereToGo();
NotificationMessage? data = HomeWidgetRepository.data;
if (data != null) {
HomeWidgetRepository.decideWhereToGoNotif();
}
AppInitializer.handleCLick(state, _tabController);
});
if (!kIsWeb) {
Future.delayed(Duration.zero, () {
HomeWidgetRepository.fetchWidget();
HomeWidgetRepository.decideWhereToGo();
NotificationMessage? data = HomeWidgetRepository.data;
if (data != null) {
HomeWidgetRepository.decideWhereToGoNotif();
}
AppInitializer.handleCLick(state, _tabController);
});
}
state.refresh();
context.read<ThemeProvider>().addListener(() {
state.refresh();
@ -120,8 +124,12 @@ class _HomeState extends State<Home>
Icon(
DidvanIcons.ai_solid,
size: MediaQuery.sizeOf(context).width / 5,
color: Theme.of(context).colorScheme.title,
),
DidvanText(
'هوشان',
color: Theme.of(context).colorScheme.title,
),
const DidvanText('هوشان'),
const SizedBox(
height: 24,
),
@ -569,7 +577,7 @@ class _HomeState extends State<Home>
Icon(
icon,
color: enable
? null
? Theme.of(context).colorScheme.title
: Theme.of(context).colorScheme.disabledText,
),
const SizedBox(
@ -580,7 +588,7 @@ class _HomeState extends State<Home>
DidvanText(text,
fontSize: 16,
color: enable
? null
? Theme.of(context).colorScheme.title
: Theme.of(context).colorScheme.disabledText),
// if (!enable) Text('در حال توسعه ...')
],

View File

@ -126,7 +126,7 @@ class _MainPageSection extends StatelessWidget {
void _moreHandler(BuildContext context) {
if (list.link.startsWith('http')) {
launchUrlString(
openInWebView(
'${list.link}?accessToken=${RequestService.token}',
mode: LaunchMode.inAppWebView,
);

View File

@ -99,7 +99,7 @@ class MainPageState extends CoreProvier {
return;
}
if (link.startsWith('http')) {
launchUrlString(
openInWebView(
'$link?accessToken=${RequestService.token}',
mode: LaunchMode.inAppWebView,
);

View File

@ -23,7 +23,7 @@ class MainPageBanner extends StatelessWidget {
onTap: () => item.link == null || item.link!.isEmpty
? ActionSheetUtils(context)
.openInteractiveViewer(context, item.image, false)
: launchUrlString(item.link!, mode: LaunchMode.inAppWebView),
: openInWebView(item.link!, mode: LaunchMode.inAppWebView),
child: SkeletonImage(
imageUrl: item.image,
),

View File

@ -138,7 +138,7 @@ class _NewStatisticState extends State<NewStatistic> {
child: ListView.builder(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
physics: const ScrollPhysics(),
physics: const BouncingScrollPhysics(),
itemCount: state.contents[id].contents.length,
itemBuilder: (context, index) => StatMainCard(
id: id,

View File

@ -90,7 +90,7 @@ class SearchResultItem extends StatelessWidget {
return;
}
if (_targetPageRouteName == null && item.link != null) {
launchUrlString(
openInWebView(
'${item.link!}?accessToken=${RequestService.token}',
mode: LaunchMode.inAppWebView,
);

View File

@ -13,7 +13,7 @@ class MainCategories extends StatelessWidget {
void _onTap(String link, BuildContext context) {
if (link.startsWith('http')) {
launchUrlString(
openInWebView(
'$link?accessToken=${RequestService.token}',
mode: LaunchMode.inAppWebView,
);

View File

@ -65,7 +65,7 @@ class StudioDetailsWidget extends StatelessWidget {
key: ValueKey(state.studio.id),
data: state.studio.description,
onAnchorTap: (href, _, __) =>
launchUrlString(href!),
openInWebView(href!),
style: {
'*': Style(
direction: TextDirection.rtl,

View File

@ -222,7 +222,7 @@ class _ProfilePageState extends State<ProfilePage> {
icon: DidvanIcons.info_circle_regular,
title: 'معرفی دیدوان',
onTap: () =>
launchUrlString('https://didvan.app/#info'),
openInWebView('https://didvan.app/#info'),
),
const DidvanDivider(),
MenuOption(
@ -338,7 +338,7 @@ class _ProfilePageState extends State<ProfilePage> {
MenuOption(
icon: DidvanIcons.alert_regular,
title: 'حریم خصوصی',
onTap: () => launchUrlString(
onTap: () => openInWebView(
'https://didvan.app/terms-of-use#privacy',
),
),

View File

@ -310,7 +310,7 @@ class _DidvanPageViewState extends State<DidvanPageView> {
),
);
} else {
launchUrlString(href);
openInWebView(href);
}
},
style: {

View File

@ -105,7 +105,7 @@ class MultitypeOverview extends StatelessWidget {
return;
}
if (_targetPageRouteName == null && item.link != null) {
launchUrlString(
openInWebView(
'${item.link!}?accessToken=${RequestService.token}',
mode: LaunchMode.inAppWebView,
);

View File

@ -1138,6 +1138,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.2"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3"
url: "https://pub.dev"
source: hosted
version: "6.3.0"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "17cd5e205ea615e2c6ea7a77323a11712dffa0720a8a90540db57a01347f9ad9"
url: "https://pub.dev"
source: hosted
version: "6.3.2"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e
url: "https://pub.dev"
source: hosted
version: "6.3.1"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af
url: "https://pub.dev"
source: hosted
version: "3.2.0"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e"
url: "https://pub.dev"
source: hosted
version: "2.3.3"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185"
url: "https://pub.dev"
source: hosted
version: "3.1.2"
uuid:
dependency: transitive
description:

View File

@ -92,6 +92,7 @@ dependencies:
mime: ^1.0.2
path: any
flutter_cache_manager: any
url_launcher: ^6.3.0
dev_dependencies:
flutter_test: