diff --git a/lib/app_router.dart b/lib/app_router.dart index 8cdc75a2..3c60e6e6 100644 --- a/lib/app_router.dart +++ b/lib/app_router.dart @@ -30,6 +30,7 @@ import 'package:eatvio_app/presentation/registration/medicine_intake_screen/medi import 'package:eatvio_app/presentation/registration/name_screen/name_screen.dart'; import 'package:eatvio_app/presentation/registration/weight_screen/weight_screen.dart'; import 'package:eatvio_app/widgets/bottom_navigation_bar_widget.dart'; +import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -241,6 +242,14 @@ class AppRouter { name: 'glossaryScreen', builder: (context, state) => GlossaryScreen.builder(context), ), + GoRoute( + path: '/photo-view-screen/:index', + name: 'photoViewScreen', + builder: (context, state) => PhotoViewScreen( + imageUrls: state.extra as List, + index: int.parse(state.pathParameters['index']!), + ), + ), ], ); } diff --git a/lib/features/chat/models/websocket_info_model.dart b/lib/features/chat/models/websocket_info_model.dart new file mode 100644 index 00000000..9fe8abc4 --- /dev/null +++ b/lib/features/chat/models/websocket_info_model.dart @@ -0,0 +1,12 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'websocket_info_model.freezed.dart'; + +part 'websocket_info_model.g.dart'; + +@freezed +class WebsocketInfoModel with _$WebsocketInfoModel { + const factory WebsocketInfoModel() = _WebsocketInfoModel; + + factory WebsocketInfoModel.fromJson(Map json) => _$WebsocketInfoModelFromJson(json); +} diff --git a/lib/features/chat/models/websocket_info_model.freezed.dart b/lib/features/chat/models/websocket_info_model.freezed.dart new file mode 100644 index 00000000..fd9ef4b9 --- /dev/null +++ b/lib/features/chat/models/websocket_info_model.freezed.dart @@ -0,0 +1,96 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'websocket_info_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +WebsocketInfoModel _$WebsocketInfoModelFromJson(Map json) { + return _WebsocketInfoModel.fromJson(json); +} + +/// @nodoc +mixin _$WebsocketInfoModel { + Map toJson() => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $WebsocketInfoModelCopyWith<$Res> { + factory $WebsocketInfoModelCopyWith( + WebsocketInfoModel value, $Res Function(WebsocketInfoModel) then) = + _$WebsocketInfoModelCopyWithImpl<$Res, WebsocketInfoModel>; +} + +/// @nodoc +class _$WebsocketInfoModelCopyWithImpl<$Res, $Val extends WebsocketInfoModel> + implements $WebsocketInfoModelCopyWith<$Res> { + _$WebsocketInfoModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$WebsocketInfoModelImplCopyWith<$Res> { + factory _$$WebsocketInfoModelImplCopyWith(_$WebsocketInfoModelImpl value, + $Res Function(_$WebsocketInfoModelImpl) then) = + __$$WebsocketInfoModelImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$WebsocketInfoModelImplCopyWithImpl<$Res> + extends _$WebsocketInfoModelCopyWithImpl<$Res, _$WebsocketInfoModelImpl> + implements _$$WebsocketInfoModelImplCopyWith<$Res> { + __$$WebsocketInfoModelImplCopyWithImpl(_$WebsocketInfoModelImpl _value, + $Res Function(_$WebsocketInfoModelImpl) _then) + : super(_value, _then); +} + +/// @nodoc +@JsonSerializable() +class _$WebsocketInfoModelImpl implements _WebsocketInfoModel { + const _$WebsocketInfoModelImpl(); + + factory _$WebsocketInfoModelImpl.fromJson(Map json) => + _$$WebsocketInfoModelImplFromJson(json); + + @override + String toString() { + return 'WebsocketInfoModel()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$WebsocketInfoModelImpl); + } + + @JsonKey(ignore: true) + @override + int get hashCode => runtimeType.hashCode; + + @override + Map toJson() { + return _$$WebsocketInfoModelImplToJson( + this, + ); + } +} + +abstract class _WebsocketInfoModel implements WebsocketInfoModel { + const factory _WebsocketInfoModel() = _$WebsocketInfoModelImpl; + + factory _WebsocketInfoModel.fromJson(Map json) = + _$WebsocketInfoModelImpl.fromJson; +} diff --git a/lib/features/chat/models/websocket_info_model.g.dart b/lib/features/chat/models/websocket_info_model.g.dart new file mode 100644 index 00000000..6573c3c7 --- /dev/null +++ b/lib/features/chat/models/websocket_info_model.g.dart @@ -0,0 +1,15 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'websocket_info_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$WebsocketInfoModelImpl _$$WebsocketInfoModelImplFromJson( + Map json) => + _$WebsocketInfoModelImpl(); + +Map _$$WebsocketInfoModelImplToJson( + _$WebsocketInfoModelImpl instance) => + {}; diff --git a/lib/features/chat/presentation/pages/chat_screen.dart b/lib/features/chat/presentation/pages/chat_screen.dart index 0095ac38..5cf8a485 100644 --- a/lib/features/chat/presentation/pages/chat_screen.dart +++ b/lib/features/chat/presentation/pages/chat_screen.dart @@ -2,7 +2,7 @@ import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_ import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_room_state.dart'; import 'package:eatvio_app/features/chat/presentation/widgets/chat_bubble_widget.dart'; import 'package:eatvio_app/locator_service.dart'; -import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter/cupertino.dart'; import 'package:eatvio_app/core/app_export.dart'; import 'package:flutter/material.dart'; @@ -22,7 +22,7 @@ class ChatScreen extends StatelessWidget { if (state is ChatRoomStateLoading) { return Scaffold( body: Center( - child: CircularProgressIndicator(), + child: CupertinoActivityIndicator(), ), ); } else if (state is ChatRoomStateLoaded) { @@ -32,7 +32,7 @@ class ChatScreen extends StatelessWidget { } else { return Scaffold( body: Center( - child: CircularProgressIndicator(), + child: CupertinoActivityIndicator(), ), ); } @@ -49,22 +49,34 @@ class ChatScreen extends StatelessWidget { appBar: AppBar( titleSpacing: 0, centerTitle: false, + backgroundColor: Colors.white, + forceMaterialTransparency: true, + elevation: 0, title: Row( children: [ - ClipOval( - child: SvgPicture.network( - width: 40, - height: 40, - state.chatRoomModel.users['to_chat_user']!.profilePhotoUrl!, - placeholderBuilder: (context) => CircleAvatar( - foregroundColor: PrimaryColors().backgroundAvatar, + Container( + width: 40, + height: 40, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Color(0xFFEBF4FF), + shape: BoxShape.circle, + ), + child: Text( + state.chatRoomModel.users['to_chat_user']!.name.split(' ').first[0] + state.chatRoomModel.users['to_chat_user']!.name.split(' ')[1][0], + style: TextStyle( + color: Color(0xFF7F9CF5), + fontWeight: FontWeight.w400, + fontSize: 20, ), ), ), SizedBox(width: 16), Text( state.chatRoomModel.users['to_chat_user']!.name, - style: CustomTextStyles.titleSmallBold.copyWith(color: PrimaryColors().blueGray800), + style: CustomTextStyles.titleSmallBold.copyWith( + color: PrimaryColors().blueGray800, + ), ), ], ), @@ -74,7 +86,7 @@ class ChatScreen extends StatelessWidget { children: [ Expanded( child: Container( - color: Colors.white, + color: Color(0xFFF0F2F5), width: double.maxFinite, child: ListView.builder( reverse: true, @@ -83,7 +95,9 @@ class ChatScreen extends StatelessWidget { itemBuilder: (BuildContext context, int index) { return ChatBubbleWidget( text: state.chatRoomModel.messages[state.chatRoomModel.messages.length - index - 1].data, - isCurrentUser: state.chatRoomModel.users['to_chat_user']!.id != state.chatRoomModel.messages[index].authorId, + isCurrentUser: + state.chatRoomModel.users['to_chat_user']!.id != state.chatRoomModel.messages[state.chatRoomModel.messages.length - index - 1].authorId, + mediaData: state.chatRoomModel.messages[state.chatRoomModel.messages.length - index - 1].mediaData, ); }, itemCount: state.chatRoomModel.messages.length, diff --git a/lib/features/chat/presentation/pages/chat_select_screen/chat_search_view.dart b/lib/features/chat/presentation/pages/chat_select_screen/chat_search_view.dart index b9447660..939bb42d 100644 --- a/lib/features/chat/presentation/pages/chat_select_screen/chat_search_view.dart +++ b/lib/features/chat/presentation/pages/chat_select_screen/chat_search_view.dart @@ -73,18 +73,13 @@ class CustomSearchView extends StatelessWidget { @override Widget build(BuildContext context) { - return alignment != null - ? Align( - alignment: alignment ?? Alignment.center, - child: searchViewWidget(context)) - : searchViewWidget(context); + return alignment != null ? Align(alignment: alignment ?? Alignment.center, child: searchViewWidget(context)) : searchViewWidget(context); } searchViewWidget(BuildContext context) => SizedBox( width: width ?? double.maxFinite, child: TextFormField( - scrollPadding: - EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + scrollPadding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), controller: controller, focusNode: focusNode, onTapOutside: (event) => onTapOutside(), @@ -110,26 +105,19 @@ class CustomSearchView extends StatelessWidget { hintText: hintText ?? "", hintStyle: hintStyle ?? CustomTextStyles.bodySmallMulishBluegray800, prefixIcon: prefix ?? - SizedBox( - child: Padding( - padding: EdgeInsets.only(left: 10), - child: Icon(Icons.search)), - ), - prefixIconConstraints: prefixConstraints ?? - BoxConstraints( - maxHeight: 40.v, + Padding( + padding: EdgeInsets.only(left: 10), + child: Icon( + Icons.search, + color: Color(0xFF7B8794), + ), ), suffixIcon: suffix ?? - Padding( - padding: EdgeInsets.only( - right: 15.h, - ), - child: IconButton( - onPressed: () => controller!.clear(), - icon: Icon( - Icons.clear, - color: Colors.grey.shade600, - ), + IconButton( + onPressed: () => controller!.clear(), + icon: Icon( + Icons.clear, + color: Colors.grey.shade600, ), ), suffixIconConstraints: suffixConstraints ?? @@ -137,12 +125,7 @@ class CustomSearchView extends StatelessWidget { maxHeight: 40.v, ), isDense: true, - contentPadding: contentPadding ?? - EdgeInsets.only( - top: 11.v, - right: 11.h, - bottom: 11.v, - ), + contentPadding: EdgeInsets.all(16), fillColor: fillColor ?? appTheme.gray100, filled: filled, border: borderDecoration ?? diff --git a/lib/features/chat/presentation/pages/chat_select_screen/chat_select_screen.dart b/lib/features/chat/presentation/pages/chat_select_screen/chat_select_screen.dart index e1c69857..ef5d8b8e 100644 --- a/lib/features/chat/presentation/pages/chat_select_screen/chat_select_screen.dart +++ b/lib/features/chat/presentation/pages/chat_select_screen/chat_select_screen.dart @@ -1,6 +1,6 @@ - import 'package:eatvio_app/features/chat/presentation/pages/chat_select_screen/models/chat_entry.dart'; import 'package:eatvio_app/features/chat/presentation/pages/chat_select_screen/widgets/chat_entry_widget.dart'; +import 'package:flutter/cupertino.dart'; import 'bloc/chat_select_bloc.dart'; import 'models/chat_select_model.dart'; @@ -13,8 +13,11 @@ class ChatSelectScreen extends StatelessWidget { static Widget builder(BuildContext context) { return BlocProvider( - create: (context) => ChatSelectBloc(ChatSelectState(chatSelectModelObj: ChatSelectModel())) - ..add( + create: (context) => ChatSelectBloc( + ChatSelectState( + chatSelectModelObj: ChatSelectModel(), + ), + )..add( ChatSelectInitialEvent(), ), child: ChatSelectScreen(), @@ -25,74 +28,105 @@ class ChatSelectScreen extends StatelessWidget { return BlocBuilder( builder: (context, state) { if (state is ChatSelectInitialState) { - // Это начальное состояние, данные ещё не загружены. - return Container( - color: Colors.white, + return Scaffold( + body: Center( + child: CupertinoActivityIndicator(), + ), ); } else if (state is ChatSelectLoadedState) { - // Данные загружены, можно строить экран. - return _buildScreen(context, state); + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.white, + forceMaterialTransparency: true, + toolbarHeight: 0, + ), + body: NestedScrollView( + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + return [ + SliverAppBar( + backgroundColor: Colors.white, + forceMaterialTransparency: true, + floating: true, + snap: true, + title: CustomSearchView( + controller: state.searchController, + fillColor: Color(0xFFF0F2F5), + hintText: "Поиск", + hintStyle: TextStyle( + color: Color(0xFF7B8794), + ), + ), + ), + ]; + }, + body: _buildChatList(context, state), + ), + ); } else if (state is ChatSelectErrorState) { - // Произошла ошибка при загрузке данных. return Center(child: Text(state.errorMessage)); } else { - // Другие состояния, если они есть. - return Container(); // Пустой контейнер для необработанных состояний. + return Scaffold( + body: Center( + child: CupertinoActivityIndicator(), + ), + ); } }, ); } +} - _buildScreen(BuildContext context, ChatSelectLoadedState state) { - return Scaffold( - resizeToAvoidBottomInset: false, - body: Container( - width: double.maxFinite, - decoration: AppDecoration.fillGray, - child: ListView( - children: [ - SizedBox(height: 30.v), - Container( - margin: EdgeInsets.only( - left: 16.h, - right: 20.h, - bottom: 10.h, - ), - padding: EdgeInsets.all(12.h), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: const BorderRadius.all(Radius.circular(12)), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 2, - blurRadius: 5, - offset: Offset(0, 3), - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - CustomSearchView( - controller: state.searchController, - hintText: "Поиск", - ), - SizedBox(height: 20.v), - _buildChatList(context, state), - ], - ), - ) - ], - ), +_buildScreen(BuildContext context, ChatSelectLoadedState state) { + return Scaffold( + resizeToAvoidBottomInset: false, + body: Container( + width: double.maxFinite, + decoration: AppDecoration.fillGray, + child: ListView( + children: [ + SizedBox(height: 30.v), + Container( + margin: EdgeInsets.only( + left: 16.h, + right: 20.h, + bottom: 10.h, + ), + padding: EdgeInsets.all(12.h), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CustomSearchView( + controller: state.searchController, + hintText: "Поиск", + ), + SizedBox(height: 20.v), + _buildChatList(context, state), + ], + ), + ) + ], ), - ); - } + ), + ); } _buildChatList(BuildContext context, ChatSelectLoadedState state) { return ListView.separated( + padding: EdgeInsets.all(16), shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), separatorBuilder: (context, index) { return SizedBox( height: 10.v, @@ -102,7 +136,7 @@ _buildChatList(BuildContext context, ChatSelectLoadedState state) { itemBuilder: (context, index) { ChatItemModel model = state.chatRooms?[index]; return ChatEntryWidget( - model, + chatEntryObj: model, ); }, ); diff --git a/lib/features/chat/presentation/pages/chat_select_screen/widgets/chat_entry_widget.dart b/lib/features/chat/presentation/pages/chat_select_screen/widgets/chat_entry_widget.dart index 6175a40e..fbd6b1b0 100644 --- a/lib/features/chat/presentation/pages/chat_select_screen/widgets/chat_entry_widget.dart +++ b/lib/features/chat/presentation/pages/chat_select_screen/widgets/chat_entry_widget.dart @@ -2,16 +2,14 @@ import 'package:eatvio_app/features/chat/presentation/pages/chat_select_screen/models/chat_entry.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:eatvio_app/core/app_export.dart'; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:go_router/go_router.dart'; class ChatEntryWidget extends StatelessWidget { - ChatEntryWidget(this.chatEntryObj, {Key? key}) - : super( - key: key, - ); + ChatEntryWidget({ + required this.chatEntryObj, + super.key, + }); ChatItemModel chatEntryObj; @@ -23,81 +21,68 @@ class ChatEntryWidget extends StatelessWidget { extra: chatEntryObj.id.toString(), ), child: Container( - padding: EdgeInsets.all(8.h), - decoration: AppDecoration.fillGray100.copyWith( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( borderRadius: BorderRadiusStyle.circleBorder8, + color: Color(0xFFF6FAFF), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ - CachedNetworkImage( - imageUrl: dotenv.env['STORAGE_URL']! + chatEntryObj.userImage!, - imageBuilder: (context, imageProvider) => CircleAvatar( - backgroundImage: imageProvider, - radius: 25, + Container( + width: 48, + height: 48, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Color(0xFFEBF4FF), + shape: BoxShape.circle, ), - placeholder: (context, url) => CircularProgressIndicator(), - errorWidget: (context, url, error) => CircleAvatar( - child: Icon(Icons.person), - backgroundColor: Colors.white, - radius: 25, + child: Text( + chatEntryObj.username!.split(' ').first[0] + chatEntryObj.username!.split(' ')[1][0], + style: TextStyle( + color: Color(0xFF7F9CF5), + fontWeight: FontWeight.w400, + fontSize: 20, + ), ), ), - /*CustomImageView( - imagePath: - dotenv.env['STORAGE_URL']! + chatEntryObj.userImage!, - height: 60.adaptSize, - width: 60.adaptSize, - radius: BorderRadius.circular( - 30.h, - ), - ),*/ + SizedBox(width: 8), Expanded( - child: Padding( - padding: EdgeInsets.only( - left: 12.h, - top: 2.v, - bottom: 11.v, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 227.h, - child: Row( - children: [ - Text( - chatEntryObj.username!.length <= 15 ? chatEntryObj.username! : chatEntryObj.username!.substring(0, 15) + '...', - style: theme.textTheme.titleMedium, - ), - Spacer(), - CustomImageView( - imagePath: ImageConstant.imageNotFound, - height: 9.v, - width: 16.h, - margin: EdgeInsets.only( - top: 4.v, - bottom: 6.v, - ), - ), - Padding( - padding: EdgeInsets.only(left: 6.h), - child: Text( - chatEntryObj.dateText!, - style: theme.textTheme.bodyMedium, - ), - ) - ], + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + chatEntryObj.username!.length <= 15 ? chatEntryObj.username! : chatEntryObj.username!.substring(0, 15) + '...', + style: theme.textTheme.titleMedium, ), - ), - SizedBox(height: 6.v), - Text( - chatEntryObj.greetingText!, - style: CustomTextStyles.bodyMediumBluegray800, - ) - ], - ), + Spacer(), + CustomImageView( + imagePath: ImageConstant.imageNotFound, + height: 9.v, + width: 16.h, + margin: EdgeInsets.only( + top: 4.v, + bottom: 6.v, + ), + ), + Padding( + padding: EdgeInsets.only(left: 6.h), + child: Text( + chatEntryObj.dateText!, + style: theme.textTheme.bodyMedium, + ), + ) + ], + ), + SizedBox(height: 6.v), + Text( + chatEntryObj.greetingText!, + style: CustomTextStyles.bodyMediumBluegray800, + ), + ], ), ) ], diff --git a/lib/features/chat/presentation/pages/photo_view_screen.dart b/lib/features/chat/presentation/pages/photo_view_screen.dart new file mode 100644 index 00000000..b9e4c595 --- /dev/null +++ b/lib/features/chat/presentation/pages/photo_view_screen.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:go_router/go_router.dart'; +import 'package:photo_view/photo_view.dart'; +import 'package:photo_view/photo_view_gallery.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class PhotoViewScreen extends StatefulWidget { + final List imageUrls; + final int index; + + const PhotoViewScreen({ + super.key, + required this.imageUrls, + required this.index, + }); + + @override + State createState() => _PhotoViewScreenState(); +} + +class _PhotoViewScreenState extends State { + late final pageController; + int imageIndex = 0; + + @override + void initState() { + imageIndex = widget.index; + pageController = PageController(initialPage: imageIndex); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + centerTitle: true, + systemOverlayStyle: SystemUiOverlayStyle( + statusBarColor: Colors.black, + statusBarBrightness: Brightness.dark, + statusBarIconBrightness: Brightness.light, + ), + backgroundColor: Colors.black, + foregroundColor: Colors.white, + title: Text( + (imageIndex + 1).toString() + ' из ' + widget.imageUrls.length.toString(), + ), + ), + body: Dismissible( + direction: DismissDirection.vertical, + key: const Key('key'), + onDismissed: (_) => GoRouter.of(context).pop(), + resizeDuration: Duration(microseconds: 1), + child: Container( + child: PhotoViewGallery.builder( + scrollPhysics: const BouncingScrollPhysics(), + builder: (BuildContext context, int index) { + return PhotoViewGalleryPageOptions( + imageProvider: NetworkImage(widget.imageUrls[index]), + initialScale: PhotoViewComputedScale.contained, + minScale: PhotoViewComputedScale.contained, + heroAttributes: PhotoViewHeroAttributes(tag: widget.imageUrls[index]), + errorBuilder: (context, object, stackTrace) { + return Column( + children: [ + SizedBox(height: 32), + Text('Файл'), + SizedBox(height: 8), + ListTile( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + contentPadding: EdgeInsets.all(16), + onTap: () async { + if (!await launchUrl(Uri.parse(widget.imageUrls[index]))) { + throw Exception('Could not launch ${widget.imageUrls[index]}'); + } + }, + leading: Icon( + Icons.file_copy_outlined, + color: Colors.white, + ), + trailing: Icon( + Icons.download_outlined, + color: Colors.white, + ), + title: Text( + widget.imageUrls[index].replaceAll(dotenv.env['STORAGE_URL']! + 'chat_files/', ''), + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ), + ], + ); + }, + ); + }, + itemCount: widget.imageUrls.length, + loadingBuilder: (context, event) => Center( + child: Container( + width: 20.0, + height: 20.0, + child: CircularProgressIndicator( + value: event == null ? 0 : event.cumulativeBytesLoaded / event.expectedTotalBytes!, + ), + ), + ), + pageController: pageController, + onPageChanged: (page) { + setState(() { + imageIndex = page; + }); + }, + ), + ), + ), + ); + } +} diff --git a/lib/features/chat/presentation/widgets/chat_bubble_widget.dart b/lib/features/chat/presentation/widgets/chat_bubble_widget.dart index 88916873..3a13dd8c 100644 --- a/lib/features/chat/presentation/widgets/chat_bubble_widget.dart +++ b/lib/features/chat/presentation/widgets/chat_bubble_widget.dart @@ -1,14 +1,20 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:go_router/go_router.dart'; +import 'package:url_launcher/url_launcher.dart'; class ChatBubbleWidget extends StatelessWidget { const ChatBubbleWidget({ super.key, required this.text, required this.isCurrentUser, + this.mediaData, }); final String text; final bool isCurrentUser; + final List? mediaData; @override Widget build(BuildContext context) { @@ -18,16 +24,80 @@ class ChatBubbleWidget extends StatelessWidget { alignment: isCurrentUser ? Alignment.centerRight : Alignment.centerLeft, child: DecoratedBox( decoration: BoxDecoration( - color: isCurrentUser ? Colors.blue : Colors.grey[300], + color: isCurrentUser ? Color(0xFFF6FAFF) : Colors.white, borderRadius: BorderRadius.circular(16), ), child: Padding( padding: const EdgeInsets.all(12), - child: Text( - text, - style: TextStyle( - color: isCurrentUser ? Colors.white : Colors.black87, - ), + child: Column( + children: [ + if (mediaData != null) + ListView.separated( + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemBuilder: (context, index) { + if (!mediaData![index].toString().contains('.png') && + !mediaData![index].toString().contains('.jpg') && + !mediaData![index].toString().contains('.jpeg')) { + return ListTile( + contentPadding: EdgeInsets.zero, + horizontalTitleGap: 8, + onTap: () async { + if (!await launchUrl(Uri.parse(dotenv.env['STORAGE_URL']! + mediaData![index].toString()))) { + throw Exception('Could not launch ${mediaData![index].toString()}'); + } + }, + leading: Icon( + Icons.file_copy_outlined, + color: Color(0xFF2F80ED), + ), + trailing: Icon( + Icons.download_outlined, + color: Color(0xFF2F80ED), + ), + title: Text( + mediaData![index].toString().replaceAll('chat_files/', ''), + maxLines: 2, + style: TextStyle( + color: Color(0xFF323F4B), + fontSize: 12, + overflow: TextOverflow.ellipsis, + ), + ), + ); + } + return GestureDetector( + onTap: () { + GoRouter.of(context).pushNamed( + 'photoViewScreen', + extra: mediaData!.map((e) => dotenv.env['STORAGE_URL']! + e.toString()).toList(), + pathParameters: { + 'index': index.toString(), + }, + ); + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + '${mediaData![index]}', + ), + ), + ); + }, + separatorBuilder: (context, index) { + return SizedBox(height: 8); + }, + itemCount: mediaData!.length, + ), + if (mediaData!.length > 0 && text.isNotEmpty) SizedBox(height: 8), + if (text.isNotEmpty) + Text( + text, + style: TextStyle( + color: Color(0xFF323F4B), + ), + ), + ], ), ), ), diff --git a/lib/features/chat/repositories/chat_repository.dart b/lib/features/chat/repositories/chat_repository.dart index 2b4b87a6..137aa878 100644 --- a/lib/features/chat/repositories/chat_repository.dart +++ b/lib/features/chat/repositories/chat_repository.dart @@ -1,4 +1,5 @@ import 'package:eatvio_app/features/chat/models/chat_room_model.dart'; +import 'package:eatvio_app/features/chat/models/websocket_info_model.dart'; import 'package:eatvio_app/features/chat/services/chat_service.dart'; class ChatRepository { @@ -17,4 +18,14 @@ class ChatRepository { throw (e); } } + + /// TODO use "dartz" package because need Either for error handling + Future getWebsocket() async { + try { + return (await chatService.getWebsocket()); + } catch (e) { + print(e); + throw (e); + } + } } diff --git a/lib/features/chat/services/chat_service.dart b/lib/features/chat/services/chat_service.dart index 706ecee1..fc213667 100644 --- a/lib/features/chat/services/chat_service.dart +++ b/lib/features/chat/services/chat_service.dart @@ -1,5 +1,6 @@ import 'package:eatvio_app/features/chat/models/chat_room_model.dart'; import 'package:dio/dio.dart'; +import 'package:eatvio_app/features/chat/models/websocket_info_model.dart'; import 'package:retrofit/retrofit.dart'; part 'chat_service.g.dart'; @@ -10,4 +11,7 @@ abstract class ChatService { @GET('/chat/show/{id}') Future> getChatRoomModel(@Path() String id); + + @GET('/profile/ws/params') + Future getWebsocket(); } diff --git a/lib/features/chat/services/chat_service.g.dart b/lib/features/chat/services/chat_service.g.dart index 435965f3..db9fdea8 100644 --- a/lib/features/chat/services/chat_service.g.dart +++ b/lib/features/chat/services/chat_service.g.dart @@ -46,6 +46,33 @@ class _ChatService implements ChatService { return value; } + @override + Future getWebsocket() async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _result = await _dio + .fetch>(_setStreamType(Options( + method: 'GET', + headers: _headers, + extra: _extra, + ) + .compose( + _dio.options, + '/profile/ws/params', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls( + _dio.options.baseUrl, + baseUrl, + )))); + final value = WebsocketInfoModel.fromJson(_result.data!); + return value; + } + RequestOptions _setStreamType(RequestOptions requestOptions) { if (T != dynamic && !(requestOptions.responseType == ResponseType.bytes || diff --git a/pubspec.yaml b/pubspec.yaml index 281a964a..018e7418 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,11 @@ dependencies: get_it: ^7.7.0 dio: ^5.4.3+1 + # photo viewer + photo_view: ^0.15.0 + #url launcher + url_launcher: ^6.3.0 + dev_dependencies: dart_code_metrics: ^5.7.6 dart_code_metrics_presets: ^1.8.0