Merge pull request 'main' (#37) from main into diary_refactor

Reviewed-on: maxxkim/eatvio_app#37
This commit is contained in:
2024-07-03 08:54:03 +00:00
21 changed files with 193 additions and 76 deletions

View File

@@ -0,0 +1,16 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part '../../../presentation/feed/blog_post_create_screen/models/blog_post_create_credentials_model.freezed.dart';
part '../../../presentation/feed/blog_post_create_screen/models/blog_post_create_credentials_model.g.dart';
@freezed
class BlogPostCreateCredentialsModel with _$BlogPostCreateCredentialsModel {
const factory BlogPostCreateCredentialsModel({
required String title,
required String description,
@JsonKey(name: 'youtube_url') required String youtubeUrl,
@Default('active') String status,
}) = _BlogPostCreateCredentialsModel;
factory BlogPostCreateCredentialsModel.fromJson(Map<String, Object?> json) =>
_$BlogPostCreateCredentialsModelFromJson(json);
}

View File

@@ -0,0 +1,239 @@
import '../../../../presentation/feed/blog_post_create_screen/bloc/blog_post_create_bloc.dart';
import '../../../../presentation/feed/blog_post_create_screen/models/blog_post_create_model.dart';
import 'package:eatvio_app/core/app_export.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:eatvio_app/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/widgets/custom_outlined_button.dart';
import 'dart:io';
class BlogPostCreateScreen extends StatelessWidget {
const BlogPostCreateScreen({Key? key}) : super(key: key);
static Widget builder(BuildContext context) {
final blog_postID = ModalRoute.of(context)!.settings.arguments;
return BlocProvider<BlogPostCreateBloc>(
create: (context) => BlogPostCreateBloc(BlogPostCreateState(
blog_postModelObj:
BlogPostCreateModel(blog_postID: blog_postID.toString())))
..add(BlogPostCreateInitialEvent()),
child: BlogPostCreateScreen(),
);
}
Widget build(BuildContext context) {
return BlocBuilder<BlogPostCreateBloc, BlogPostCreateState>(
builder: (context, state) {
if (state is BlogPostCreateInitialState) {
// Это начальное состояние, данные ещё не загружены.
return Container(
color: Colors.white,
);
} else if (state is BlogPostCreateLoadedState) {
// Данные загружены, можно строить экран.
return _buildBlogPostCreate(context, state);
} else if (state is BlogPostCreateErrorState) {
// Произошла ошибка при загрузке данных.
return Center(child: Text(state.errorMessage));
} else {
// Другие состояния, если они есть.
return Container(); // Пустой контейнер для необработанных состояний.
}
},
);
}
_buildBlogPostCreate(BuildContext context, BlogPostCreateLoadedState state) {
print(state.image);
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: [
Column(
children: <Widget>[
Text("Заголовок новости",
style: theme.textTheme.bodyLarge),
CustomTextFormField(
controller: state.title,
validator: _validateTitle,
),
SizedBox(height: 8.0),
Text("Ссылка на YouTube",
style: theme.textTheme.bodyLarge),
CustomTextFormField(
controller: state.youtubeURL,
validator: _validateYouTubeLink,
),
SizedBox(height: 8.0),
Text("Описание", style: theme.textTheme.bodyLarge),
CustomTextFormField(
controller: state.description,
validator: _validateDescription,
maxLines: 6,
),
SizedBox(height: 16.0),
_buildAddPhoto(context, state),
SizedBox(height: 16.0),
// Add widget for image upload here
CustomOutlinedButton(
onPressed: () => _submitForm(context, state.title!.text,
state.description!.text, state.youtubeURL!.text),
buttonStyle: CustomButtonStyles.outlinePrimaryTL15,
text: 'Создать',
),
],
),
],
),
),
],
),
),
);
}
}
_submitForm(
BuildContext context, String title, String description, String youtubeURL) {
String? titleError = _validateTitle(title);
String? descriptionError = _validateDescription(description);
String? youtubeURLError = _validateYouTubeLink(youtubeURL);
if (titleError != null ||
descriptionError != null ||
youtubeURLError != null) {
// Display error messages or handle invalid input as needed
// For example, show a snackbar with the error message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(titleError ?? descriptionError ?? youtubeURLError!)),
);
} else {
// Proceed with form submission
context.read<BlogPostCreateBloc>().add(CreateBlogPost());
}
}
_buildAddPhoto(BuildContext context, BlogPostCreateLoadedState state) {
bool isLoading = state.isLoading;
return GestureDetector(
onTap: addImage(context, isLoading),
child: Stack(
children: [
if (state.image == null)
Container(
margin: EdgeInsets.symmetric(horizontal: 10.h),
padding: EdgeInsets.symmetric(
horizontal: 40.h,
vertical: 80.v,
),
decoration: AppDecoration.fillGray.copyWith(
borderRadius: BorderRadiusStyle.roundedBorder5,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: EdgeInsets.only(
left: 7.h,
top: 5.v,
),
child: Text(
"Добавить фото",
selectionColor: appTheme.blueA400,
),
),
Spacer(),
Icon(
Icons.photo_library,
color: appTheme.blueGray300,
),
],
),
),
if (state.image != null)
Container(
margin: EdgeInsets.symmetric(horizontal: 10.h),
padding: EdgeInsets.symmetric(
horizontal: 40.h,
vertical: 80.v,
),
decoration: AppDecoration.fillGray.copyWith(
borderRadius: BorderRadiusStyle.roundedBorder5,
),
child: Image(image: FileImage(File(state.image!.path))),
)
],
),
);
}
addImage(BuildContext context, bool isLoading) {
if (!isLoading) {
return () async {
final picker = ImagePicker();
List<XFile>? images = await picker.pickMultiImage();
if (images.isNotEmpty) {
for (var image in images) {
context.read<BlogPostCreateBloc>().add(UploadImages(image));
}
}
};
}
}
String? _validateTitle(value) {
if (value!.isEmpty) {
return 'Пожалуйста, введите заголовок';
}
return null;
}
String? _validateDescription(value) {
if (value!.isEmpty) {
return 'Пожалуйста, введите описание';
}
return null;
}
String? _validateYouTubeLink(value) {
if (value!.isEmpty) {
return 'Пожалуйста, введите ссылку';
} else if (!_isValidUrl(value)) {
return 'Пожалуйста, введите корректную ссылку';
}
return null;
}
bool _isValidUrl(String url) {
final RegExp urlRegExp =
new RegExp(r"^(http|https):\/\/([\w-]+\.)+[\w-]+(\/[\w-./?%&=]*)?$");
return urlRegExp.hasMatch(url);
}

