| 
									
										
										
										
											2022-05-02 16:02:49 +08:00
										 |  |  | import 'package:draggable_float_widget/draggable_float_widget.dart'; | 
					
						
							|  |  |  | import 'package:flutter/material.dart'; | 
					
						
							|  |  |  | import 'package:flutter_hbb/common.dart'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import '../models/model.dart'; | 
					
						
							|  |  |  | import '../pages/chat_page.dart'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | OverlayEntry? chatIconOverlayEntry; | 
					
						
							|  |  |  | OverlayEntry? chatWindowOverlayEntry; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | OverlayEntry? mobileActionsOverlayEntry; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class DraggableChatWindow extends StatelessWidget { | 
					
						
							|  |  |  |   DraggableChatWindow( | 
					
						
							|  |  |  |       {this.position = Offset.zero, required this.width, required this.height}); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   final Offset position; | 
					
						
							|  |  |  |   final double width; | 
					
						
							|  |  |  |   final double height; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @override | 
					
						
							|  |  |  |   Widget build(BuildContext context) { | 
					
						
							|  |  |  |     return Draggable( | 
					
						
							|  |  |  |         checkKeyboard: true, | 
					
						
							|  |  |  |         position: position, | 
					
						
							|  |  |  |         width: width, | 
					
						
							|  |  |  |         height: height, | 
					
						
							|  |  |  |         builder: (_, onPanUpdate) { | 
					
						
							|  |  |  |           return isIOS | 
					
						
							|  |  |  |               ? chatPage | 
					
						
							|  |  |  |               : Scaffold( | 
					
						
							|  |  |  |                   resizeToAvoidBottomInset: false, | 
					
						
							|  |  |  |                   appBar: CustomAppBar( | 
					
						
							|  |  |  |                     onPanUpdate: onPanUpdate, | 
					
						
							|  |  |  |                     appBar: Container( | 
					
						
							|  |  |  |                       color: MyTheme.accent50, | 
					
						
							|  |  |  |                       height: 50, | 
					
						
							|  |  |  |                       child: Row( | 
					
						
							|  |  |  |                         mainAxisAlignment: MainAxisAlignment.spaceBetween, | 
					
						
							|  |  |  |                         children: [ | 
					
						
							|  |  |  |                           Padding( | 
					
						
							|  |  |  |                               padding: EdgeInsets.symmetric(horizontal: 15), | 
					
						
							|  |  |  |                               child: Text( | 
					
						
							|  |  |  |                                 translate("Chat"), | 
					
						
							|  |  |  |                                 style: TextStyle( | 
					
						
							|  |  |  |                                     color: Colors.white, | 
					
						
							|  |  |  |                                     fontFamily: 'WorkSans', | 
					
						
							|  |  |  |                                     fontWeight: FontWeight.bold, | 
					
						
							|  |  |  |                                     fontSize: 20), | 
					
						
							|  |  |  |                               )), | 
					
						
							|  |  |  |                           Row( | 
					
						
							|  |  |  |                             crossAxisAlignment: CrossAxisAlignment.center, | 
					
						
							|  |  |  |                             children: [ | 
					
						
							|  |  |  |                               IconButton( | 
					
						
							|  |  |  |                                   onPressed: () { | 
					
						
							|  |  |  |                                     hideChatWindowOverlay(); | 
					
						
							|  |  |  |                                   }, | 
					
						
							|  |  |  |                                   icon: Icon(Icons.keyboard_arrow_down)), | 
					
						
							|  |  |  |                               IconButton( | 
					
						
							|  |  |  |                                   onPressed: () { | 
					
						
							|  |  |  |                                     hideChatWindowOverlay(); | 
					
						
							|  |  |  |                                     hideChatIconOverlay(); | 
					
						
							|  |  |  |                                   }, | 
					
						
							|  |  |  |                                   icon: Icon(Icons.close)) | 
					
						
							|  |  |  |                             ], | 
					
						
							|  |  |  |                           ) | 
					
						
							|  |  |  |                         ], | 
					
						
							|  |  |  |                       ), | 
					
						
							|  |  |  |                     ), | 
					
						
							|  |  |  |                   ), | 
					
						
							|  |  |  |                   body: chatPage, | 
					
						
							|  |  |  |                 ); | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { | 
					
						
							|  |  |  |   final GestureDragUpdateCallback onPanUpdate; | 
					
						
							|  |  |  |   final Widget appBar; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const CustomAppBar( | 
					
						
							|  |  |  |       {Key? key, required this.onPanUpdate, required this.appBar}) | 
					
						
							|  |  |  |       : super(key: key); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @override | 
					
						
							|  |  |  |   Widget build(BuildContext context) { | 
					
						
							|  |  |  |     return GestureDetector(onPanUpdate: onPanUpdate, child: appBar); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @override | 
					
						
							|  |  |  |   Size get preferredSize => new Size.fromHeight(kToolbarHeight); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | showChatIconOverlay({Offset offset = const Offset(200, 50)}) { | 
					
						
							|  |  |  |   if (chatIconOverlayEntry != null) { | 
					
						
							|  |  |  |     chatIconOverlayEntry!.remove(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   if (globalKey.currentState == null || globalKey.currentState!.overlay == null) | 
					
						
							|  |  |  |     return; | 
					
						
							| 
									
										
										
										
											2022-05-16 00:01:27 +08:00
										 |  |  |   final bar = navigationBarKey.currentWidget; | 
					
						
							|  |  |  |   if (bar != null) { | 
					
						
							|  |  |  |     if ((bar as BottomNavigationBar).currentIndex == 1) { | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2022-05-02 16:02:49 +08:00
										 |  |  |   final globalOverlayState = globalKey.currentState!.overlay!; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   final overlay = OverlayEntry(builder: (context) { | 
					
						
							|  |  |  |     return DraggableFloatWidget( | 
					
						
							|  |  |  |         config: DraggableFloatWidgetBaseConfig( | 
					
						
							|  |  |  |           initPositionYInTop: false, | 
					
						
							|  |  |  |           initPositionYMarginBorder: 100, | 
					
						
							|  |  |  |           borderTopContainTopBar: true, | 
					
						
							|  |  |  |         ), | 
					
						
							|  |  |  |         child: FloatingActionButton( | 
					
						
							|  |  |  |             onPressed: () { | 
					
						
							|  |  |  |               if (chatWindowOverlayEntry == null) { | 
					
						
							|  |  |  |                 showChatWindowOverlay(); | 
					
						
							|  |  |  |               } else { | 
					
						
							|  |  |  |                 hideChatWindowOverlay(); | 
					
						
							|  |  |  |               } | 
					
						
							|  |  |  |             }, | 
					
						
							|  |  |  |             child: Icon(Icons.message))); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  |   globalOverlayState.insert(overlay); | 
					
						
							|  |  |  |   chatIconOverlayEntry = overlay; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | hideChatIconOverlay() { | 
					
						
							|  |  |  |   if (chatIconOverlayEntry != null) { | 
					
						
							|  |  |  |     chatIconOverlayEntry!.remove(); | 
					
						
							|  |  |  |     chatIconOverlayEntry = null; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | showChatWindowOverlay() { | 
					
						
							|  |  |  |   if (chatWindowOverlayEntry != null) return; | 
					
						
							|  |  |  |   if (globalKey.currentState == null || globalKey.currentState!.overlay == null) | 
					
						
							|  |  |  |     return; | 
					
						
							|  |  |  |   final globalOverlayState = globalKey.currentState!.overlay!; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   final overlay = OverlayEntry(builder: (context) { | 
					
						
							|  |  |  |     return DraggableChatWindow( | 
					
						
							|  |  |  |         position: Offset(20, 80), width: 250, height: 350); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  |   globalOverlayState.insert(overlay); | 
					
						
							|  |  |  |   chatWindowOverlayEntry = overlay; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | hideChatWindowOverlay() { | 
					
						
							|  |  |  |   if (chatWindowOverlayEntry != null) { | 
					
						
							|  |  |  |     chatWindowOverlayEntry!.remove(); | 
					
						
							|  |  |  |     chatWindowOverlayEntry = null; | 
					
						
							|  |  |  |     return; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | toggleChatOverlay() { | 
					
						
							|  |  |  |   if (chatIconOverlayEntry == null || chatWindowOverlayEntry == null) { | 
					
						
							|  |  |  |     FFI.invokeMethod("enable_soft_keyboard", true); | 
					
						
							|  |  |  |     showChatIconOverlay(); | 
					
						
							|  |  |  |     showChatWindowOverlay(); | 
					
						
							|  |  |  |   } else { | 
					
						
							|  |  |  |     hideChatIconOverlay(); | 
					
						
							|  |  |  |     hideChatWindowOverlay(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /// floating buttons of back/home/recent actions for android
 | 
					
						
							|  |  |  | class DraggableMobileActions extends StatelessWidget { | 
					
						
							|  |  |  |   DraggableMobileActions( | 
					
						
							|  |  |  |       {this.position = Offset.zero, | 
					
						
							|  |  |  |       this.onBackPressed, | 
					
						
							|  |  |  |       this.onRecentPressed, | 
					
						
							|  |  |  |       this.onHomePressed, | 
					
						
							|  |  |  |       required this.width, | 
					
						
							|  |  |  |       required this.height}); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   final Offset position; | 
					
						
							|  |  |  |   final double width; | 
					
						
							|  |  |  |   final double height; | 
					
						
							|  |  |  |   final VoidCallback? onBackPressed; | 
					
						
							|  |  |  |   final VoidCallback? onHomePressed; | 
					
						
							|  |  |  |   final VoidCallback? onRecentPressed; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @override | 
					
						
							|  |  |  |   Widget build(BuildContext context) { | 
					
						
							|  |  |  |     return Draggable( | 
					
						
							|  |  |  |         position: position, | 
					
						
							|  |  |  |         width: width, | 
					
						
							|  |  |  |         height: height, | 
					
						
							|  |  |  |         builder: (_, onPanUpdate) { | 
					
						
							|  |  |  |           return GestureDetector( | 
					
						
							|  |  |  |               onPanUpdate: onPanUpdate, | 
					
						
							|  |  |  |               child: Container( | 
					
						
							|  |  |  |                 decoration: BoxDecoration( | 
					
						
							|  |  |  |                     color: MyTheme.accent.withOpacity(0.4), | 
					
						
							|  |  |  |                     borderRadius: BorderRadius.all(Radius.circular(15))), | 
					
						
							|  |  |  |                 child: Row( | 
					
						
							|  |  |  |                   mainAxisAlignment: MainAxisAlignment.spaceAround, | 
					
						
							|  |  |  |                   children: [ | 
					
						
							|  |  |  |                     IconButton( | 
					
						
							|  |  |  |                         color: MyTheme.white, | 
					
						
							|  |  |  |                         onPressed: onBackPressed, | 
					
						
							|  |  |  |                         icon: Icon(Icons.arrow_back)), | 
					
						
							|  |  |  |                     IconButton( | 
					
						
							|  |  |  |                         color: MyTheme.white, | 
					
						
							|  |  |  |                         onPressed: onHomePressed, | 
					
						
							|  |  |  |                         icon: Icon(Icons.home)), | 
					
						
							|  |  |  |                     IconButton( | 
					
						
							|  |  |  |                         color: MyTheme.white, | 
					
						
							|  |  |  |                         onPressed: onRecentPressed, | 
					
						
							|  |  |  |                         icon: Icon(Icons.more_horiz)), | 
					
						
							|  |  |  |                     VerticalDivider( | 
					
						
							|  |  |  |                       width: 0, | 
					
						
							|  |  |  |                       thickness: 2, | 
					
						
							|  |  |  |                       indent: 10, | 
					
						
							|  |  |  |                       endIndent: 10, | 
					
						
							|  |  |  |                     ), | 
					
						
							|  |  |  |                     IconButton( | 
					
						
							|  |  |  |                         color: MyTheme.white, | 
					
						
							|  |  |  |                         onPressed: hideMobileActionsOverlay, | 
					
						
							|  |  |  |                         icon: Icon(Icons.keyboard_arrow_down)), | 
					
						
							|  |  |  |                   ], | 
					
						
							|  |  |  |                 ), | 
					
						
							|  |  |  |               )); | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-02 17:16:23 +08:00
										 |  |  | resetMobileActionsOverlay() { | 
					
						
							|  |  |  |   if (mobileActionsOverlayEntry == null) return; | 
					
						
							|  |  |  |   hideMobileActionsOverlay(); | 
					
						
							|  |  |  |   showMobileActionsOverlay(); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-05-02 16:02:49 +08:00
										 |  |  | showMobileActionsOverlay() { | 
					
						
							|  |  |  |   if (mobileActionsOverlayEntry != null) return; | 
					
						
							|  |  |  |   if (globalKey.currentContext == null || | 
					
						
							|  |  |  |       globalKey.currentState == null || | 
					
						
							|  |  |  |       globalKey.currentState!.overlay == null) return; | 
					
						
							|  |  |  |   final globalOverlayState = globalKey.currentState!.overlay!; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // compute overlay position
 | 
					
						
							|  |  |  |   final screenW = MediaQuery.of(globalKey.currentContext!).size.width; | 
					
						
							|  |  |  |   final screenH = MediaQuery.of(globalKey.currentContext!).size.height; | 
					
						
							|  |  |  |   final double overlayW = 200; | 
					
						
							|  |  |  |   final double overlayH = 45; | 
					
						
							|  |  |  |   final left = (screenW - overlayW) / 2; | 
					
						
							| 
									
										
										
										
											2022-05-16 00:01:27 +08:00
										 |  |  |   final top = screenH - overlayH - 80; | 
					
						
							| 
									
										
										
										
											2022-05-02 16:02:49 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   final overlay = OverlayEntry(builder: (context) { | 
					
						
							|  |  |  |     return DraggableMobileActions( | 
					
						
							|  |  |  |       position: Offset(left, top), | 
					
						
							|  |  |  |       width: overlayW, | 
					
						
							|  |  |  |       height: overlayH, | 
					
						
							|  |  |  |       onBackPressed: () => FFI.tap(MouseButtons.right), | 
					
						
							|  |  |  |       onHomePressed: () => FFI.tap(MouseButtons.wheel), | 
					
						
							|  |  |  |       onRecentPressed: () async { | 
					
						
							|  |  |  |         FFI.sendMouse('down', MouseButtons.wheel); | 
					
						
							|  |  |  |         await Future.delayed(Duration(milliseconds: 500)); | 
					
						
							|  |  |  |         FFI.sendMouse('up', MouseButtons.wheel); | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  |   globalOverlayState.insert(overlay); | 
					
						
							|  |  |  |   mobileActionsOverlayEntry = overlay; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | hideMobileActionsOverlay() { | 
					
						
							|  |  |  |   if (mobileActionsOverlayEntry != null) { | 
					
						
							|  |  |  |     mobileActionsOverlayEntry!.remove(); | 
					
						
							|  |  |  |     mobileActionsOverlayEntry = null; | 
					
						
							|  |  |  |     return; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class Draggable extends StatefulWidget { | 
					
						
							|  |  |  |   Draggable( | 
					
						
							|  |  |  |       {this.checkKeyboard = false, | 
					
						
							|  |  |  |       this.checkScreenSize = false, | 
					
						
							|  |  |  |       this.position = Offset.zero, | 
					
						
							|  |  |  |       required this.width, | 
					
						
							|  |  |  |       required this.height, | 
					
						
							|  |  |  |       required this.builder}); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   final bool checkKeyboard; | 
					
						
							|  |  |  |   final bool checkScreenSize; | 
					
						
							|  |  |  |   final Offset position; | 
					
						
							|  |  |  |   final double width; | 
					
						
							|  |  |  |   final double height; | 
					
						
							|  |  |  |   final Widget Function(BuildContext, GestureDragUpdateCallback) builder; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @override | 
					
						
							|  |  |  |   State<StatefulWidget> createState() => _DraggableState(); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class _DraggableState extends State<Draggable> { | 
					
						
							|  |  |  |   late Offset _position; | 
					
						
							|  |  |  |   bool _keyboardVisible = false; | 
					
						
							|  |  |  |   double _saveHeight = 0; | 
					
						
							|  |  |  |   double _lastBottomHeight = 0; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @override | 
					
						
							|  |  |  |   void initState() { | 
					
						
							|  |  |  |     super.initState(); | 
					
						
							|  |  |  |     _position = widget.position; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   void onPanUpdate(DragUpdateDetails d) { | 
					
						
							|  |  |  |     final offset = d.delta; | 
					
						
							|  |  |  |     final size = MediaQuery.of(context).size; | 
					
						
							|  |  |  |     double x = 0; | 
					
						
							|  |  |  |     double y = 0; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (_position.dx + offset.dx + widget.width > size.width) { | 
					
						
							|  |  |  |       x = size.width - widget.width; | 
					
						
							|  |  |  |     } else if (_position.dx + offset.dx < 0) { | 
					
						
							|  |  |  |       x = 0; | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       x = _position.dx + offset.dx; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (_position.dy + offset.dy + widget.height > size.height) { | 
					
						
							|  |  |  |       y = size.height - widget.height; | 
					
						
							|  |  |  |     } else if (_position.dy + offset.dy < 0) { | 
					
						
							|  |  |  |       y = 0; | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       y = _position.dy + offset.dy; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     setState(() { | 
					
						
							|  |  |  |       _position = Offset(x, y); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   checkScreenSize() {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   checkKeyboard() { | 
					
						
							|  |  |  |     final bottomHeight = MediaQuery.of(context).viewInsets.bottom; | 
					
						
							|  |  |  |     final currentVisible = bottomHeight != 0; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     debugPrint(bottomHeight.toString() + currentVisible.toString()); | 
					
						
							|  |  |  |     // save
 | 
					
						
							|  |  |  |     if (!_keyboardVisible && currentVisible) { | 
					
						
							|  |  |  |       _saveHeight = _position.dy; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // reset
 | 
					
						
							|  |  |  |     if (_lastBottomHeight > 0 && bottomHeight == 0) { | 
					
						
							|  |  |  |       setState(() { | 
					
						
							|  |  |  |         _position = Offset(_position.dx, _saveHeight); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // onKeyboardVisible
 | 
					
						
							|  |  |  |     if (_keyboardVisible && currentVisible) { | 
					
						
							|  |  |  |       final sumHeight = bottomHeight + widget.height; | 
					
						
							|  |  |  |       final contextHeight = MediaQuery.of(context).size.height; | 
					
						
							|  |  |  |       if (sumHeight + _position.dy > contextHeight) { | 
					
						
							|  |  |  |         final y = contextHeight - sumHeight; | 
					
						
							|  |  |  |         setState(() { | 
					
						
							|  |  |  |           _position = Offset(_position.dx, y); | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     _keyboardVisible = currentVisible; | 
					
						
							|  |  |  |     _lastBottomHeight = bottomHeight; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   @override | 
					
						
							|  |  |  |   Widget build(BuildContext context) { | 
					
						
							|  |  |  |     if (widget.checkKeyboard) { | 
					
						
							|  |  |  |       checkKeyboard(); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     if (widget.checkKeyboard) { | 
					
						
							|  |  |  |       checkScreenSize(); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return Positioned( | 
					
						
							|  |  |  |         top: _position.dy, | 
					
						
							|  |  |  |         left: _position.dx, | 
					
						
							|  |  |  |         width: widget.width, | 
					
						
							|  |  |  |         height: widget.height, | 
					
						
							|  |  |  |         child: widget.builder(context, onPanUpdate)); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } |