forked from eatvio/eatvio_app
675 lines
20 KiB
Dart
675 lines
20 KiB
Dart
import 'dart:async';
|
|
import 'package:eatvio_app/features/diary/models/diary_model.dart';
|
|
import 'package:eatvio_app/features/diary/models/meal.dart';
|
|
import 'package:eatvio_app/features/diary/models/product.dart';
|
|
import 'package:eatvio_app/features/diary/repositories/diary_repository.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:equatable/equatable.dart';
|
|
import 'package:flutter/material.dart';
|
|
import '/core/app_export.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'dart:convert';
|
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|
|
|
part 'diary_event.dart';
|
|
|
|
/// A bloc that manages the state of a Diary according to the event that is dispatched to it.
|
|
class DiaryBloc extends Bloc<DiaryEvent, DiaryState> {
|
|
final DiaryRepository diaryRepository;
|
|
|
|
DiaryBloc({required this.diaryRepository})
|
|
: super(const DiaryState.loading()) {
|
|
on<LoadDiary>(loadDiary);
|
|
on<ChangeSatiety>(_changeSatiety); // пользователь нажимает на шкалу сытости
|
|
on<ChangeMood>(_changeMood); // пользователь нажимает на шкалу настроения
|
|
on<ChangeMealTime>(_changeMealTime); // пользовательяет времяа пиши
|
|
on<ChangeMeal>(_changeMeal);
|
|
on<ChangePortion>(_changePortion);
|
|
on<WakeupEvent>(_changeWakeup); // пользователь выбирает время пробуждения
|
|
on<RetireEvent>(_changeRetire); // пользователь выбирает время отхода ко сну
|
|
on<ToggleMoodExpandedEvent>(_toggleMood);
|
|
on<ToggleWakeupExpandedEvent>(_toggleWakeup);
|
|
on<AddPortionEvent>(_addPortion); //пользователь нажимает "Добавить продукт"
|
|
on<RemovePortionEvent>(_removePortion);
|
|
on<RemoveImageEvent>(
|
|
_removeImage,
|
|
); // пользователь нажимает на крестик около продукта
|
|
on<UploadImages>(_uploadImages); //пользователь нажимает "Добавить фото"
|
|
on<AddComment>(_addComment); //пользователь нажимает "Добавить комментарий"
|
|
on<ProductInput>(
|
|
_inputProduct,
|
|
);
|
|
on<FinishDay>(_finishDay);
|
|
on<SendModeration>(_sendModeration);
|
|
on<RefreshEvent>(
|
|
reloadScreen,
|
|
);
|
|
on<ClearInput>(_clearInput);
|
|
}
|
|
|
|
loadDiary(
|
|
LoadDiary event,
|
|
Emitter<DiaryState> emit,
|
|
) async {
|
|
emit(const DiaryState.loading());
|
|
try {
|
|
Map<String, int> data = await diaryRepository.getDiaryID();
|
|
final int? diaryID = data['id'];
|
|
final DiaryModel diary = await diaryRepository.getDiary(diaryID ?? 45);
|
|
final Meal currentMeal = diary.meals[0];
|
|
List<SelectionPopupModel> portionList = [
|
|
SelectionPopupModel(title: "gr"),
|
|
SelectionPopupModel(title: "pcs")
|
|
];
|
|
List<SelectionPopupModel> mealList = [
|
|
SelectionPopupModel(title: "Прием пищи 1", id: 1),
|
|
SelectionPopupModel(title: "Прием пищи 2", id: 2),
|
|
SelectionPopupModel(title: "Прием пищи 3", id: 3),
|
|
SelectionPopupModel(title: "Прием пищи 4", id: 4),
|
|
SelectionPopupModel(title: "Прием пищи 5", id: 5),
|
|
SelectionPopupModel(title: "Прием пищи 6", id: 6),
|
|
];
|
|
emit(DiaryState.loaded(
|
|
diary: diary,
|
|
currentMeal: currentMeal,
|
|
portions: currentMeal.portions,
|
|
photos: currentMeal.photos,
|
|
portionList: portionList,
|
|
portionController: portionList[0],
|
|
mealList: mealList,
|
|
selectedMood: currentMeal.mood,
|
|
selectedSatiety: currentMeal.satiety,
|
|
commentController: TextEditingController(),
|
|
productNameController: TextEditingController(),
|
|
productQuantityController: TextEditingController(),
|
|
caloriesController: TextEditingController(),
|
|
fatsController: TextEditingController(),
|
|
proteinsController: TextEditingController(),
|
|
carbsController: TextEditingController(),
|
|
wakeupExpanded: false,
|
|
moderationExpanded: false,
|
|
moodExpanded: false,
|
|
isLoading: false,
|
|
isAdded: false,
|
|
autocompleteList: [],
|
|
recommendedChartValues: [1, 1, 1, 1],
|
|
consumedChartValues: [1, 1, 1, 1]));
|
|
} catch (e) {
|
|
emit(DiaryState.error(errorMessage: e.toString()));
|
|
}
|
|
}
|
|
|
|
fetchChartValues(
|
|
String token,
|
|
int diaryID,
|
|
Emitter<DiaryState> emit,
|
|
) async {
|
|
final responseChart = await http.get(
|
|
Uri.parse(
|
|
dotenv.env['SERVER_ROOT']! + '/diary/$diaryID/dailyNutritionData/',
|
|
),
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': 'Bearer $token',
|
|
},
|
|
);
|
|
|
|
if (responseChart.statusCode == 200) {
|
|
final responseBody = jsonDecode(responseChart.body);
|
|
|
|
final recommendedChartValues =
|
|
responseBody['dailyNutritionRecommendation']
|
|
.values
|
|
.map<int>((value) => convertToInt(value))
|
|
.toList();
|
|
|
|
final consumedChartValues = responseBody['dailyNutritionIntake']
|
|
.values
|
|
.map<int>((value) => convertToInt(value))
|
|
.toList();
|
|
|
|
print([recommendedChartValues, consumedChartValues]);
|
|
return [recommendedChartValues, consumedChartValues];
|
|
} else {
|
|
emit(DiaryStateError(
|
|
errorMessage: 'Ошибка при декодировании данных КБЖУ'));
|
|
print('fail');
|
|
return [[], []]; // Return empty lists instead of -1
|
|
}
|
|
}
|
|
|
|
refreshScreen(
|
|
int mealID,
|
|
DiaryEvent event,
|
|
Emitter<DiaryState> emit,
|
|
) async {
|
|
await reloadScreen(event, emit);
|
|
}
|
|
|
|
reloadScreen(
|
|
DiaryEvent event,
|
|
Emitter<DiaryState> emit,
|
|
) async {
|
|
print("Reloading...");
|
|
}
|
|
|
|
Future<void> _changeSatiety(
|
|
ChangeSatiety event,
|
|
Emitter<DiaryState> emit,
|
|
) async {
|
|
if (state is DiaryStateLoaded) {
|
|
var currentState = state as DiaryStateLoaded;
|
|
var updatedState =
|
|
currentState.copyWith(selectedSatiety: event.selectedElement);
|
|
diaryRepository.changeSatiety(event.selectedElement,
|
|
currentState.currentMeal.id, currentState.diary.id);
|
|
emit(updatedState);
|
|
}
|
|
}
|
|
|
|
Future<void> _changeMood(
|
|
ChangeMood event,
|
|
Emitter<DiaryState> emit,
|
|
) async {
|
|
if (state is DiaryStateLoaded) {
|
|
var currentState = state as DiaryStateLoaded;
|
|
var updatedState =
|
|
currentState.copyWith(selectedMood: event.selectedElement);
|
|
|
|
diaryRepository.changeMood(event.selectedElement,
|
|
currentState.currentMeal.id, currentState.diary.id);
|
|
|
|
emit(updatedState); // Передаем обновленное состояние в emit
|
|
}
|
|
}
|
|
|
|
FutureOr<void> _changeWakeup(
|
|
WakeupEvent event,
|
|
Emitter<DiaryState> emit,
|
|
) async {
|
|
if (state is DiaryStateLoaded) {
|
|
var currentState = state as DiaryStateLoaded;
|
|
var updatedState =
|
|
currentState.copyWith(wakeupController: event.selectedTime);
|
|
final DateFormat formatter = DateFormat('HH:mm');
|
|
final String wakeupString = formatter.format(DateTime(
|
|
2022, 1, 1, event.selectedTime.hour, event.selectedTime.minute));
|
|
await diaryRepository.changeWakeup(
|
|
currentState.diary.id,
|
|
wakeupString,
|
|
);
|
|
emit(updatedState);
|
|
}
|
|
}
|
|
|
|
FutureOr<void> _changeRetire(
|
|
RetireEvent event,
|
|
Emitter<DiaryState> emit,
|
|
) async {
|
|
if (state is DiaryStateLoaded) {
|
|
var currentState = state as DiaryStateLoaded;
|
|
var updatedState =
|
|
currentState.copyWith(retireController: event.selectedTime);
|
|
final DateFormat formatter = DateFormat('HH:mm');
|
|
final String retireString = formatter.format(DateTime(
|
|
2022, 1, 1, event.selectedTime.hour, event.selectedTime.minute));
|
|
await diaryRepository.changeRetire(
|
|
currentState.diary.id,
|
|
retireString,
|
|
);
|
|
emit(updatedState);
|
|
}
|
|
}
|
|
|
|
FutureOr<void> _changeMealTime(
|
|
ChangeMealTime event,
|
|
Emitter<DiaryState> emit,
|
|
) async {
|
|
if (state is DiaryStateLoaded) {
|
|
var currentState = state as DiaryStateLoaded;
|
|
var updatedState = currentState.copyWith(mealTimeController: event.time);
|
|
emit(updatedState);
|
|
}
|
|
}
|
|
|
|
FutureOr<void> _changeMeal(
|
|
ChangeMeal event,
|
|
Emitter<DiaryState> emit,
|
|
) async {
|
|
if (state is DiaryStateLoaded) {
|
|
final currentState = state as DiaryStateLoaded;
|
|
final Meal meal = currentState.diary.meals
|
|
.firstWhere((meal) => meal.type == event.value.id);
|
|
DiaryStateLoaded updatedState = currentState.copyWith(
|
|
currentMeal: meal,
|
|
portions: meal.portions,
|
|
photos: meal.photos,
|
|
mealTimeController: toTime(meal.time),
|
|
selectedMood: meal.mood,
|
|
selectedSatiety: meal.satiety,
|
|
);
|
|
emit(updatedState);
|
|
}
|
|
}
|
|
|
|
Future<void> _inputProduct(
|
|
ProductInput event,
|
|
Emitter<DiaryState> emit,
|
|
) async {
|
|
if (state is DiaryStateLoaded) {
|
|
//var currentState = state as DiaryStateLoaded;
|
|
|
|
print(await diaryRepository.inputProduct(event.input));
|
|
final Map<String, List<Product>> productNutritionalValues =
|
|
await diaryRepository.inputProduct(event.input);
|
|
// Create a new controller with the input text and update the autocomplete list
|
|
/*var updatedState = currentState.copyWith(
|
|
productNameController: TextEditingController(text: event.input),
|
|
autocompleteList: productNutritionalValues.map((product) => product['name']).toList(),
|
|
);
|
|
if (event.input.length > 3) {
|
|
updatedState = updatedState.copyWith(moderationExpanded: true);
|
|
} else {
|
|
updatedState = updatedState.copyWith(moderationExpanded: false);
|
|
}
|
|
|
|
emit(updatedState); */
|
|
}
|
|
}
|
|
|
|
FutureOr<void> _clearInput(
|
|
ClearInput event,
|
|
Emitter<DiaryState> emit,
|
|
) {
|
|
if (state is DiaryStateLoaded) {
|
|
var currentState = state as DiaryStateLoaded;
|
|
DiaryStateLoaded updatedState = currentState.copyWith();
|
|
emit(updatedState);
|
|
}
|
|
}
|
|
|
|
FutureOr<void> _changePortion(
|
|
ChangePortion event,
|
|
Emitter<DiaryState> emit,
|
|
) {
|
|
if (state is DiaryStateLoaded) {
|
|
var currentState = state as DiaryStateLoaded;
|
|
DiaryStateLoaded updatedState = currentState.copyWith(
|
|
portionController: event.value,
|
|
);
|
|
emit(updatedState);
|
|
}
|
|
}
|
|
|
|
FutureOr<void> _toggleMood(
|
|
ToggleMoodExpandedEvent event,
|
|
Emitter<DiaryState> emit,
|
|
) {
|
|
if (state is DiaryStateLoaded) {
|
|
var currentState = state as DiaryStateLoaded;
|
|
|
|
emit(currentState.copyWith(moodExpanded: !currentState.moodExpanded));
|
|
}
|
|
}
|
|
|
|
FutureOr<void> _toggleWakeup(
|
|
ToggleWakeupExpandedEvent event,
|
|
Emitter<DiaryState> emit,
|
|
) {
|
|
if (state is DiaryStateLoaded) {
|
|
var currentState = state as DiaryStateLoaded;
|
|
|
|
emit(currentState.copyWith(wakeupExpanded: !currentState.wakeupExpanded));
|
|
}
|
|
}
|
|
|
|
FutureOr<void> _addPortion(
|
|
AddPortionEvent event,
|
|
Emitter<DiaryState> emit,
|
|
) async {
|
|
if (state is DiaryStateLoaded) {
|
|
var currentState = state as DiaryStateLoaded;
|
|
|
|
await diaryRepository.addPortion(
|
|
currentState.diary.id,
|
|
currentState.currentMeal.id,
|
|
currentState.productNameController.text,
|
|
currentState.portionController.value,
|
|
int.parse(currentState.productQuantityController.text),
|
|
);
|
|
|
|
emit(currentState.copyWith(isAdded: true));
|
|
}
|
|
}
|
|
|
|
Future<void> _removePortion(
|
|
RemovePortionEvent event,
|
|
Emitter<DiaryState> emit,
|
|
) async {
|
|
if (state is DiaryStateLoaded) {
|
|
var currentState = state as DiaryStateLoaded;
|
|
|
|
int? mealId =
|
|
currentState.currentMeal.id; // Rename id to mealId for clarity
|
|
|
|
var response = await sendRemovePortion(
|
|
event.portionId,
|
|
mealId,
|
|
currentState.diary.id,
|
|
);
|
|
|
|
if (response.statusCode == 204) {
|
|
await reloadScreen(event, emit);
|
|
} else {
|
|
print(
|
|
'Failed to add portion. Status Code: ${response.statusCode} ${response.body}',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<http.Response> sendRemovePortion(
|
|
int? portionID,
|
|
int? mealID,
|
|
int diaryID,
|
|
) async {
|
|
String url =
|
|
'${dotenv.env['SERVER_ROOT']!}/diary/$diaryID/meal/$mealID/portions/$portionID/delete';
|
|
|
|
var response = await http.delete(
|
|
Uri.parse(url),
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
encoding: utf8,
|
|
);
|
|
|
|
return response;
|
|
}
|
|
|
|
FutureOr<void> _removeImage(
|
|
RemoveImageEvent event,
|
|
Emitter<DiaryState> emit,
|
|
) async {
|
|
if (state is DiaryStateLoaded) {
|
|
var currentState = state as DiaryStateLoaded;
|
|
|
|
var response = await sendRemovePhoto(
|
|
event.path,
|
|
event.photoID,
|
|
currentState.currentMeal.id,
|
|
currentState.diary.id,
|
|
);
|
|
print(response.statusCode);
|
|
if (response.statusCode == 204) {
|
|
await reloadScreen(event, emit);
|
|
} else {
|
|
print(
|
|
'Failed to remove photo. Status Code: ${response.statusCode} ${response.body}',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<http.Response> sendRemovePhoto(
|
|
String? path,
|
|
String? photoID,
|
|
int? mealID,
|
|
int diaryID,
|
|
) async {
|
|
String url =
|
|
'${dotenv.env['SERVER_ROOT']!}/diary/$diaryID/meal/$mealID/photo/delete';
|
|
final Map<String, dynamic> credentials = {
|
|
'id': photoID,
|
|
'path': path,
|
|
};
|
|
var response = await http.delete(
|
|
Uri.parse(url),
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: json.encode(credentials),
|
|
encoding: utf8,
|
|
);
|
|
|
|
return response;
|
|
}
|
|
|
|
Future<void> _finishDay(FinishDay event, Emitter<DiaryState> emit) async {
|
|
if (state is DiaryStateLoaded) {
|
|
var currentState = state as DiaryStateLoaded;
|
|
|
|
var response = await sendFinishDay(
|
|
currentState.diary.id,
|
|
currentState.wakeupController,
|
|
currentState.retireController,
|
|
currentState.commentController.text,
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
await reloadScreen(event, emit);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<http.Response> sendFinishDay(
|
|
int diaryID,
|
|
TimeOfDay? wakeup,
|
|
TimeOfDay? retire,
|
|
String? comment,
|
|
) async {
|
|
final formatter = DateFormat('HH:mm');
|
|
final wakeupString =
|
|
formatter.format(DateTime(2022, 1, 1, wakeup!.hour, wakeup.minute));
|
|
final retireString =
|
|
formatter.format(DateTime(2022, 1, 1, retire!.hour, retire.minute));
|
|
|
|
final Map<String, dynamic> credentials = {
|
|
'wakeup': wakeupString,
|
|
'retire': retireString,
|
|
'comment': comment ?? " ",
|
|
};
|
|
|
|
String url = '${dotenv.env['SERVER_ROOT']!}/diary/$diaryID/finishDay';
|
|
|
|
var response = await http.post(
|
|
Uri.parse(url),
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: json.encode(credentials),
|
|
encoding: utf8,
|
|
);
|
|
|
|
return response;
|
|
}
|
|
|
|
Future<void> _uploadImages(
|
|
UploadImages event,
|
|
Emitter<DiaryState> emit,
|
|
) async {
|
|
if (state is DiaryStateLoaded) {
|
|
var currentState = state as DiaryStateLoaded;
|
|
|
|
emit(currentState.copyWith(isLoading: true));
|
|
await uploadImageToServer(
|
|
event.image.path,
|
|
currentState.diary.id,
|
|
currentState.currentMeal.id,
|
|
);
|
|
await reloadScreen(event, emit);
|
|
}
|
|
}
|
|
|
|
Future<String> uploadImageToServer(
|
|
String imagePath,
|
|
int diaryID,
|
|
int mealId,
|
|
) async {
|
|
String url =
|
|
'${dotenv.env['SERVER_ROOT']!}/diary/$diaryID/meal/$mealId/photo/upload';
|
|
|
|
var request = http.MultipartRequest('POST', Uri.parse(url));
|
|
|
|
request.files.add(await http.MultipartFile.fromPath('photo', imagePath));
|
|
|
|
var client = http.Client();
|
|
var streamedResponse = await client.send(request);
|
|
|
|
if (streamedResponse.statusCode == 200) {
|
|
var response = await http.Response.fromStream(streamedResponse);
|
|
var imageUrl = response.body;
|
|
print('Image successfully uploaded');
|
|
return imageUrl;
|
|
} else {
|
|
throw Exception(
|
|
'Failed to upload image. Status code: ${streamedResponse.statusCode}}',
|
|
);
|
|
}
|
|
}
|
|
|
|
FutureOr<void> _sendModeration(
|
|
SendModeration event,
|
|
Emitter<DiaryState> emit,
|
|
) async {
|
|
if (state is DiaryStateLoaded) {
|
|
final response = await _sendModerationData(
|
|
event.name,
|
|
event.energyOn100gr,
|
|
event.proteinsOn100gr,
|
|
event.fatsOn100gr,
|
|
event.carbohydratesOn100gr,
|
|
);
|
|
|
|
if (response.statusCode == 200)
|
|
print("Moderation success");
|
|
else
|
|
print("Moderation error");
|
|
}
|
|
}
|
|
|
|
Future<http.Response> _sendModerationData(
|
|
String name,
|
|
String energy_on_100gr,
|
|
String proteins_on_100gr,
|
|
String fats_on_100gr,
|
|
String carbohydrates_on_100gr,
|
|
) async {
|
|
final Uri url = Uri.parse(
|
|
'${dotenv.env['SERVER_ROOT']!}/diary/product_nutritional_values/sendToModeration',
|
|
);
|
|
final Uri serverURL = Uri.parse('${dotenv.env['SERVER_ROOT']!}');
|
|
String host = serverURL.host;
|
|
final Map<String, String> credentials = {
|
|
'name': name,
|
|
'energy_on_100gr': energy_on_100gr,
|
|
'proteins_on_100gr': proteins_on_100gr,
|
|
'fats_on_100gr': fats_on_100gr,
|
|
'carbohydrates_on_100gr': carbohydrates_on_100gr,
|
|
};
|
|
|
|
var headers = {
|
|
//'Content-Length': bodyBytes,
|
|
'Host': host,
|
|
};
|
|
|
|
var response = await http.post(
|
|
url,
|
|
headers: headers,
|
|
body: credentials,
|
|
encoding: utf8,
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
return response;
|
|
} else {
|
|
throw Exception(
|
|
'Failed to send a product for moderation ${response.statusCode}',
|
|
);
|
|
}
|
|
}
|
|
|
|
FutureOr<void> _addComment(
|
|
AddComment event,
|
|
Emitter<DiaryState> emit,
|
|
) async {
|
|
if (state is DiaryStateLoaded) {
|
|
var currentState = state as DiaryStateLoaded;
|
|
var updatedState = currentState.copyWith(
|
|
commentController: TextEditingController(text: event.comment),
|
|
);
|
|
|
|
// Отправляем данные по HTTP маршруту и ждем ответ
|
|
var response = await sendCommentData(
|
|
event.comment,
|
|
currentState.diary.id,
|
|
);
|
|
|
|
if (response.statusCode == 200) {
|
|
emit(updatedState); // Передаем обновленное состояние в emit
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<http.Response> sendCommentData(
|
|
String comment,
|
|
int diaryID,
|
|
) async {
|
|
String url = dotenv.env['SERVER_ROOT']! + '/diary/$diaryID/retire/fill';
|
|
final Map<String, dynamic> credentials = {
|
|
'comment': comment,
|
|
};
|
|
|
|
var response = await http.post(
|
|
Uri.parse(url),
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: json.encode(credentials),
|
|
encoding: utf8,
|
|
);
|
|
|
|
return response;
|
|
}
|
|
|
|
TimeOfDay toTime(String timeStr) {
|
|
List<String> timeParts = timeStr.split(":");
|
|
int hour = int.parse(timeParts[0]);
|
|
int minute = int.parse(timeParts[1]);
|
|
|
|
TimeOfDay timeOfDay = TimeOfDay(hour: hour, minute: minute);
|
|
return timeOfDay;
|
|
}
|
|
|
|
List<SelectionPopupModel> fillMealList(List<dynamic> meals) {
|
|
meals.sort(compareTypes);
|
|
return meals.map((meal) {
|
|
String title = "Прием пищи ${meal['type']}";
|
|
return SelectionPopupModel(id: meal['id'], title: title);
|
|
}).toList();
|
|
}
|
|
|
|
int compareTypes(a, b) {
|
|
return a['type'].compareTo(b['type']);
|
|
}
|
|
|
|
List<SelectionPopupModel> fillPortion() {
|
|
return [
|
|
SelectionPopupModel(id: 0, title: "порций", isSelected: true),
|
|
SelectionPopupModel(id: 1, title: "грамм"),
|
|
];
|
|
}
|
|
}
|
|
|
|
int convertToInt(dynamic value) {
|
|
if (value is int) {
|
|
return value;
|
|
} else if (value is double) {
|
|
return value.toInt();
|
|
} else {
|
|
throw FormatException('Unexpected value type: ${value.runtimeType}');
|
|
}
|
|
}
|