View File

@@ -0,0 +1,20 @@
import 'package:eatvio_app/features/blog_post_create/models/blog_post_create_credentials_model.dart';
import 'package:eatvio_app/features/blog_post_create/services/blog_service.dart';
class BlogRepository {
final BlogService blogService;
BlogRepository({
required this.blogService,
});
Future<BlogPostCreateCredentialsModel> createBlogPost(
BlogPostCreateCredentialsModel model) async {
try {
return (await blogService.createBlogPost(model));
} catch (e) {
print(e);
throw (e);
}
}
}

View File

@@ -0,0 +1,14 @@
import 'package:eatvio_app/features/blog_post_create/models/blog_post_create_credentials_model.dart';
import 'package:dio/dio.dart';
import 'package:retrofit/retrofit.dart';
part 'blog_service.g.dart';
@RestApi()
abstract class BlogService {
factory BlogService(Dio dio, {String baseUrl}) = _BlogService;
@POST('/blog_post/create')
Future<BlogPostCreateCredentialsModel> createBlogPost(
BlogPostCreateCredentialsModel model);
}

View File

@@ -0,0 +1,78 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'blog_service.dart';
// **************************************************************************
// RetrofitGenerator
// **************************************************************************
// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers
class _BlogService implements BlogService {
_BlogService(
this._dio, {
this.baseUrl,
});
final Dio _dio;
String? baseUrl;
@override
Future<BlogPostCreateCredentialsModel> createBlogPost(
BlogPostCreateCredentialsModel model) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _result = await _dio.fetch<Map<String, dynamic>>(
_setStreamType<BlogPostCreateCredentialsModel>(Options(
method: 'POST',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/blog_post/create',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
))));
final value = BlogPostCreateCredentialsModel.fromJson(_result.data!);
return value;
}
RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
if (T != dynamic &&
!(requestOptions.responseType == ResponseType.bytes ||
requestOptions.responseType == ResponseType.stream)) {
if (T == String) {
requestOptions.responseType = ResponseType.plain;
} else {
requestOptions.responseType = ResponseType.json;
}
}
return requestOptions;
}
String _combineBaseUrls(
String dioBaseUrl,
String? baseUrl,
) {
if (baseUrl == null || baseUrl.trim().isEmpty) {
return dioBaseUrl;
}
final url = Uri.parse(baseUrl);
if (url.isAbsolute) {
return url.toString();
}
return Uri.parse(dioBaseUrl).resolveUri(url).toString();
}
}

