forked from eatvio/eatvio_app
feat(video_calls, welcome_screen): ios mic fix, welcome scrin layoute fix
This commit is contained in:
@@ -40,11 +40,18 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
void Function(bool isOnline)? _presenceOnlineListener;
|
||||
var uuid = const Uuid();
|
||||
|
||||
ChatRoomCubit({required this.chatRepository, required this.wsManager, required this.cardsRepository}) : super(const ChatRoomState.loading());
|
||||
ChatRoomCubit({
|
||||
required this.chatRepository,
|
||||
required this.wsManager,
|
||||
required this.cardsRepository,
|
||||
}) : super(const ChatRoomState.loading());
|
||||
|
||||
void setTimer() {
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(const Duration(seconds: 5), (_) => updateChosenPlanState());
|
||||
_timer = Timer.periodic(
|
||||
const Duration(seconds: 5),
|
||||
(_) => updateChosenPlanState(),
|
||||
);
|
||||
}
|
||||
|
||||
void cancelTimer() {
|
||||
@@ -58,13 +65,19 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
_emitLoaded((current) => current.copyWith(isLoading: isLoading));
|
||||
|
||||
try {
|
||||
final chatMessages = await chatRepository.getChatMessages(uuid, currentPage + 1);
|
||||
final chatMessages = await chatRepository.getChatMessages(
|
||||
uuid,
|
||||
currentPage + 1,
|
||||
);
|
||||
|
||||
isLoading = false;
|
||||
_emitLoaded(
|
||||
(current) => current.copyWith(
|
||||
isLoading: isLoading,
|
||||
chatMessages: [...current.chatMessages, ...chatMessages.messagesList],
|
||||
chatMessages: [
|
||||
...current.chatMessages,
|
||||
...chatMessages.messagesList,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -98,7 +111,11 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
/// `GET /api/consultation/resolve/order_id_to_conversation/{orderId}`.
|
||||
///
|
||||
/// Возвращает conversation uuid, которым инициализировали чат (для reload без повторного resolve).
|
||||
Future<String?> initChat(String? uuid, {String? orderId, bool sendInitialMessage = false}) async {
|
||||
Future<String?> initChat(
|
||||
String? uuid, {
|
||||
String? orderId,
|
||||
bool sendInitialMessage = false,
|
||||
}) async {
|
||||
try {
|
||||
emit(const ChatRoomState.loading());
|
||||
|
||||
@@ -106,7 +123,9 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
int? resolvedChosenPlanId;
|
||||
|
||||
if (orderId != null && orderId.isNotEmpty) {
|
||||
final resolved = await chatRepository.resolveOrderIdToConversation(orderId: orderId);
|
||||
final resolved = await chatRepository.resolveOrderIdToConversation(
|
||||
orderId: orderId,
|
||||
);
|
||||
resolvedUuid = resolved.conversationUuid;
|
||||
resolvedChosenPlanId = resolved.chosenPlanId;
|
||||
}
|
||||
@@ -117,6 +136,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
|
||||
currentPage = 1;
|
||||
isLoading = false;
|
||||
_activeChatUuid = resolvedUuid;
|
||||
|
||||
final chatRoom = await chatRepository.getChatRoom(resolvedUuid);
|
||||
ChosenPlanModel? chosenPlan;
|
||||
@@ -124,7 +144,10 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
if (planId != null) {
|
||||
chosenPlan = await chatRepository.getChosenPlan(id: planId);
|
||||
}
|
||||
final chatMessages = await chatRepository.getChatMessages(resolvedUuid, currentPage);
|
||||
final chatMessages = await chatRepository.getChatMessages(
|
||||
resolvedUuid,
|
||||
currentPage,
|
||||
);
|
||||
lastPage = chatMessages.meta.lastPage;
|
||||
await _subscribeToChatChannel(uuid: resolvedUuid, chatId: chatRoom.id);
|
||||
_listenToReconnect();
|
||||
@@ -177,7 +200,9 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
final page = await chatRepository.getChatMessages(uuid, 1);
|
||||
lastPage = page.meta.lastPage;
|
||||
_emitLoaded((current) {
|
||||
final byId = {for (final message in current.chatMessages) message.id: message};
|
||||
final byId = {
|
||||
for (final message in current.chatMessages) message.id: message,
|
||||
};
|
||||
for (final message in page.messagesList) {
|
||||
byId.putIfAbsent(message.id, () => message);
|
||||
}
|
||||
@@ -190,6 +215,50 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Перезагружает сообщения, комнату и состояние плана без экрана загрузки.
|
||||
Future<void> refreshChat() async {
|
||||
final uuid = _activeChatUuid;
|
||||
if (uuid == null || uuid.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final currentState = state;
|
||||
if (currentState is! ChatRoomStateLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
currentPage = 1;
|
||||
isLoading = false;
|
||||
|
||||
final chatRoom = await chatRepository.getChatRoom(uuid);
|
||||
ChosenPlanModel? chosenPlan;
|
||||
final planId =
|
||||
chatRoom.chosenPlanId ?? currentState.chatRoom.chosenPlanId;
|
||||
if (planId != null) {
|
||||
chosenPlan = await chatRepository.getChosenPlan(id: planId);
|
||||
}
|
||||
final chatMessages = await chatRepository.getChatMessages(
|
||||
uuid,
|
||||
currentPage,
|
||||
);
|
||||
lastPage = chatMessages.meta.lastPage;
|
||||
|
||||
_emitLoaded(
|
||||
(latest) => latest.copyWith(
|
||||
chatRoom: chatRoom.copyWith(chosenPlan: chosenPlan),
|
||||
chatMessages: chatMessages.messagesList,
|
||||
isLoading: false,
|
||||
),
|
||||
);
|
||||
|
||||
if (chosenPlan?.consultantId == chatRoom.users.sender.id) {
|
||||
await checkCards();
|
||||
}
|
||||
} catch (e, st) {
|
||||
await reportFrom(e, st, this);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateChosenPlanState() async {
|
||||
try {
|
||||
final currentState = state;
|
||||
@@ -213,9 +282,16 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
final planId = (state as ChatRoomStateLoaded).chatRoom.chosenPlanId;
|
||||
if (planId == null) return false;
|
||||
final stringDate = date.format('yyyy-MM-dd');
|
||||
await chatRepository.choseStartPlanDate(id: planId, date: {'start_date': stringDate});
|
||||
await chatRepository.choseStartPlanDate(
|
||||
id: planId,
|
||||
date: {'start_date': stringDate},
|
||||
);
|
||||
final chosenPlan = await chatRepository.getChosenPlan(id: planId);
|
||||
emit((state as ChatRoomStateLoaded).copyWith.chatRoom(chosenPlan: chosenPlan));
|
||||
emit(
|
||||
(state as ChatRoomStateLoaded).copyWith.chatRoom(
|
||||
chosenPlan: chosenPlan,
|
||||
),
|
||||
);
|
||||
return true;
|
||||
} catch (e, st) {
|
||||
await reportFrom(e, st, this);
|
||||
@@ -224,7 +300,10 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> cancelChosenPlan({required String chatId, required int planId}) async {
|
||||
Future<void> cancelChosenPlan({
|
||||
required String chatId,
|
||||
required int planId,
|
||||
}) async {
|
||||
try {
|
||||
await chatRepository.cancelChosenPlan(id: planId);
|
||||
emit((state as ChatRoomStateLoaded).copyWith.chatRoom(chosenPlan: null));
|
||||
@@ -234,7 +313,9 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
}
|
||||
}
|
||||
|
||||
bool _emitLoaded(ChatRoomStateLoaded Function(ChatRoomStateLoaded current) update) {
|
||||
bool _emitLoaded(
|
||||
ChatRoomStateLoaded Function(ChatRoomStateLoaded current) update,
|
||||
) {
|
||||
if (isClosed) {
|
||||
return false;
|
||||
}
|
||||
@@ -246,7 +327,10 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> _subscribeToChatChannel({required String uuid, required String chatId}) async {
|
||||
Future<void> _subscribeToChatChannel({
|
||||
required String uuid,
|
||||
required String chatId,
|
||||
}) async {
|
||||
// Активный чат запоминаем до проверки клиента: он нужен и для догрузки
|
||||
// пропущенного после реконнекта (S5), даже если сокет появится позже.
|
||||
_activeChatUuid = uuid;
|
||||
@@ -266,18 +350,26 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
);
|
||||
|
||||
typingSubscription = wsManager.clientTypingStream?.listen((event) {
|
||||
_emitLoaded((current) => current.copyWith(isTyping: true, isLoading: isLoading));
|
||||
_emitLoaded(
|
||||
(current) => current.copyWith(isTyping: true, isLoading: isLoading),
|
||||
);
|
||||
EasyDebounce.debounce(
|
||||
'client-typing',
|
||||
const Duration(seconds: 3),
|
||||
() => _emitLoaded((current) => current.copyWith(isTyping: false, isLoading: isLoading)),
|
||||
() => _emitLoaded(
|
||||
(current) => current.copyWith(isTyping: false, isLoading: isLoading),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _onDirectChatEvent(ChannelReadEvent event) {
|
||||
try {
|
||||
if (addIncomingMessage(ChatMessageModel.fromJson(jsonDecode(event.data) as Map<String, dynamic>))) {
|
||||
if (addIncomingMessage(
|
||||
ChatMessageModel.fromJson(
|
||||
jsonDecode(event.data) as Map<String, dynamic>,
|
||||
),
|
||||
)) {
|
||||
unawaited(_playIncomingSound());
|
||||
}
|
||||
} catch (e, st) {
|
||||
@@ -305,7 +397,6 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
if (currentState is! ChatRoomStateLoaded) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final currentUserId = wsManager.userId;
|
||||
final isOwn = currentUserId != null && message.authorId == currentUserId;
|
||||
|
||||
@@ -321,7 +412,11 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
}
|
||||
|
||||
final added = _emitLoaded((current) {
|
||||
if (current.chatMessages.any((existing) => message.id != 0 && existing.id == message.id)) {
|
||||
if (current.chatMessages.any(
|
||||
(existing) =>
|
||||
(message.id != 0 && existing.id == message.id) ||
|
||||
(message.uuid != null && existing.uuid == message.uuid),
|
||||
)) {
|
||||
return current;
|
||||
}
|
||||
return current.copyWith(
|
||||
@@ -352,12 +447,15 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
|
||||
await wsManager.initPresenceDirectChatChannel(chatId);
|
||||
_presenceOnlineListener = (isOnline) {
|
||||
unawaited(_handleCounterpartOnlineChanged(
|
||||
counterpartField: counterpart.id == currentState.chatRoom.users.receiver.id
|
||||
? _ChatCounterpartField.receiver
|
||||
: _ChatCounterpartField.sender,
|
||||
isOnline: isOnline,
|
||||
));
|
||||
unawaited(
|
||||
_handleCounterpartOnlineChanged(
|
||||
counterpartField:
|
||||
counterpart.id == currentState.chatRoom.users.receiver.id
|
||||
? _ChatCounterpartField.receiver
|
||||
: _ChatCounterpartField.sender,
|
||||
isOnline: isOnline,
|
||||
),
|
||||
);
|
||||
};
|
||||
wsManager.addPresenceDirectChatListener(
|
||||
counterpartUserId: counterpart.id,
|
||||
@@ -376,7 +474,8 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
? currentState.chatRoom.users.receiver
|
||||
: currentState.chatRoom.users.sender;
|
||||
|
||||
if (counterpart.isOnline == isOnline && (isOnline || counterpart.lastSeenText.isNotEmpty)) {
|
||||
if (counterpart.isOnline == isOnline &&
|
||||
(isOnline || counterpart.lastSeenText.isNotEmpty)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -453,10 +552,16 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> sendMessage({String? data, List<File>? files, required String userSlug}) async {
|
||||
Future<bool> sendMessage({
|
||||
String? data,
|
||||
List<File>? files,
|
||||
required String userSlug,
|
||||
}) async {
|
||||
try {
|
||||
final messageUuid = uuid.v4();
|
||||
final compressedFiles = files != null ? await ImageCompressUtility.compressFilesList(List<File>.from(files)) : null;
|
||||
final compressedFiles = files != null
|
||||
? await ImageCompressUtility.compressFilesList(List<File>.from(files))
|
||||
: null;
|
||||
try {
|
||||
unawaited(AudioPlayer().play(AssetSource('audio/send_message.wav')));
|
||||
} catch (_) {}
|
||||
@@ -482,8 +587,15 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
);
|
||||
|
||||
try {
|
||||
final serverMessage = await chatRepository.sendMessage(data: data, files: compressedFiles, slug: userSlug);
|
||||
_replaceOptimisticMessage(messageUuid: messageUuid, serverMessage: serverMessage);
|
||||
final serverMessage = await chatRepository.sendMessage(
|
||||
data: data,
|
||||
files: compressedFiles,
|
||||
slug: userSlug,
|
||||
);
|
||||
_replaceOptimisticMessage(
|
||||
messageUuid: messageUuid,
|
||||
serverMessage: serverMessage,
|
||||
);
|
||||
} catch (e, st) {
|
||||
await reportFrom(e, st, this);
|
||||
_emitLoaded(
|
||||
@@ -516,7 +628,11 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
_emitLoaded(
|
||||
(current) => current.copyWith(
|
||||
chatMessages: current.chatMessages
|
||||
.map((message) => idSet.contains(message.id) ? message.copyWith(isRead: true) : message)
|
||||
.map(
|
||||
(message) => idSet.contains(message.id)
|
||||
? message.copyWith(isRead: true)
|
||||
: message,
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
@@ -538,17 +654,26 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
messageState: MessageState.sent,
|
||||
isCurrentUserAuthor: true,
|
||||
);
|
||||
final withoutDuplicate = current.chatMessages.where((message) => message.id != serverMessage.id || message.uuid == messageUuid);
|
||||
final hasOptimistic = withoutDuplicate.any((message) => message.uuid == messageUuid);
|
||||
final withoutDuplicate = current.chatMessages.where(
|
||||
(message) =>
|
||||
message.id != serverMessage.id || message.uuid == messageUuid,
|
||||
);
|
||||
final hasOptimistic = withoutDuplicate.any(
|
||||
(message) => message.uuid == messageUuid,
|
||||
);
|
||||
if (!hasOptimistic) {
|
||||
return current.copyWith(chatMessages: [sentMessage, ...withoutDuplicate]);
|
||||
return current.copyWith(
|
||||
chatMessages: [sentMessage, ...withoutDuplicate],
|
||||
);
|
||||
}
|
||||
return current.copyWith(
|
||||
chatMessages: withoutDuplicate.map((elem) {
|
||||
if (elem.uuid == messageUuid) {
|
||||
return sentMessage.copyWith(
|
||||
isRead: elem.isRead,
|
||||
roomId: elem.roomId?.isNotEmpty == true ? elem.roomId : serverMessage.roomId,
|
||||
roomId: elem.roomId?.isNotEmpty == true
|
||||
? elem.roomId
|
||||
: serverMessage.roomId,
|
||||
);
|
||||
}
|
||||
return elem;
|
||||
@@ -561,7 +686,10 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
try {
|
||||
final planId = (state as ChatRoomStateLoaded).chatRoom.chosenPlan?.id;
|
||||
if (planId == null) return null;
|
||||
final paymentModel = await chatRepository.getPaymentLink(id: planId, paymentProvider: 'alfa-bank');
|
||||
final paymentModel = await chatRepository.getPaymentLink(
|
||||
id: planId,
|
||||
paymentProvider: 'alfa-bank',
|
||||
);
|
||||
return paymentModel.paymentUrl;
|
||||
} catch (e, st) {
|
||||
await reportFrom(e, st, this);
|
||||
@@ -618,18 +746,31 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
void deleteMessage(String uuid) {
|
||||
final currentState = state;
|
||||
if (currentState is ChatRoomStateLoaded) {
|
||||
emit(currentState.copyWith(chatMessages: currentState.chatMessages.where((message) => message.uuid != uuid).toList()));
|
||||
emit(
|
||||
currentState.copyWith(
|
||||
chatMessages: currentState.chatMessages
|
||||
.where((message) => message.uuid != uuid)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> resendMessage({required String uuid, required String userSlug}) async {
|
||||
Future<void> resendMessage({
|
||||
required String uuid,
|
||||
required String userSlug,
|
||||
}) async {
|
||||
final currentState = state;
|
||||
if (currentState is ChatRoomStateLoaded) {
|
||||
final message = currentState.chatMessages.firstWhereOrNull((message) => message.uuid == uuid);
|
||||
final message = currentState.chatMessages.firstWhereOrNull(
|
||||
(message) => message.uuid == uuid,
|
||||
);
|
||||
if (message != null) {
|
||||
try {
|
||||
try {
|
||||
unawaited(AudioPlayer().play(AssetSource('audio/send_message.wav')));
|
||||
unawaited(
|
||||
AudioPlayer().play(AssetSource('audio/send_message.wav')),
|
||||
);
|
||||
} catch (_) {}
|
||||
|
||||
List<File>? filesToResend;
|
||||
@@ -661,8 +802,15 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
);
|
||||
|
||||
try {
|
||||
final serverMessage = await chatRepository.sendMessage(data: message.data, files: filesToResend, slug: userSlug);
|
||||
_replaceOptimisticMessage(messageUuid: uuid, serverMessage: serverMessage);
|
||||
final serverMessage = await chatRepository.sendMessage(
|
||||
data: message.data,
|
||||
files: filesToResend,
|
||||
slug: userSlug,
|
||||
);
|
||||
_replaceOptimisticMessage(
|
||||
messageUuid: uuid,
|
||||
serverMessage: serverMessage,
|
||||
);
|
||||
} catch (e, st) {
|
||||
await reportFrom(e, st, this);
|
||||
_emitLoaded(
|
||||
|
||||
@@ -46,7 +46,12 @@ class ChatScreen extends StatefulWidget {
|
||||
final String? orderId;
|
||||
final bool? sendInitialMessage;
|
||||
|
||||
const ChatScreen({this.uuid, this.orderId, super.key, required this.sendInitialMessage});
|
||||
const ChatScreen({
|
||||
this.uuid,
|
||||
this.orderId,
|
||||
super.key,
|
||||
required this.sendInitialMessage,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ChatScreen> createState() => _ChatScreenState();
|
||||
@@ -57,7 +62,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
final TextEditingController textEditingController = TextEditingController();
|
||||
final ChatRoomCubit chatRoomCubit = sl<ChatRoomCubit>();
|
||||
final List<int> unreadMessageIds = [];
|
||||
final showableChatBadgeStates = ChosenPlanStateEnum.getShowableChatBadgeStates();
|
||||
final showableChatBadgeStates =
|
||||
ChosenPlanStateEnum.getShowableChatBadgeStates();
|
||||
late AppLifecycleListener _listener;
|
||||
List<File> pickedFiles = [];
|
||||
|
||||
@@ -108,7 +114,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
void _onScroll() {
|
||||
final uuid = conversationUuid;
|
||||
if (uuid == null) return;
|
||||
if (scrollController.offset >= scrollController.position.maxScrollExtent && !scrollController.position.outOfRange) {
|
||||
if (scrollController.offset >= scrollController.position.maxScrollExtent &&
|
||||
!scrollController.position.outOfRange) {
|
||||
chatRoomCubit.loadNextPage(uuid);
|
||||
}
|
||||
}
|
||||
@@ -168,9 +175,13 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
}
|
||||
|
||||
Scaffold _buildChat(BuildContext context, ChatRoomStateLoaded state) {
|
||||
final chatCounterpart = resolveChatCounterpart(users: state.chatRoom.users, currentUserId: sl<WsManager>().userId);
|
||||
final chatCounterpart = resolveChatCounterpart(
|
||||
users: state.chatRoom.users,
|
||||
currentUserId: sl<WsManager>().userId,
|
||||
);
|
||||
final chosenPlan = state.chatRoom.chosenPlan;
|
||||
final amIConsultant = chosenPlan?.consultantId == state.chatRoom.users.sender.id;
|
||||
final amIConsultant =
|
||||
chosenPlan?.consultantId == state.chatRoom.users.sender.id;
|
||||
final showUserDetailsButton = [
|
||||
ChosenPlanStateEnum.stateStartDateHasBeenSet,
|
||||
ChosenPlanStateEnum.statePendingPayment,
|
||||
@@ -186,22 +197,37 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
}
|
||||
|
||||
final showAboutOrderButton =
|
||||
state.chatRoom.chosenPlan != null && state.chatRoom.chosenPlan!.chosenPlanState != ChosenPlanStateEnum.stateCanceledByTheInitiator;
|
||||
state.chatRoom.chosenPlan != null &&
|
||||
state.chatRoom.chosenPlan!.chosenPlanState !=
|
||||
ChosenPlanStateEnum.stateCanceledByTheInitiator;
|
||||
BannerType? bannerType;
|
||||
if (chosenPlan == null || chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateCanceledByTheInitiator) {
|
||||
if (chosenPlan == null ||
|
||||
chosenPlan.chosenPlanState ==
|
||||
ChosenPlanStateEnum.stateCanceledByTheInitiator) {
|
||||
bannerType = null;
|
||||
} else if (amIConsultant && !state.hasCard && chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateInCoordination) {
|
||||
} else if (amIConsultant &&
|
||||
!state.hasCard &&
|
||||
chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateInCoordination) {
|
||||
bannerType = BannerType.card;
|
||||
} else if (amIConsultant && chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateInCoordination) {
|
||||
} else if (amIConsultant &&
|
||||
chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateInCoordination) {
|
||||
bannerType = BannerType.date;
|
||||
} else if (!amIConsultant &&
|
||||
(chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateStartDateHasBeenSet || chosenPlan.chosenPlanState == ChosenPlanStateEnum.statePendingPayment)) {
|
||||
(chosenPlan.chosenPlanState ==
|
||||
ChosenPlanStateEnum.stateStartDateHasBeenSet ||
|
||||
chosenPlan.chosenPlanState ==
|
||||
ChosenPlanStateEnum.statePendingPayment)) {
|
||||
bannerType = BannerType.payment;
|
||||
} else if (chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateDone && amIConsultant) {
|
||||
} else if (chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateDone &&
|
||||
amIConsultant) {
|
||||
bannerType = BannerType.collectCharge;
|
||||
} else if (chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateLastDayFinished && !amIConsultant) {
|
||||
} else if (chosenPlan.chosenPlanState ==
|
||||
ChosenPlanStateEnum.stateLastDayFinished &&
|
||||
!amIConsultant) {
|
||||
bannerType = BannerType.finishPlan;
|
||||
} else if (chosenOption?.hasPersonalDiet == true && amIConsultant && chosenPlan.chosenPlanState != ChosenPlanStateEnum.stateDone) {
|
||||
} else if (chosenOption?.hasPersonalDiet == true &&
|
||||
amIConsultant &&
|
||||
chosenPlan.chosenPlanState != ChosenPlanStateEnum.stateDone) {
|
||||
bannerType = BannerType.setPersonalDiet;
|
||||
}
|
||||
|
||||
@@ -223,25 +249,40 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
|
||||
title: GestureDetector(
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('profileScreen', extra: state.chatRoom.users.receiver.slug);
|
||||
GoRouter.of(context).pushNamed(
|
||||
'profileScreen',
|
||||
extra: state.chatRoom.users.receiver.slug,
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
if (state.chatRoom.users.receiver.profilePhotoUrl != null)
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
foregroundImage: NetworkImage(state.chatRoom.users.receiver.profilePhotoUrl!),
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
foregroundImage: NetworkImage(
|
||||
state.chatRoom.users.receiver.profilePhotoUrl!,
|
||||
),
|
||||
backgroundImage: const AssetImage(
|
||||
'assets/images/no_avatar.png',
|
||||
),
|
||||
),
|
||||
if (state.chatRoom.users.receiver.profilePhotoUrl == null)
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
alignment: Alignment.center,
|
||||
decoration: const BoxDecoration(color: Color(0xFFEBF4FF), shape: BoxShape.circle),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFEBF4FF),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Text(
|
||||
state.chatRoom.users.receiver.name.split(' ').first[0] + state.chatRoom.users.receiver.name.split(' ')[1][0],
|
||||
style: const TextStyle(color: Color(0xFF7F9CF5), fontWeight: FontWeight.w400, fontSize: 20),
|
||||
state.chatRoom.users.receiver.name.split(' ').first[0] +
|
||||
state.chatRoom.users.receiver.name.split(' ')[1][0],
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF7F9CF5),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
@@ -253,10 +294,17 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
state.chatRoom.users.receiver.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: CustomTextStyles.titleSmallBold.copyWith(color: PrimaryColors().black),
|
||||
style: CustomTextStyles.titleSmallBold.copyWith(
|
||||
color: PrimaryColors().black,
|
||||
),
|
||||
),
|
||||
AnimatedCrossFade(
|
||||
firstChild: Text('Печатает...', maxLines: 1, overflow: TextOverflow.ellipsis, style: CustomTextStyles.bodySmallMulishPrimary),
|
||||
firstChild: Text(
|
||||
'Печатает...',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: CustomTextStyles.bodySmallMulishPrimary,
|
||||
),
|
||||
firstCurve: Curves.easeIn,
|
||||
secondCurve: Curves.easeOut,
|
||||
secondChild: ChatUserOnlineStatusLine(
|
||||
@@ -265,7 +313,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
style: CustomTextStyles.bodySmallMulishPrimary,
|
||||
showOnlineDot: false,
|
||||
),
|
||||
crossFadeState: state.isTyping ? CrossFadeState.showFirst : CrossFadeState.showSecond,
|
||||
crossFadeState: state.isTyping
|
||||
? CrossFadeState.showFirst
|
||||
: CrossFadeState.showSecond,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
),
|
||||
],
|
||||
@@ -277,7 +327,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 8),
|
||||
child: _ChatHeaderVideoCallButton(onPressed: () => _openVideoCall(context, state, chatCounterpart)),
|
||||
child: _ChatHeaderVideoCallButton(
|
||||
onPressed: () => _openVideoCall(context, state, chatCounterpart),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -291,44 +343,133 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
child: Container(
|
||||
color: appTheme.whiteA700,
|
||||
width: double.maxFinite,
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
controller: scrollController,
|
||||
reverse: true,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
ListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
reverse: true,
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == state.chatMessages.length) {
|
||||
return const Padding(padding: EdgeInsets.all(8), child: Loader());
|
||||
}
|
||||
child: RefreshIndicator(
|
||||
onRefresh: chatRoomCubit.refreshChat,
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(
|
||||
parent: BouncingScrollPhysics(),
|
||||
),
|
||||
controller: scrollController,
|
||||
reverse: true,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
ListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
reverse: true,
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == state.chatMessages.length) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
child: Loader(),
|
||||
);
|
||||
}
|
||||
|
||||
if ((index + 1 != state.chatMessages.length &&
|
||||
state.chatMessages[index].updatedAt.day != state.chatMessages[index + 1].updatedAt.day) ||
|
||||
index + 1 == state.chatMessages.length) {
|
||||
final isCurrentUserAuthor = state.chatRoom.users.receiver.id != state.chatMessages[index].authorId;
|
||||
if ((index + 1 != state.chatMessages.length &&
|
||||
state
|
||||
.chatMessages[index]
|
||||
.updatedAt
|
||||
.day !=
|
||||
state
|
||||
.chatMessages[index + 1]
|
||||
.updatedAt
|
||||
.day) ||
|
||||
index + 1 == state.chatMessages.length) {
|
||||
final isCurrentUserAuthor =
|
||||
state.chatRoom.users.receiver.id !=
|
||||
state.chatMessages[index].authorId;
|
||||
final message = state.chatMessages[index];
|
||||
final isPlanOrderMessage =
|
||||
message.type == ChatMessageType.planOrder;
|
||||
|
||||
PacketOptions? messageOption;
|
||||
if (isPlanOrderMessage &&
|
||||
chosenPlan != null) {
|
||||
if (chosenPlan.optionType == 'options1') {
|
||||
messageOption =
|
||||
chosenPlan.plan.packet1Options;
|
||||
} else if (chosenPlan.optionType ==
|
||||
'options2') {
|
||||
messageOption =
|
||||
chosenPlan.plan.packet2Options;
|
||||
} else if (chosenPlan.optionType ==
|
||||
'options3') {
|
||||
messageOption =
|
||||
chosenPlan.plan.packet3Options;
|
||||
}
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_messageDate(message.updatedAt),
|
||||
if (isPlanOrderMessage &&
|
||||
chosenPlan != null &&
|
||||
messageOption != null)
|
||||
PlanOrderMessageWidget(
|
||||
planName: chosenPlan.plan.name,
|
||||
planSlug: chosenPlan.plan.slug,
|
||||
price: messageOption.price,
|
||||
sendTime: message.updatedAt,
|
||||
isCurrentUser: isCurrentUserAuthor,
|
||||
),
|
||||
VisibilityDetector(
|
||||
key: ValueKey(
|
||||
'msg-${message.uuid ?? message.id}',
|
||||
),
|
||||
onVisibilityChanged:
|
||||
(VisibilityInfo info) {
|
||||
_queueMarkAsRead(
|
||||
context: context,
|
||||
chatId: state.chatRoom.id,
|
||||
message: message,
|
||||
isCurrentUserAuthor:
|
||||
isCurrentUserAuthor,
|
||||
visibleFraction:
|
||||
info.visibleFraction,
|
||||
);
|
||||
},
|
||||
child: _buildMessageBubble(
|
||||
context: context,
|
||||
state: state,
|
||||
chatCounterpart: chatCounterpart,
|
||||
isCurrentUserAuthor:
|
||||
isCurrentUserAuthor,
|
||||
message: message,
|
||||
amIConsultant: amIConsultant,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
final isCurrentUserAuthor =
|
||||
state.chatRoom.users.receiver.id !=
|
||||
state.chatMessages[index].authorId;
|
||||
final message = state.chatMessages[index];
|
||||
final isPlanOrderMessage = message.type == ChatMessageType.planOrder;
|
||||
final isPlanOrderMessage =
|
||||
message.type == ChatMessageType.planOrder;
|
||||
|
||||
PacketOptions? messageOption;
|
||||
if (isPlanOrderMessage && chosenPlan != null) {
|
||||
if (chosenPlan.optionType == 'options1') {
|
||||
messageOption = chosenPlan.plan.packet1Options;
|
||||
} else if (chosenPlan.optionType == 'options2') {
|
||||
messageOption = chosenPlan.plan.packet2Options;
|
||||
} else if (chosenPlan.optionType == 'options3') {
|
||||
messageOption = chosenPlan.plan.packet3Options;
|
||||
messageOption =
|
||||
chosenPlan.plan.packet1Options;
|
||||
} else if (chosenPlan.optionType ==
|
||||
'options2') {
|
||||
messageOption =
|
||||
chosenPlan.plan.packet2Options;
|
||||
} else if (chosenPlan.optionType ==
|
||||
'options3') {
|
||||
messageOption =
|
||||
chosenPlan.plan.packet3Options;
|
||||
}
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_messageDate(message.updatedAt),
|
||||
if (isPlanOrderMessage && chosenPlan != null && messageOption != null)
|
||||
if (isPlanOrderMessage &&
|
||||
chosenPlan != null &&
|
||||
messageOption != null)
|
||||
PlanOrderMessageWidget(
|
||||
planName: chosenPlan.plan.name,
|
||||
planSlug: chosenPlan.plan.slug,
|
||||
@@ -337,117 +478,94 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
isCurrentUser: isCurrentUserAuthor,
|
||||
),
|
||||
VisibilityDetector(
|
||||
key: ValueKey('msg-${message.uuid ?? message.id}'),
|
||||
onVisibilityChanged: (VisibilityInfo info) {
|
||||
_queueMarkAsRead(
|
||||
context: context,
|
||||
chatId: state.chatRoom.id,
|
||||
message: message,
|
||||
isCurrentUserAuthor: isCurrentUserAuthor,
|
||||
visibleFraction: info.visibleFraction,
|
||||
);
|
||||
},
|
||||
key: ValueKey(
|
||||
'msg-${message.uuid ?? message.id}',
|
||||
),
|
||||
onVisibilityChanged:
|
||||
(VisibilityInfo info) {
|
||||
_queueMarkAsRead(
|
||||
context: context,
|
||||
chatId: state.chatRoom.id,
|
||||
message: message,
|
||||
isCurrentUserAuthor:
|
||||
isCurrentUserAuthor,
|
||||
visibleFraction:
|
||||
info.visibleFraction,
|
||||
);
|
||||
},
|
||||
child: _buildMessageBubble(
|
||||
context: context,
|
||||
state: state,
|
||||
chatCounterpart: chatCounterpart,
|
||||
isCurrentUserAuthor: isCurrentUserAuthor,
|
||||
isCurrentUserAuthor:
|
||||
isCurrentUserAuthor,
|
||||
message: message,
|
||||
amIConsultant: amIConsultant,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
final isCurrentUserAuthor = state.chatRoom.users.receiver.id != state.chatMessages[index].authorId;
|
||||
final message = state.chatMessages[index];
|
||||
final isPlanOrderMessage = message.type == ChatMessageType.planOrder;
|
||||
|
||||
PacketOptions? messageOption;
|
||||
if (isPlanOrderMessage && chosenPlan != null) {
|
||||
if (chosenPlan.optionType == 'options1') {
|
||||
messageOption = chosenPlan.plan.packet1Options;
|
||||
} else if (chosenPlan.optionType == 'options2') {
|
||||
messageOption = chosenPlan.plan.packet2Options;
|
||||
} else if (chosenPlan.optionType == 'options3') {
|
||||
messageOption = chosenPlan.plan.packet3Options;
|
||||
}
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (isPlanOrderMessage && chosenPlan != null && messageOption != null)
|
||||
PlanOrderMessageWidget(
|
||||
planName: chosenPlan.plan.name,
|
||||
planSlug: chosenPlan.plan.slug,
|
||||
price: messageOption.price,
|
||||
sendTime: message.updatedAt,
|
||||
isCurrentUser: isCurrentUserAuthor,
|
||||
},
|
||||
itemCount:
|
||||
state.chatMessages.length +
|
||||
(state.isLoading ? 1 : 0),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (showUserDetailsButton) ...[
|
||||
// OrderCard(
|
||||
// plan: chosenPlan.plan,
|
||||
// isCustomerView: !amIConsultant,
|
||||
// optionType: chosenPlan.optionType,
|
||||
// onCancel: () async {
|
||||
// await context.read<ChatRoomCubit>().cancelChosenPlan(chatId: state.chatRoom.id, planId: chosenPlan.id);
|
||||
// },
|
||||
// ),
|
||||
if (amIConsultant)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
horizontal: 16,
|
||||
),
|
||||
child: CustomOutlinedButton(
|
||||
text: S.current.user_info,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold
|
||||
.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(
|
||||
side: const WidgetStatePropertyAll(
|
||||
BorderSide.none,
|
||||
),
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
appTheme.blueA400,
|
||||
),
|
||||
),
|
||||
VisibilityDetector(
|
||||
key: ValueKey('msg-${message.uuid ?? message.id}'),
|
||||
onVisibilityChanged: (VisibilityInfo info) {
|
||||
_queueMarkAsRead(
|
||||
context: context,
|
||||
chatId: state.chatRoom.id,
|
||||
message: message,
|
||||
isCurrentUserAuthor: isCurrentUserAuthor,
|
||||
visibleFraction: info.visibleFraction,
|
||||
onPressed: () async {
|
||||
GoRouter.of(context).push(
|
||||
UserDetailsScreen.path,
|
||||
extra:
|
||||
state.chatRoom.users.receiver.slug,
|
||||
);
|
||||
},
|
||||
child: _buildMessageBubble(
|
||||
context: context,
|
||||
state: state,
|
||||
chatCounterpart: chatCounterpart,
|
||||
isCurrentUserAuthor: isCurrentUserAuthor,
|
||||
message: message,
|
||||
amIConsultant: amIConsultant,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
itemCount: state.chatMessages.length + (state.isLoading ? 1 : 0),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (showUserDetailsButton) ...[
|
||||
// OrderCard(
|
||||
// plan: chosenPlan.plan,
|
||||
// isCustomerView: !amIConsultant,
|
||||
// optionType: chosenPlan.optionType,
|
||||
// onCancel: () async {
|
||||
// await context.read<ChatRoomCubit>().cancelChosenPlan(chatId: state.chatRoom.id, planId: chosenPlan.id);
|
||||
// },
|
||||
// ),
|
||||
if (amIConsultant)
|
||||
),
|
||||
],
|
||||
if (bannerType != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
child: CustomOutlinedButton(
|
||||
text: S.current.user_info,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||
),
|
||||
onPressed: () async {
|
||||
GoRouter.of(context).push(UserDetailsScreen.path, extra: state.chatRoom.users.receiver.slug);
|
||||
},
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
horizontal: 16,
|
||||
),
|
||||
child: NotificationBanner(
|
||||
bannerType: bannerType,
|
||||
chosenPlanId: chosenPlan?.id,
|
||||
planName: chosenPlan?.plan.name,
|
||||
planPrice: chosenOption?.price.toDouble(),
|
||||
hasPersonalDiet:
|
||||
chosenOption?.hasPersonalDiet ?? false,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (bannerType != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
child: NotificationBanner(
|
||||
bannerType: bannerType,
|
||||
chosenPlanId: chosenPlan?.id,
|
||||
planName: chosenPlan?.plan.name,
|
||||
planPrice: chosenOption?.price.toDouble(),
|
||||
hasPersonalDiet: chosenOption?.hasPersonalDiet ?? false,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -472,19 +590,40 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
_isImageFile(pickedFiles[i].path)
|
||||
? GestureDetector(
|
||||
onTap: () {
|
||||
final imageFiles = pickedFiles.where((f) => _isImageFile(f.path)).toList();
|
||||
final imageIndex = _imageIndexAt(pickedFiles, i);
|
||||
final imageFiles = pickedFiles
|
||||
.where(
|
||||
(f) => _isImageFile(f.path),
|
||||
)
|
||||
.toList();
|
||||
final imageIndex = _imageIndexAt(
|
||||
pickedFiles,
|
||||
i,
|
||||
);
|
||||
|
||||
context.pushTransparentRoute(
|
||||
PhotoViewScreen(imageUrls: imageFiles.map((e) => e.path).toList(), isLocal: true, index: imageIndex),
|
||||
PhotoViewScreen(
|
||||
imageUrls: imageFiles
|
||||
.map((e) => e.path)
|
||||
.toList(),
|
||||
isLocal: true,
|
||||
index: imageIndex,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Hero(
|
||||
tag: pickedFiles[i].path,
|
||||
transitionOnUserGestures: true,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: SizedBox(height: 104, width: 104, child: Image.file(pickedFiles[i], fit: BoxFit.cover)),
|
||||
borderRadius:
|
||||
BorderRadius.circular(8),
|
||||
child: SizedBox(
|
||||
height: 104,
|
||||
width: 104,
|
||||
child: Image.file(
|
||||
pickedFiles[i],
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -501,8 +640,15 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(color: appTheme.gray50, shape: BoxShape.circle),
|
||||
child: const Icon(size: 20, Icons.close, color: Colors.black),
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.gray50,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
size: 20,
|
||||
Icons.close,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -519,14 +665,20 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
children: [
|
||||
const SizedBox(width: 8),
|
||||
InkWell(
|
||||
onTap: () => showImagePicker(state.chatRoom.users.receiver.slug),
|
||||
onTap: () => showImagePicker(
|
||||
state.chatRoom.users.receiver.slug,
|
||||
),
|
||||
customBorder: const CircleBorder(),
|
||||
child: Ink(
|
||||
height: 40,
|
||||
width: 40,
|
||||
child: Transform.rotate(
|
||||
angle: pi / 4,
|
||||
child: const Icon(Icons.attach_file_rounded, size: 30, color: Color(0xFF858E99)),
|
||||
child: const Icon(
|
||||
Icons.attach_file_rounded,
|
||||
size: 30,
|
||||
color: Color(0xFF858E99),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -538,24 +690,46 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
maxLines: 3,
|
||||
minLines: 1,
|
||||
keyboardType: TextInputType.multiline,
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.black,
|
||||
height: 1,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 10,
|
||||
),
|
||||
hintText: 'Сообщение',
|
||||
hintStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w400, height: 1, color: Color(0xFFAEAEB2)),
|
||||
hintStyle: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1,
|
||||
color: Color(0xFFAEAEB2),
|
||||
),
|
||||
filled: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||
borderSide: const BorderSide(
|
||||
color: Color(0xFFD1D1D6),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||
borderSide: const BorderSide(
|
||||
color: Color(0xFFD1D1D6),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||
borderSide: const BorderSide(
|
||||
color: Color(0xFFD1D1D6),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -570,12 +744,18 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
final blockedState = blockedUsersCubit.state;
|
||||
|
||||
if (blockedState is BlockedUsersStateLoaded) {
|
||||
final isBlocked = blockedState.profilesList.any((profile) => profile.slug == state.chatRoom.users.receiver.slug);
|
||||
final isBlocked = blockedState.profilesList.any(
|
||||
(profile) =>
|
||||
profile.slug ==
|
||||
state.chatRoom.users.receiver.slug,
|
||||
);
|
||||
if (isBlocked) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Вы не можете отправить сообщение заблокированному пользователю'),
|
||||
content: Text(
|
||||
'Вы не можете отправить сообщение заблокированному пользователю',
|
||||
),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
@@ -584,10 +764,15 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
if (textEditingController.text.isNotEmpty || pickedFiles.isNotEmpty) {
|
||||
if (textEditingController.text.isNotEmpty ||
|
||||
pickedFiles.isNotEmpty) {
|
||||
chatRoomCubit.sendMessage(
|
||||
data: textEditingController.text.isEmpty ? null : textEditingController.text.trim(),
|
||||
files: pickedFiles.isEmpty ? null : pickedFiles,
|
||||
data: textEditingController.text.isEmpty
|
||||
? null
|
||||
: textEditingController.text.trim(),
|
||||
files: pickedFiles.isEmpty
|
||||
? null
|
||||
: pickedFiles,
|
||||
userSlug: state.chatRoom.users.receiver.slug,
|
||||
);
|
||||
setState(() {
|
||||
@@ -597,10 +782,17 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
}
|
||||
},
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(color: appTheme.blueA400, shape: BoxShape.circle),
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.blueA400,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
height: 40,
|
||||
width: 40,
|
||||
child: const Icon(Icons.arrow_upward_rounded, size: 24, color: Colors.white),
|
||||
child: const Icon(
|
||||
Icons.arrow_upward_rounded,
|
||||
size: 24,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
@@ -619,12 +811,19 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
child: AboutOrderButton(
|
||||
onTap: () async {
|
||||
if (amIConsultant) {
|
||||
GoRouter.of(
|
||||
context,
|
||||
).push(InfoByOrderScreen.path, extra: {'planId': state.chatRoom.chosenPlan?.id, 'customerSlug': state.chatRoom.users.receiver.slug});
|
||||
GoRouter.of(context).push(
|
||||
InfoByOrderScreen.path,
|
||||
extra: {
|
||||
'planId': state.chatRoom.chosenPlan?.id,
|
||||
'customerSlug': state.chatRoom.users.receiver.slug,
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
GoRouter.of(context).push(InfoByOrderScreen.path, extra: {'planId': state.chatRoom.chosenPlan?.id});
|
||||
GoRouter.of(context).push(
|
||||
InfoByOrderScreen.path,
|
||||
extra: {'planId': state.chatRoom.chosenPlan?.id},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -649,7 +848,13 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
const Icon(Icons.attach_file, size: 32, color: Colors.black54),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Text(fileName, textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12)),
|
||||
child: Text(
|
||||
fileName,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -697,7 +902,13 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
// await chatRoomCubit.sendMessage(files: [File(image.path)], userSlug: slug);
|
||||
// }
|
||||
},
|
||||
child: const Row(children: [Icon(Icons.photo_camera), SizedBox(width: 8), Text('Фото')]),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.photo_camera),
|
||||
SizedBox(width: 8),
|
||||
Text('Фото'),
|
||||
],
|
||||
),
|
||||
),
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
@@ -706,13 +917,21 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
List<XFile> images = await picker.pickMultiImage();
|
||||
if (images != []) {
|
||||
setState(() {
|
||||
pickedFiles.addAll(images.map((image) => File(image.path)).toList());
|
||||
pickedFiles.addAll(
|
||||
images.map((image) => File(image.path)).toList(),
|
||||
);
|
||||
});
|
||||
// List<File> files = images.map((image) => File(image.path)).toList();
|
||||
// await chatRoomCubit.sendMessage(files: files, userSlug: slug);
|
||||
}
|
||||
},
|
||||
child: const Row(children: [Icon(Icons.photo_library_rounded), SizedBox(width: 8), Text('Галерея')]),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.photo_library_rounded),
|
||||
SizedBox(width: 8),
|
||||
Text('Галерея'),
|
||||
],
|
||||
),
|
||||
),
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
@@ -744,15 +963,27 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
if (result != null) {
|
||||
// List<File> files = result.paths.map((path) => File(path!)).toList();
|
||||
setState(() {
|
||||
pickedFiles.addAll(result.paths.map((path) => File(path!)).toList());
|
||||
pickedFiles.addAll(
|
||||
result.paths.map((path) => File(path!)).toList(),
|
||||
);
|
||||
});
|
||||
// await chatRoomCubit.sendMessage(files: files, userSlug: slug);
|
||||
}
|
||||
},
|
||||
child: const Row(children: [Icon(Icons.file_present_rounded), SizedBox(width: 8), Text('Файлы')]),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.file_present_rounded),
|
||||
SizedBox(width: 8),
|
||||
Text('Файлы'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
cancelButton: CupertinoActionSheetAction(onPressed: () => Navigator.of(context).pop(), isDestructiveAction: true, child: const Text('Отмена')),
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
isDestructiveAction: true,
|
||||
child: const Text('Отмена'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -763,13 +994,21 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
padding: const EdgeInsets.only(top: 32),
|
||||
child: Text(
|
||||
DateFormat.yMMMMd('RU').format(date),
|
||||
style: const TextStyle(height: 1, color: Color(0xFF8393A1), fontWeight: FontWeight.w500),
|
||||
style: const TextStyle(
|
||||
height: 1,
|
||||
color: Color(0xFF8393A1),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _openVideoCall(BuildContext context, ChatRoomStateLoaded state, ChatUserModel chatCounterpart) {
|
||||
void _openVideoCall(
|
||||
BuildContext context,
|
||||
ChatRoomStateLoaded state,
|
||||
ChatUserModel chatCounterpart,
|
||||
) {
|
||||
GoRouter.of(context).pushNamed(
|
||||
VideoCallScreen.name,
|
||||
extra: <String, dynamic>{
|
||||
@@ -797,18 +1036,28 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
if (!unreadMessageIds.contains(message.id)) {
|
||||
unreadMessageIds.add(message.id);
|
||||
}
|
||||
EasyDebounce.debounce('mark_as_read', const Duration(milliseconds: 500), () async {
|
||||
final ids = List<int>.from(unreadMessageIds);
|
||||
if (ids.isEmpty) {
|
||||
return;
|
||||
}
|
||||
unreadMessageIds.clear();
|
||||
final result = await context.read<ChatRoomCubit>().markMessagesAsRead(chatId, ids);
|
||||
if (!result || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
context.read<ChatsCubit>().reduceUnreadMessagesCount(chatId: chatId, count: ids.length);
|
||||
});
|
||||
EasyDebounce.debounce(
|
||||
'mark_as_read',
|
||||
const Duration(milliseconds: 500),
|
||||
() async {
|
||||
final ids = List<int>.from(unreadMessageIds);
|
||||
if (ids.isEmpty) {
|
||||
return;
|
||||
}
|
||||
unreadMessageIds.clear();
|
||||
final result = await context.read<ChatRoomCubit>().markMessagesAsRead(
|
||||
chatId,
|
||||
ids,
|
||||
);
|
||||
if (!result || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
context.read<ChatsCubit>().reduceUnreadMessagesCount(
|
||||
chatId: chatId,
|
||||
count: ids.length,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMessageBubble({
|
||||
@@ -819,11 +1068,13 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
required ChatMessageModel message,
|
||||
required bool? amIConsultant,
|
||||
}) {
|
||||
if (message.type == ChatMessageType.videoCallStarted && !isCurrentUserAuthor) {
|
||||
if (message.type == ChatMessageType.videoCallStarted &&
|
||||
!isCurrentUserAuthor) {
|
||||
return VideoCallChatMessageWidget(
|
||||
text: message.data.trim(),
|
||||
sendTime: message.updatedAt,
|
||||
onVideoCallPressed: () => _openVideoCall(context, state, chatCounterpart),
|
||||
onVideoCallPressed: () =>
|
||||
_openVideoCall(context, state, chatCounterpart),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -862,7 +1113,13 @@ class _ChatHeaderVideoCallButton extends StatelessWidget {
|
||||
child: SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: Center(child: Icon(Icons.videocam_rounded, color: appTheme.blueA400, size: 18)),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.videocam_rounded,
|
||||
color: appTheme.blueA400,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -27,6 +27,12 @@ class AddItemWidget extends StatefulWidget {
|
||||
class _AddItemWidgetState extends State<AddItemWidget> {
|
||||
final controller = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
@@ -38,14 +44,24 @@ class _AddItemWidgetState extends State<AddItemWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
if (widget.items.length > widget.index && controller.text != widget.items[widget.index]) {
|
||||
if (widget.items.length > widget.index &&
|
||||
controller.text != widget.items[widget.index]) {
|
||||
controller.text = widget.items[widget.index];
|
||||
}
|
||||
return BlocBuilder<CreatePlanCubit, CreatePlanState>(
|
||||
buildWhen: (previous, current) => previous.validationState != current.validationState,
|
||||
buildWhen: (previous, current) =>
|
||||
previous.validationState != current.validationState,
|
||||
builder: (context, state) {
|
||||
final hasError = state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].isItemsEmpty ||
|
||||
(state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].itemsErrors[widget.index] ?? false);
|
||||
final hasError =
|
||||
state
|
||||
.validationState
|
||||
.optionsValidationState[state.selectedOptionIndex ?? 0]
|
||||
.isItemsEmpty ||
|
||||
(state
|
||||
.validationState
|
||||
.optionsValidationState[state.selectedOptionIndex ?? 0]
|
||||
.itemsErrors[widget.index] ??
|
||||
false);
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
@@ -54,9 +70,14 @@ class _AddItemWidgetState extends State<AddItemWidget> {
|
||||
child: CustomTextFormField(
|
||||
controller: controller,
|
||||
hasError: hasError,
|
||||
hintText: widget.enableHint ? s.option_content_hint_text : null,
|
||||
hintText: widget.enableHint
|
||||
? s.option_content_hint_text
|
||||
: null,
|
||||
onChanged: (value) {
|
||||
context.read<CreatePlanCubit>().setOptionContent(value, widget.index);
|
||||
context.read<CreatePlanCubit>().setOptionContent(
|
||||
value,
|
||||
widget.index,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -64,7 +85,9 @@ class _AddItemWidgetState extends State<AddItemWidget> {
|
||||
const SizedBox(width: 8),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.read<CreatePlanCubit>().removeOptionContentItem(widget.index);
|
||||
context.read<CreatePlanCubit>().removeOptionContentItem(
|
||||
widget.index,
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
@@ -72,19 +95,30 @@ class _AddItemWidgetState extends State<AddItemWidget> {
|
||||
color: appTheme.lightGray0,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: SvgPicture.asset('assets/icons/trash_can.svg', width: 20, height: 24),
|
||||
child: SvgPicture.asset(
|
||||
'assets/icons/trash_can.svg',
|
||||
width: 20,
|
||||
height: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
if (state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].isItemsEmpty) ...[
|
||||
if (state
|
||||
.validationState
|
||||
.optionsValidationState[state.selectedOptionIndex ?? 0]
|
||||
.isItemsEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
ValidationErrorWidget(error: s.addAtLeastOneItem)
|
||||
] else if (state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].itemsErrors[widget.index] ?? false) ...[
|
||||
ValidationErrorWidget(error: s.addAtLeastOneItem),
|
||||
] else if (state
|
||||
.validationState
|
||||
.optionsValidationState[state.selectedOptionIndex ?? 0]
|
||||
.itemsErrors[widget.index] ??
|
||||
false) ...[
|
||||
const SizedBox(height: 4),
|
||||
ValidationErrorWidget(error: s.atLeast3Symbols)
|
||||
]
|
||||
ValidationErrorWidget(error: s.atLeast3Symbols),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:eatvio_app/features/create_diet/presentation/widgets/error_widge
|
||||
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_cubit.dart';
|
||||
import 'package:eatvio_app/features/create_plan/presentation/cubit/create_plan_state.dart';
|
||||
import 'package:eatvio_app/features/create_plan/presentation/widgets/add_item_widget.dart';
|
||||
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -21,47 +22,107 @@ class OptionWidget extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _OptionWidgetState extends State<OptionWidget> {
|
||||
final firstOptionCustomerPriceController = TextEditingController()..text = '1200₽';
|
||||
final secondOptionCustomerPriceController = TextEditingController()..text = '1200₽';
|
||||
final thirdOptionCustomerPriceController = TextEditingController()..text = '1200₽';
|
||||
static const _optionsCount = 3;
|
||||
|
||||
final creatorPriceController = TextEditingController()..text = '1000₽';
|
||||
|
||||
final firstOptionTitleController = TextEditingController();
|
||||
final secondOptionTitleController = TextEditingController();
|
||||
final thirdOptionTitleController = TextEditingController();
|
||||
|
||||
final firstOptionDescriptionController = TextEditingController();
|
||||
final secondOptionDescriptionController = TextEditingController();
|
||||
final thirdOptionDescriptionController = TextEditingController();
|
||||
final List<TextEditingController> _customerPriceControllers = List.generate(
|
||||
_optionsCount,
|
||||
(_) => TextEditingController(text: '1200₽'),
|
||||
);
|
||||
final List<TextEditingController> _titleControllers = List.generate(
|
||||
_optionsCount,
|
||||
(_) => TextEditingController(),
|
||||
);
|
||||
final List<TextEditingController> _descriptionControllers = List.generate(
|
||||
_optionsCount,
|
||||
(_) => TextEditingController(),
|
||||
);
|
||||
final creatorPriceController = TextEditingController(text: '1000₽');
|
||||
|
||||
int? _syncedOptionIndex;
|
||||
bool firstExplanationVisible = false;
|
||||
bool isChecked = false;
|
||||
bool secondExplanationVisible = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
firstOptionCustomerPriceController.addListener(() {
|
||||
if (!firstOptionCustomerPriceController.text.endsWith('₽')) {
|
||||
final text = firstOptionCustomerPriceController.text.replaceAll('₽', '');
|
||||
firstOptionCustomerPriceController.text = '$text₽';
|
||||
firstOptionCustomerPriceController.selection = TextSelection.collapsed(offset: text.length);
|
||||
}
|
||||
});
|
||||
secondOptionCustomerPriceController.addListener(() {
|
||||
if (!secondOptionCustomerPriceController.text.endsWith('₽')) {
|
||||
final text = secondOptionCustomerPriceController.text.replaceAll('₽', '');
|
||||
secondOptionCustomerPriceController.text = '$text₽';
|
||||
secondOptionCustomerPriceController.selection = TextSelection.collapsed(offset: text.length);
|
||||
}
|
||||
});
|
||||
thirdOptionCustomerPriceController.addListener(() {
|
||||
if (!thirdOptionCustomerPriceController.text.endsWith('₽')) {
|
||||
final text = thirdOptionCustomerPriceController.text.replaceAll('₽', '');
|
||||
thirdOptionCustomerPriceController.text = '$text₽';
|
||||
thirdOptionCustomerPriceController.selection = TextSelection.collapsed(offset: text.length);
|
||||
}
|
||||
});
|
||||
for (final controller in _customerPriceControllers) {
|
||||
controller.addListener(() => _ensureRubleSuffix(controller));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final controller in _customerPriceControllers) {
|
||||
controller.dispose();
|
||||
}
|
||||
for (final controller in _titleControllers) {
|
||||
controller.dispose();
|
||||
}
|
||||
for (final controller in _descriptionControllers) {
|
||||
controller.dispose();
|
||||
}
|
||||
creatorPriceController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _ensureRubleSuffix(TextEditingController controller) {
|
||||
if (!controller.text.endsWith('₽')) {
|
||||
final text = controller.text.replaceAll('₽', '');
|
||||
controller.text = '$text₽';
|
||||
controller.selection = TextSelection.collapsed(offset: text.length);
|
||||
}
|
||||
}
|
||||
|
||||
void _syncController(TextEditingController controller, String value) {
|
||||
if (controller.text == value) {
|
||||
return;
|
||||
}
|
||||
controller.value = TextEditingValue(
|
||||
text: value,
|
||||
selection: TextSelection.collapsed(offset: value.length),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatCustomerPrice(dynamic price) {
|
||||
if (price is num) {
|
||||
return '${price.toStringAsFixed(0)}₽';
|
||||
}
|
||||
final parsed = double.tryParse(price.toString().replaceAll('₽', ''));
|
||||
return '${(parsed ?? 0).toStringAsFixed(0)}₽';
|
||||
}
|
||||
|
||||
void _applyOptionToControllers(int index, PacketOptions option) {
|
||||
_syncController(_titleControllers[index], option.short);
|
||||
_syncController(_descriptionControllers[index], option.description ?? '');
|
||||
_syncController(
|
||||
_customerPriceControllers[index],
|
||||
_formatCustomerPrice(option.price),
|
||||
);
|
||||
}
|
||||
|
||||
void _syncSelectedOption(CreatePlanState state) {
|
||||
final selectedOptionIndex = state.selectedOptionIndex ?? 0;
|
||||
final option = state.planData.options[selectedOptionIndex];
|
||||
final price = double.tryParse(
|
||||
_customerPriceControllers[selectedOptionIndex].text.replaceAll('₽', ''),
|
||||
);
|
||||
final creatorPrice = '${((price ?? 0) * 0.8).toStringAsFixed(2)}₽';
|
||||
|
||||
if (_syncedOptionIndex != selectedOptionIndex) {
|
||||
_syncedOptionIndex = selectedOptionIndex;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
_applyOptionToControllers(selectedOptionIndex, option);
|
||||
creatorPriceController.text = creatorPrice;
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (creatorPriceController.text != creatorPrice) {
|
||||
creatorPriceController.text = creatorPrice;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -70,31 +131,21 @@ class _OptionWidgetState extends State<OptionWidget> {
|
||||
return BlocBuilder<CreatePlanCubit, CreatePlanState>(
|
||||
builder: (context, state) {
|
||||
final selectedOptionIndex = state.selectedOptionIndex ?? 0;
|
||||
_syncSelectedOption(state);
|
||||
final optionNameHintText = switch (selectedOptionIndex) {
|
||||
0 => s.economy,
|
||||
2 => s.comfort,
|
||||
_ => s.standard,
|
||||
};
|
||||
final optionTitleController = switch (selectedOptionIndex) {
|
||||
0 => firstOptionTitleController,
|
||||
2 => thirdOptionTitleController,
|
||||
_ => secondOptionTitleController,
|
||||
};
|
||||
final optionDescriptionController = switch (selectedOptionIndex) {
|
||||
0 => firstOptionDescriptionController,
|
||||
2 => thirdOptionDescriptionController,
|
||||
_ => secondOptionDescriptionController,
|
||||
};
|
||||
final customerPriceController = switch (selectedOptionIndex) {
|
||||
0 => firstOptionCustomerPriceController,
|
||||
2 => thirdOptionCustomerPriceController,
|
||||
_ => secondOptionCustomerPriceController,
|
||||
};
|
||||
final optionTitleController = _titleControllers[selectedOptionIndex];
|
||||
final optionDescriptionController =
|
||||
_descriptionControllers[selectedOptionIndex];
|
||||
final customerPriceController =
|
||||
_customerPriceControllers[selectedOptionIndex];
|
||||
final optionItems = state.planData.options[selectedOptionIndex].items;
|
||||
final optionValidation =
|
||||
state.validationState.optionsValidationState[selectedOptionIndex];
|
||||
|
||||
final price = double.tryParse(customerPriceController.text.replaceAll('₽', ''));
|
||||
creatorPriceController.text = '${((price ?? 0) * 0.8).toStringAsFixed(2)}₽';
|
||||
firstOptionTitleController.text = state.planData.options[selectedOptionIndex].short;
|
||||
firstOptionDescriptionController.text = state.planData.options[selectedOptionIndex].description ?? '';
|
||||
return MainBackgroundBouble(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
@@ -110,14 +161,14 @@ class _OptionWidgetState extends State<OptionWidget> {
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
hintText: optionNameHintText,
|
||||
hasError: state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].titleHasError,
|
||||
hasError: optionValidation.titleHasError,
|
||||
controller: optionTitleController,
|
||||
onChanged: (value) {
|
||||
context.read<CreatePlanCubit>().setOptionTitle(value);
|
||||
},
|
||||
maxLength: 150,
|
||||
),
|
||||
if (state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].titleHasError) ...[
|
||||
if (optionValidation.titleHasError) ...[
|
||||
const SizedBox(height: 4),
|
||||
ValidationErrorWidget(error: s.atLeast3Symbols),
|
||||
],
|
||||
@@ -135,7 +186,7 @@ class _OptionWidgetState extends State<OptionWidget> {
|
||||
child: CustomTextFormField(
|
||||
controller: optionDescriptionController,
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
hasError: state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].descriptionHasError,
|
||||
hasError: optionValidation.descriptionHasError,
|
||||
maxLength: 1000,
|
||||
minLines: null,
|
||||
maxLines: null,
|
||||
@@ -146,7 +197,7 @@ class _OptionWidgetState extends State<OptionWidget> {
|
||||
},
|
||||
),
|
||||
),
|
||||
if (state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].descriptionHasError) ...[
|
||||
if (optionValidation.descriptionHasError) ...[
|
||||
const SizedBox(height: 4),
|
||||
ValidationErrorWidget(error: s.atLeast3Symbols),
|
||||
],
|
||||
@@ -158,7 +209,9 @@ class _OptionWidgetState extends State<OptionWidget> {
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
|
||||
style: TextStyles.textSMedium.copyWith(
|
||||
color: appTheme.black,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const _PriceExplanationWidget(),
|
||||
@@ -179,7 +232,9 @@ class _OptionWidgetState extends State<OptionWidget> {
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
|
||||
style: TextStyles.textSMedium.copyWith(
|
||||
color: appTheme.black,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const _PriceExplanationWidget(),
|
||||
@@ -188,8 +243,10 @@ class _OptionWidgetState extends State<OptionWidget> {
|
||||
const SizedBox(height: 4),
|
||||
CustomTextFormField(
|
||||
controller: customerPriceController,
|
||||
textInputType: const TextInputType.numberWithOptions(decimal: true),
|
||||
hasError: state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].priceHasError,
|
||||
textInputType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
hasError: optionValidation.priceHasError,
|
||||
onChanged: (value) {
|
||||
final price = double.tryParse(value.replaceAll('₽', ''));
|
||||
if (price != null) {
|
||||
@@ -222,34 +279,41 @@ class _OptionWidgetState extends State<OptionWidget> {
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 8),
|
||||
reverse: false,
|
||||
itemBuilder: (context, index) {
|
||||
final items = state.planData.options[selectedOptionIndex].items;
|
||||
|
||||
if (items.isEmpty) {
|
||||
if (optionItems.isEmpty) {
|
||||
return AddItemWidget(
|
||||
key: ValueKey(
|
||||
'plan-option-$selectedOptionIndex-item-$index',
|
||||
),
|
||||
index: index,
|
||||
canBeDeleted: false,
|
||||
enableHint: true,
|
||||
items: items,
|
||||
items: optionItems,
|
||||
);
|
||||
} else {
|
||||
if (index < items.length) {
|
||||
if (index < optionItems.length) {
|
||||
return AddItemWidget(
|
||||
key: ValueKey(
|
||||
'plan-option-$selectedOptionIndex-item-$index',
|
||||
),
|
||||
canBeDeleted: true,
|
||||
enableHint: false,
|
||||
index: index,
|
||||
items: items,
|
||||
items: optionItems,
|
||||
);
|
||||
} else {
|
||||
return AddItemWidget(
|
||||
key: ValueKey(
|
||||
'plan-option-$selectedOptionIndex-item-$index',
|
||||
),
|
||||
canBeDeleted: false,
|
||||
enableHint: false,
|
||||
index: index,
|
||||
items: items,
|
||||
items: optionItems,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
itemCount: state.planData.options[selectedOptionIndex].items.length + 1,
|
||||
itemCount: optionItems.length + 1,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
),
|
||||
@@ -259,23 +323,30 @@ class _OptionWidgetState extends State<OptionWidget> {
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: state.planData.options[selectedOptionIndex].hasPersonalDiet,
|
||||
value: state
|
||||
.planData
|
||||
.options[selectedOptionIndex]
|
||||
.hasPersonalDiet,
|
||||
activeColor: appTheme.gray,
|
||||
checkColor: appTheme.gray,
|
||||
fillColor: const WidgetStatePropertyAll(Colors.transparent),
|
||||
side: WidgetStateBorderSide.resolveWith(
|
||||
(Set<WidgetState> states) {
|
||||
return BorderSide(width: 1, color: appTheme.gray);
|
||||
},
|
||||
),
|
||||
side: WidgetStateBorderSide.resolveWith((
|
||||
Set<WidgetState> states,
|
||||
) {
|
||||
return BorderSide(width: 1, color: appTheme.gray);
|
||||
}),
|
||||
onChanged: (bool? value) {
|
||||
context.read<CreatePlanCubit>().togglePersonalizedDietOptionState();
|
||||
context
|
||||
.read<CreatePlanCubit>()
|
||||
.togglePersonalizedDietOptionState();
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
s.option_personal_diet_desc,
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||
style: TextStyles.textMMedium.copyWith(
|
||||
color: appTheme.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -292,7 +363,8 @@ class _PriceExplanationWidget extends StatefulWidget {
|
||||
const _PriceExplanationWidget();
|
||||
|
||||
@override
|
||||
State<_PriceExplanationWidget> createState() => _PriceExplanationWidgetState();
|
||||
State<_PriceExplanationWidget> createState() =>
|
||||
_PriceExplanationWidgetState();
|
||||
}
|
||||
|
||||
class _PriceExplanationWidgetState extends State<_PriceExplanationWidget> {
|
||||
@@ -314,9 +386,7 @@ class _PriceExplanationWidgetState extends State<_PriceExplanationWidget> {
|
||||
color: visible ? appTheme.whiteA700 : Colors.transparent,
|
||||
),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: Text(
|
||||
s.price_explanation,
|
||||
),
|
||||
child: Text(s.price_explanation),
|
||||
),
|
||||
),
|
||||
visible: visible,
|
||||
@@ -331,7 +401,11 @@ class _PriceExplanationWidgetState extends State<_PriceExplanationWidget> {
|
||||
visible = !visible;
|
||||
});
|
||||
},
|
||||
child: SvgPicture.asset('assets/icons/question_mark.svg', height: 12, width: 12),
|
||||
child: SvgPicture.asset(
|
||||
'assets/icons/question_mark.svg',
|
||||
height: 12,
|
||||
width: 12,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
import 'package:livekit_client/livekit_client.dart' as lk;
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
typedef VideoCallManagerListener = void Function(VideoCallManagerSnapshot snapshot);
|
||||
typedef VideoCallManagerListener =
|
||||
void Function(VideoCallManagerSnapshot snapshot);
|
||||
|
||||
class VideoCallManager {
|
||||
VideoCallManager({
|
||||
@@ -158,9 +159,7 @@ class VideoCallManager {
|
||||
return lk.CameraCaptureOptions(
|
||||
cameraPosition:
|
||||
cameraPosition ??
|
||||
(_useFrontCamera
|
||||
? lk.CameraPosition.front
|
||||
: lk.CameraPosition.back),
|
||||
(_useFrontCamera ? lk.CameraPosition.front : lk.CameraPosition.back),
|
||||
params: lk.VideoParametersPresets.h1080_169,
|
||||
);
|
||||
}
|
||||
@@ -286,10 +285,11 @@ class VideoCallManager {
|
||||
if (_liveKitDisposed) return false;
|
||||
|
||||
_callStageEntered = true;
|
||||
await _startCallAudioSession();
|
||||
if (_liveKitDisposed || !_callStageEntered) return false;
|
||||
await _publishLocalMedia();
|
||||
if (_liveKitDisposed || !_callStageEntered) return false;
|
||||
await _startForegroundIfPossible();
|
||||
await _startCallAudioSession();
|
||||
return true;
|
||||
} catch (e) {
|
||||
_emitSnapshot(
|
||||
@@ -352,8 +352,7 @@ class VideoCallManager {
|
||||
return;
|
||||
}
|
||||
final room = _room;
|
||||
if (room == null ||
|
||||
room.connectionState != lk.ConnectionState.connected) {
|
||||
if (room == null || room.connectionState != lk.ConnectionState.connected) {
|
||||
return;
|
||||
}
|
||||
if (room.remoteParticipants.isEmpty) return;
|
||||
@@ -469,13 +468,20 @@ class VideoCallManager {
|
||||
if (next) {
|
||||
final granted = await _ensureMicrophonePermission();
|
||||
if (!granted) return;
|
||||
await _startCallAudioSession();
|
||||
}
|
||||
final participant = _room!.localParticipant;
|
||||
if (participant == null) return;
|
||||
try {
|
||||
await participant.setMicrophoneEnabled(
|
||||
next,
|
||||
audioCaptureOptions: _micCaptureOptions,
|
||||
);
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
_emitSnapshot(_snapshot.copyWith(micEnabled: next));
|
||||
_lastMutedMicTotalEnergy = null;
|
||||
await _room!.localParticipant?.setMicrophoneEnabled(
|
||||
next,
|
||||
audioCaptureOptions: _micCaptureOptions,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> toggleVideo(bool currentEnabled) async {
|
||||
@@ -536,9 +542,7 @@ class VideoCallManager {
|
||||
try {
|
||||
await lp.setCameraEnabled(
|
||||
true,
|
||||
cameraCaptureOptions: cameraCaptureOptions(
|
||||
cameraPosition: newPosition,
|
||||
),
|
||||
cameraCaptureOptions: cameraCaptureOptions(cameraPosition: newPosition),
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
@@ -663,6 +667,15 @@ class VideoCallManager {
|
||||
|
||||
Future<void> _onAppResumed() async {
|
||||
if (!isActiveCall) return;
|
||||
await _startCallAudioSession();
|
||||
if (_snapshot.micEnabled) {
|
||||
try {
|
||||
await _room?.localParticipant?.setMicrophoneEnabled(
|
||||
true,
|
||||
audioCaptureOptions: _micCaptureOptions,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
await _resumeVideoAfterForeground();
|
||||
}
|
||||
|
||||
@@ -670,12 +683,10 @@ class VideoCallManager {
|
||||
if (!isActiveCall) return;
|
||||
final ctx = _navigatorContext;
|
||||
final s = ctx != null ? S.of(ctx) : null;
|
||||
final title =
|
||||
resolvedRemoteDisplayName(
|
||||
s?.video_call_counterpart_default ?? 'Call',
|
||||
);
|
||||
final body =
|
||||
s?.video_call_notification_in_progress ?? 'Call in progress';
|
||||
final title = resolvedRemoteDisplayName(
|
||||
s?.video_call_counterpart_default ?? 'Call',
|
||||
);
|
||||
final body = s?.video_call_notification_in_progress ?? 'Call in progress';
|
||||
final endLabel = s?.video_call_notification_end ?? 'End call';
|
||||
await _foreground.startOngoingCall(
|
||||
conversationUuid: _snapshot.conversationUuid,
|
||||
@@ -739,11 +750,7 @@ class VideoCallManager {
|
||||
_roomConnectFuture = null;
|
||||
_clearPreparedConnectionCredentials();
|
||||
_callAcceptNotifiedConversationUuids.clear();
|
||||
_emitSnapshot(
|
||||
_snapshot.copyWith(
|
||||
counterpartPresentInLobby: false,
|
||||
),
|
||||
);
|
||||
_emitSnapshot(_snapshot.copyWith(counterpartPresentInLobby: false));
|
||||
}
|
||||
|
||||
void _clearPreparedConnectionCredentials() {
|
||||
@@ -925,10 +932,7 @@ class VideoCallManager {
|
||||
_preparedWsUrl = tokenResponse.wsUrl;
|
||||
_preparedToken = tokenResponse.token;
|
||||
|
||||
await room.prepareConnection(
|
||||
tokenResponse.wsUrl,
|
||||
tokenResponse.token,
|
||||
);
|
||||
await room.prepareConnection(tokenResponse.wsUrl, tokenResponse.token);
|
||||
if (_liveKitDisposed) return;
|
||||
|
||||
_emitSnapshot(
|
||||
@@ -977,8 +981,7 @@ class VideoCallManager {
|
||||
_syncRemoteParticipantNameFromRoom();
|
||||
|
||||
_syncPoorConnectionOverlay(
|
||||
room.localParticipant?.connectionQuality ??
|
||||
lk.ConnectionQuality.unknown,
|
||||
room.localParticipant?.connectionQuality ?? lk.ConnectionQuality.unknown,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1002,8 +1005,6 @@ class VideoCallManager {
|
||||
status.isPermanentlyDenied || status.isRestricted;
|
||||
|
||||
Future<bool> _ensureCameraPermission() async {
|
||||
final ctx = _navigatorContext;
|
||||
if (ctx == null) return false;
|
||||
var status = await Permission.camera.status;
|
||||
if (status.isGranted) return true;
|
||||
status = await Permission.camera.request();
|
||||
@@ -1023,8 +1024,6 @@ class VideoCallManager {
|
||||
}
|
||||
|
||||
Future<bool> _ensureMicrophonePermission() async {
|
||||
final ctx = _navigatorContext;
|
||||
if (ctx == null) return false;
|
||||
var status = await Permission.microphone.status;
|
||||
if (status.isGranted) return true;
|
||||
status = await Permission.microphone.request();
|
||||
@@ -1136,4 +1135,4 @@ class VideoCallManager {
|
||||
await _audioSession.stop();
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,10 +66,9 @@ class VideoCallSessionCubit extends BaseCubit<VideoCallSessionState> {
|
||||
connectPhase: snapshot.connectPhase,
|
||||
linkIssue: snapshot.linkIssue,
|
||||
callStageEntered: resetCallStage ? false : state.callStageEntered,
|
||||
uiMode:
|
||||
_isCallSessionInactive(snapshot)
|
||||
? VideoCallUiMode.hidden
|
||||
: state.uiMode,
|
||||
uiMode: _isCallSessionInactive(snapshot)
|
||||
? VideoCallUiMode.hidden
|
||||
: state.uiMode,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -364,8 +363,6 @@ class VideoCallSessionCubit extends BaseCubit<VideoCallSessionState> {
|
||||
if (!granted || isClosed) return;
|
||||
|
||||
await _videoCallManager.toggleMic(state.isMicActiveForUi);
|
||||
if (isClosed) return;
|
||||
emit(state.copyWith(micEnabled: true));
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
|
||||
@@ -1,43 +1,65 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:audio_session/audio_session.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
typedef VideoCallAudioSessionFactory = Future<AudioSession> Function();
|
||||
typedef IosCallAudioSessionHandler = Future<void> Function(String method);
|
||||
|
||||
/// Configures and owns the platform audio session for an active VoIP call.
|
||||
class VideoCallAudioSessionCoordinator {
|
||||
VideoCallAudioSessionCoordinator({
|
||||
VideoCallAudioSessionFactory? audioSessionFactory,
|
||||
bool? isIos,
|
||||
IosCallAudioSessionHandler? iosCallAudioSession,
|
||||
}) : _audioSessionFactory =
|
||||
audioSessionFactory ?? (() => AudioSession.instance);
|
||||
audioSessionFactory ?? (() => AudioSession.instance),
|
||||
_isIos = isIos ?? Platform.isIOS,
|
||||
_iosCallAudioSession = iosCallAudioSession ?? _invokeIosCallAudioSession;
|
||||
|
||||
static const MethodChannel _iosChannel = MethodChannel(
|
||||
'com.eatvioapp/call_audio_session',
|
||||
);
|
||||
|
||||
final VideoCallAudioSessionFactory _audioSessionFactory;
|
||||
final bool _isIos;
|
||||
final IosCallAudioSessionHandler _iosCallAudioSession;
|
||||
|
||||
static final AudioSessionConfiguration callConfiguration =
|
||||
AudioSessionConfiguration(
|
||||
avAudioSessionCategory: AVAudioSessionCategory.playAndRecord,
|
||||
avAudioSessionCategoryOptions:
|
||||
AVAudioSessionCategoryOptions.allowBluetooth |
|
||||
avAudioSessionCategory: AVAudioSessionCategory.playAndRecord,
|
||||
avAudioSessionCategoryOptions:
|
||||
AVAudioSessionCategoryOptions.allowBluetooth |
|
||||
AVAudioSessionCategoryOptions.defaultToSpeaker,
|
||||
avAudioSessionMode: AVAudioSessionMode.voiceChat,
|
||||
avAudioSessionRouteSharingPolicy:
|
||||
AVAudioSessionRouteSharingPolicy.defaultPolicy,
|
||||
avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.none,
|
||||
androidAudioAttributes: const AndroidAudioAttributes(
|
||||
contentType: AndroidAudioContentType.speech,
|
||||
flags: AndroidAudioFlags.none,
|
||||
usage: AndroidAudioUsage.voiceCommunication,
|
||||
),
|
||||
androidAudioFocusGainType: AndroidAudioFocusGainType.gain,
|
||||
androidWillPauseWhenDucked: true,
|
||||
);
|
||||
avAudioSessionMode: AVAudioSessionMode.voiceChat,
|
||||
avAudioSessionRouteSharingPolicy:
|
||||
AVAudioSessionRouteSharingPolicy.defaultPolicy,
|
||||
avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.none,
|
||||
androidAudioAttributes: const AndroidAudioAttributes(
|
||||
contentType: AndroidAudioContentType.speech,
|
||||
flags: AndroidAudioFlags.none,
|
||||
usage: AndroidAudioUsage.voiceCommunication,
|
||||
),
|
||||
androidAudioFocusGainType: AndroidAudioFocusGainType.gain,
|
||||
androidWillPauseWhenDucked: true,
|
||||
);
|
||||
|
||||
AudioSession? _session;
|
||||
bool _configured = false;
|
||||
bool _iosActive = false;
|
||||
|
||||
bool get isActive => _session != null;
|
||||
bool get isActive => _isIos ? _iosActive : _session != null;
|
||||
|
||||
static Future<void> _invokeIosCallAudioSession(String method) {
|
||||
return _iosChannel.invokeMethod<void>(method);
|
||||
}
|
||||
|
||||
Future<void> start() async {
|
||||
if (_isIos) {
|
||||
await _iosCallAudioSession('activate');
|
||||
_iosActive = true;
|
||||
return;
|
||||
}
|
||||
final session = await _audioSessionFactory();
|
||||
_session = session;
|
||||
if (!_configured) {
|
||||
@@ -48,6 +70,15 @@ class VideoCallAudioSessionCoordinator {
|
||||
}
|
||||
|
||||
Future<bool> reactivate() async {
|
||||
if (_isIos) {
|
||||
try {
|
||||
await _iosCallAudioSession('activate');
|
||||
_iosActive = true;
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
final session = _session;
|
||||
if (session == null) return false;
|
||||
try {
|
||||
@@ -58,6 +89,13 @@ class VideoCallAudioSessionCoordinator {
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
if (_isIos) {
|
||||
_iosActive = false;
|
||||
try {
|
||||
await _iosCallAudioSession('deactivate');
|
||||
} catch (_) {}
|
||||
return;
|
||||
}
|
||||
final session = _session;
|
||||
_session = null;
|
||||
if (session == null) return;
|
||||
|
||||
@@ -44,82 +44,112 @@ class _WelcomeScreenState extends State<WelcomeScreen> {
|
||||
),
|
||||
Positioned.fill(
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 80),
|
||||
SvgPicture.asset('assets/images/logo_welcome.svg'),
|
||||
const Spacer(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
color: Colors.white,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Добро пожаловать!',
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w800,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'Это первая версия приложения,\nи мы активно работаем над его\nулучшением. Если вы заметите ошибку\nили у вас есть идея — пожалуйста,\nсообщите нам!\n\nВаша помощь бесценна',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.15,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
InkWell(
|
||||
key: AppKeys.welcomeScreenButton,
|
||||
onTap: () async {
|
||||
final sharedPreferences = sl<Prefs>();
|
||||
await sharedPreferences.setBool(PrefsKeys.welcomeScreenWasShowed, true);
|
||||
if (await TokenStorage.haveTokenInStorage()) {
|
||||
goRouter.go('/feed-screen');
|
||||
} else {
|
||||
goRouter.go('/auth-screen');
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Ink(
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.blueA400,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Начать пользоваться',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1,
|
||||
color: Colors.white,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final textScale = MediaQuery.textScalerOf(context).scale(1);
|
||||
final topGap = textScale > 1.2 ? 24.0 : 80.0;
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight - 16,
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(height: topGap),
|
||||
SvgPicture.asset('assets/images/logo_welcome.svg'),
|
||||
const Spacer(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
color: Colors.white,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Добро пожаловать!',
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w800,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'Это первая версия приложения,\nи мы активно работаем над его\nулучшением. Если вы заметите ошибку\nили у вас есть идея — пожалуйста,\nсообщите нам!\n\nВаша помощь бесценна',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.15,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
InkWell(
|
||||
key: AppKeys.welcomeScreenButton,
|
||||
onTap: () async {
|
||||
final sharedPreferences = sl<Prefs>();
|
||||
await sharedPreferences.setBool(
|
||||
PrefsKeys.welcomeScreenWasShowed,
|
||||
true,
|
||||
);
|
||||
if (await TokenStorage.haveTokenInStorage()) {
|
||||
goRouter.go('/feed-screen');
|
||||
} else {
|
||||
goRouter.go('/auth-screen');
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.blueA400,
|
||||
borderRadius: BorderRadius.circular(
|
||||
8,
|
||||
),
|
||||
),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 52,
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 14,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Начать пользоваться',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user