View File

@@ -10,6 +10,9 @@ class WebsocketInfoModel with _$WebsocketInfoModel {
required String key,
required String host,
required String port,
required String cluster,
@JsonKey(name: 'user_id')
required int userId,
}) = _WebsocketInfoModel;
factory WebsocketInfoModel.fromJson(Map<String, Object?> json) => _$WebsocketInfoModelFromJson(json);

View File

@@ -23,6 +23,9 @@ mixin _$WebsocketInfoModel {
String get key => throw _privateConstructorUsedError;
String get host => throw _privateConstructorUsedError;
String get port => throw _privateConstructorUsedError;
String get cluster => throw _privateConstructorUsedError;
@JsonKey(name: 'user_id')
int get userId => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
@@ -36,7 +39,12 @@ abstract class $WebsocketInfoModelCopyWith<$Res> {
WebsocketInfoModel value, $Res Function(WebsocketInfoModel) then) =
_$WebsocketInfoModelCopyWithImpl<$Res, WebsocketInfoModel>;
@useResult
$Res call({String key, String host, String port});
$Res call(
{String key,
String host,
String port,
String cluster,
@JsonKey(name: 'user_id') int userId});
}
/// @nodoc
@@ -55,6 +63,8 @@ class _$WebsocketInfoModelCopyWithImpl<$Res, $Val extends WebsocketInfoModel>
Object? key = null,
Object? host = null,
Object? port = null,
Object? cluster = null,
Object? userId = null,
}) {
return _then(_value.copyWith(
key: null == key
@@ -69,6 +79,14 @@ class _$WebsocketInfoModelCopyWithImpl<$Res, $Val extends WebsocketInfoModel>
? _value.port
: port // ignore: cast_nullable_to_non_nullable
as String,
cluster: null == cluster
? _value.cluster
: cluster // ignore: cast_nullable_to_non_nullable
as String,
userId: null == userId
? _value.userId
: userId // ignore: cast_nullable_to_non_nullable
as int,
) as $Val);
}
}
@@ -81,7 +99,12 @@ abstract class _$$WebsocketInfoModelImplCopyWith<$Res>
__$$WebsocketInfoModelImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({String key, String host, String port});
$Res call(
{String key,
String host,
String port,
String cluster,
@JsonKey(name: 'user_id') int userId});
}
/// @nodoc
@@ -98,6 +121,8 @@ class __$$WebsocketInfoModelImplCopyWithImpl<$Res>
Object? key = null,
Object? host = null,
Object? port = null,
Object? cluster = null,
Object? userId = null,
}) {
return _then(_$WebsocketInfoModelImpl(
key: null == key
@@ -112,6 +137,14 @@ class __$$WebsocketInfoModelImplCopyWithImpl<$Res>
? _value.port
: port // ignore: cast_nullable_to_non_nullable
as String,
cluster: null == cluster
? _value.cluster
: cluster // ignore: cast_nullable_to_non_nullable
as String,
userId: null == userId
? _value.userId
: userId // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
@@ -120,7 +153,11 @@ class __$$WebsocketInfoModelImplCopyWithImpl<$Res>
@JsonSerializable()
class _$WebsocketInfoModelImpl implements _WebsocketInfoModel {
const _$WebsocketInfoModelImpl(
{required this.key, required this.host, required this.port});
{required this.key,
required this.host,
required this.port,
required this.cluster,
@JsonKey(name: 'user_id') required this.userId});
factory _$WebsocketInfoModelImpl.fromJson(Map<String, dynamic> json) =>
_$$WebsocketInfoModelImplFromJson(json);
@@ -131,10 +168,15 @@ class _$WebsocketInfoModelImpl implements _WebsocketInfoModel {
final String host;
@override
final String port;
@override
final String cluster;
@override
@JsonKey(name: 'user_id')
final int userId;
@override
String toString() {
return 'WebsocketInfoModel(key: $key, host: $host, port: $port)';
return 'WebsocketInfoModel(key: $key, host: $host, port: $port, cluster: $cluster, userId: $userId)';
}
@override
@@ -144,12 +186,15 @@ class _$WebsocketInfoModelImpl implements _WebsocketInfoModel {
other is _$WebsocketInfoModelImpl &&
(identical(other.key, key) || other.key == key) &&
(identical(other.host, host) || other.host == host) &&
(identical(other.port, port) || other.port == port));
(identical(other.port, port) || other.port == port) &&
(identical(other.cluster, cluster) || other.cluster == cluster) &&
(identical(other.userId, userId) || other.userId == userId));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, key, host, port);
int get hashCode =>
Object.hash(runtimeType, key, host, port, cluster, userId);
@JsonKey(ignore: true)
@override
@@ -168,9 +213,12 @@ class _$WebsocketInfoModelImpl implements _WebsocketInfoModel {
abstract class _WebsocketInfoModel implements WebsocketInfoModel {
const factory _WebsocketInfoModel(
{required final String key,
required final String host,
required final String port}) = _$WebsocketInfoModelImpl;
{required final String key,
required final String host,
required final String port,
required final String cluster,
@JsonKey(name: 'user_id') required final int userId}) =
_$WebsocketInfoModelImpl;
factory _WebsocketInfoModel.fromJson(Map<String, dynamic> json) =
_$WebsocketInfoModelImpl.fromJson;
@@ -182,6 +230,11 @@ abstract class _WebsocketInfoModel implements WebsocketInfoModel {
@override
String get port;
@override
String get cluster;
@override
@JsonKey(name: 'user_id')
int get userId;
@override
@JsonKey(ignore: true)
_$$WebsocketInfoModelImplCopyWith<_$WebsocketInfoModelImpl> get copyWith =>
throw _privateConstructorUsedError;

View File

@@ -12,6 +12,8 @@ _$WebsocketInfoModelImpl _$$WebsocketInfoModelImplFromJson(
key: json['key'] as String,
host: json['host'] as String,
port: json['port'] as String,
cluster: json['cluster'] as String,
userId: (json['user_id'] as num).toInt(),
);
Map<String, dynamic> _$$WebsocketInfoModelImplToJson(
@@ -20,4 +22,6 @@ Map<String, dynamic> _$$WebsocketInfoModelImplToJson(
'key': instance.key,
'host': instance.host,
'port': instance.port,
'cluster': instance.cluster,
'user_id': instance.userId,
};