Compare commits
	
		
			4 Commits
		
	
	
		
			2.1.1+39
			...
			619c90cdd9
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 619c90cdd9 | |||
| 168d51c9fe | |||
| d4b831f98e | |||
| 4d96a15c31 | 
							
								
								
									
										30
									
								
								api/Passport/Developer Notify All Users.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								api/Passport/Developer Notify All Users.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| meta { | ||||
|   name: Developer Notify All Users | ||||
|   type: http | ||||
|   seq: 1 | ||||
| } | ||||
|  | ||||
| post { | ||||
|   url: {{endpoint}}/cgi/id/dev/notify/all | ||||
|   body: json | ||||
|   auth: bearer | ||||
| } | ||||
|  | ||||
| auth:bearer { | ||||
|   token: {{atk}} | ||||
| } | ||||
|  | ||||
| body:json { | ||||
|   { | ||||
|     "client_id": "{{third_client_id}}", | ||||
|     "client_secret":"{{third_client_tk}}", | ||||
|     "type": "general", | ||||
|     "subject": "Merry Christmas!", | ||||
|     "subtitle": "一条来自 Solar Network 团队的信息", | ||||
|     "content": "今天是 12 月 25 日 (UTC+8),小羊祝您圣诞快乐 🎄", | ||||
|     "metadata": { | ||||
|       "image": "6EqsYQwmFRCkbmhR" | ||||
|     }, | ||||
|     "priority": 10 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										9
									
								
								api/bruno.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								api/bruno.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| { | ||||
|   "version": "1", | ||||
|   "name": "Solar Network", | ||||
|   "type": "collection", | ||||
|   "ignore": [ | ||||
|     "node_modules", | ||||
|     ".git" | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										8
									
								
								api/environments/Prod.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								api/environments/Prod.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| vars { | ||||
|   endpoint: https://api.sn.solsynth.dev | ||||
|   third_client_id: alphabot | ||||
| } | ||||
| vars:secret [ | ||||
|   atk, | ||||
|   third_client_tk | ||||
| ] | ||||
| @@ -281,16 +281,22 @@ | ||||
|     "one": "{} attachment", | ||||
|     "other": "{} attachments" | ||||
|   }, | ||||
|   "fieldAttachmentRandomId": "Random ID", | ||||
|   "addAttachmentFromAlbum": "Add from album", | ||||
|   "addAttachmentFromClipboard": "Paste file", | ||||
|   "addAttachmentFromCameraPhoto": "Take photo", | ||||
|   "addAttachmentFromCameraVideo": "Take video", | ||||
|   "addAttachmentFromRandomId": "Link via RID", | ||||
|   "attachmentPastedImage": "Pasted Image", | ||||
|   "attachmentInsertLink": "Insert Link", | ||||
|   "attachmentSetAsPostThumbnail": "Set as post thumbnail", | ||||
|   "attachmentUnsetAsPostThumbnail": "Unset as post thumbnail", | ||||
|   "attachmentSetThumbnail": "Set thumbnail", | ||||
|   "attachmentCopyRandomId": "Copy RID", | ||||
|   "attachmentUpload": "Upload", | ||||
|   "attachmentInputDialog": "Upload attachments", | ||||
|   "attachmentInputUseRandomId": "Use Random ID", | ||||
|   "attachmentInputNew": "New Upload", | ||||
|   "notification": "Notification", | ||||
|   "notificationUnreadCount": { | ||||
|     "zero": "All notifications read", | ||||
| @@ -506,5 +512,6 @@ | ||||
|   "postCategoryKnowledge": "Knowledge", | ||||
|   "postCategoryLiterature": "Literature", | ||||
|   "postCategoryFunny": "Funny", | ||||
|   "postCategoryUncategorized": "Uncategorized" | ||||
|   "postCategoryUncategorized": "Uncategorized", | ||||
|   "waitingForUpload": "Waiting for upload" | ||||
| } | ||||
|   | ||||
| @@ -279,16 +279,22 @@ | ||||
|     "one": "{} 个附件", | ||||
|     "other": "{} 个附件" | ||||
|   }, | ||||
|   "fieldAttachmentRandomId": "访问 ID", | ||||
|   "addAttachmentFromAlbum": "从相册中添加附件", | ||||
|   "addAttachmentFromClipboard": "粘贴附件", | ||||
|   "addAttachmentFromCameraPhoto": "拍摄照片", | ||||
|   "addAttachmentFromCameraVideo": "拍摄视频", | ||||
|   "addAttachmentFromRandomId": "通过访问 ID 链接", | ||||
|   "attachmentPastedImage": "粘贴的图片", | ||||
|   "attachmentInsertLink": "插入连接", | ||||
|   "attachmentSetAsPostThumbnail": "设置为帖子缩略图", | ||||
|   "attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图", | ||||
|   "attachmentSetThumbnail": "设置缩略图", | ||||
|   "attachmentCopyRandomId": "复制访问 ID", | ||||
|   "attachmentUpload": "上传", | ||||
|   "attachmentInputDialog": "上传附件", | ||||
|   "attachmentInputUseRandomId": "使用访问 ID", | ||||
|   "attachmentInputNew": "新上传附件", | ||||
|   "notification": "通知", | ||||
|   "notificationUnreadCount": { | ||||
|     "zero": "无未读通知", | ||||
| @@ -504,5 +510,6 @@ | ||||
|   "postCategoryKnowledge": "知识", | ||||
|   "postCategoryLiterature": "文学", | ||||
|   "postCategoryFunny": "搞笑", | ||||
|   "postCategoryUncategorized": "未分类" | ||||
|   "postCategoryUncategorized": "未分类", | ||||
|   "waitingForUpload": "等待上传" | ||||
| } | ||||
|   | ||||
| @@ -173,7 +173,7 @@ PODS: | ||||
|   - in_app_review (2.0.0): | ||||
|     - Flutter | ||||
|   - Kingfisher (8.1.3) | ||||
|   - livekit_client (2.3.2): | ||||
|   - livekit_client (2.3.3): | ||||
|     - Flutter | ||||
|     - flutter_webrtc | ||||
|     - WebRTC-SDK (= 125.6422.06) | ||||
| @@ -386,7 +386,7 @@ SPEC CHECKSUMS: | ||||
|   image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 | ||||
|   in_app_review: a31b5257259646ea78e0e35fc914979b0031d011 | ||||
|   Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef | ||||
|   livekit_client: 6108dad8b77db3142bafd4c630f471d0a54335cd | ||||
|   livekit_client: 02cf2cc4357a655af12ccee70ff5596ae4e6feef | ||||
|   media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 | ||||
|   media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a | ||||
|   media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e | ||||
|   | ||||
| @@ -215,4 +215,18 @@ class SnAttachmentProvider { | ||||
|  | ||||
|     return place; | ||||
|   } | ||||
|  | ||||
|   Future<SnAttachment> updateOne( | ||||
|     int id, | ||||
|     String alt, { | ||||
|     required Map<String, dynamic> metadata, | ||||
|     bool isMature = false, | ||||
|   }) async { | ||||
|     final resp = await _sn.client.put('/cgi/uc/attachments/$id', data: { | ||||
|       'alt': alt, | ||||
|       'metadata': metadata, | ||||
|       'is_mature': isMature, | ||||
|     }); | ||||
|     return SnAttachment.fromJson(resp.data); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -31,9 +31,10 @@ class UserProvider extends ChangeNotifier { | ||||
|     final value = _config.prefs.getString(kAtkStoreKey); | ||||
|     isAuthorized = value != null; | ||||
|     notifyListeners(); | ||||
|     refreshUser().then((value) { | ||||
|     refreshUser().then((value) async { | ||||
|       if (value != null) { | ||||
|         log('Logged in as @${value.name}'); | ||||
|         log('Atk: ${await atk}'); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|   | ||||
| @@ -96,38 +96,6 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   final _imagePicker = ImagePicker(); | ||||
|  | ||||
|   void _takeMedia(bool isVideo) async { | ||||
|     final result = isVideo | ||||
|         ? await _imagePicker.pickVideo(source: ImageSource.camera) | ||||
|         : await _imagePicker.pickImage(source: ImageSource.camera); | ||||
|     if (result == null) return; | ||||
|     _writeController.addAttachments([ | ||||
|       PostWriteMedia.fromFile(result), | ||||
|     ]); | ||||
|   } | ||||
|  | ||||
|   void _selectMedia() async { | ||||
|     final result = await _imagePicker.pickMultipleMedia(); | ||||
|     if (result.isEmpty) return; | ||||
|     _writeController.addAttachments( | ||||
|       result.map((e) => PostWriteMedia.fromFile(e)), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   void _pasteMedia() async { | ||||
|     final imageBytes = await Pasteboard.image; | ||||
|     if (imageBytes == null) return; | ||||
|     _writeController.addAttachments([ | ||||
|       PostWriteMedia.fromBytes( | ||||
|         imageBytes, | ||||
|         'attachmentPastedImage'.tr(), | ||||
|         PostWriteMediaType.image, | ||||
|       ), | ||||
|     ]); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     _writeController.dispose(); | ||||
| @@ -435,63 +403,12 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|                               scrollDirection: Axis.vertical, | ||||
|                               child: Row( | ||||
|                                 children: [ | ||||
|                                   PopupMenuButton( | ||||
|                                     icon: Icon( | ||||
|                                       Symbols.add_photo_alternate, | ||||
|                                       color: Theme.of(context).colorScheme.primary, | ||||
|                                     ), | ||||
|                                     itemBuilder: (context) => [ | ||||
|                                       if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows) | ||||
|                                         PopupMenuItem( | ||||
|                                           child: Row( | ||||
|                                             children: [ | ||||
|                                               const Icon(Symbols.photo_camera), | ||||
|                                               const Gap(16), | ||||
|                                               Text('addAttachmentFromCameraPhoto').tr(), | ||||
|                                             ], | ||||
|                                           ), | ||||
|                                           onTap: () { | ||||
|                                             _takeMedia(false); | ||||
|                                           }, | ||||
|                                         ), | ||||
|                                       if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows) | ||||
|                                         PopupMenuItem( | ||||
|                                           child: Row( | ||||
|                                             children: [ | ||||
|                                               const Icon(Symbols.videocam), | ||||
|                                               const Gap(16), | ||||
|                                               Text('addAttachmentFromCameraVideo').tr(), | ||||
|                                             ], | ||||
|                                           ), | ||||
|                                           onTap: () { | ||||
|                                             _takeMedia(true); | ||||
|                                           }, | ||||
|                                         ), | ||||
|                                       PopupMenuItem( | ||||
|                                         child: Row( | ||||
|                                           children: [ | ||||
|                                             const Icon(Symbols.photo_library), | ||||
|                                             const Gap(16), | ||||
|                                             Text('addAttachmentFromAlbum').tr(), | ||||
|                                           ], | ||||
|                                         ), | ||||
|                                         onTap: () { | ||||
|                                           _selectMedia(); | ||||
|                                         }, | ||||
|                                       ), | ||||
|                                       PopupMenuItem( | ||||
|                                         child: Row( | ||||
|                                           children: [ | ||||
|                                             const Icon(Symbols.content_paste), | ||||
|                                             const Gap(16), | ||||
|                                             Text('addAttachmentFromClipboard').tr(), | ||||
|                                           ], | ||||
|                                         ), | ||||
|                                         onTap: () { | ||||
|                                           _pasteMedia(); | ||||
|                                         }, | ||||
|                                       ), | ||||
|                                     ], | ||||
|                                   AddPostMediaButton( | ||||
|                                     onAdd: (items) { | ||||
|                                       setState(() { | ||||
|                                         _writeController.addAttachments(items); | ||||
|                                       }); | ||||
|                                     }, | ||||
|                                   ), | ||||
|                                 ], | ||||
|                               ), | ||||
|   | ||||
							
								
								
									
										114
									
								
								lib/widgets/attachment/attachment_input.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								lib/widgets/attachment/attachment_input.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:image_picker/image_picker.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/sn_attachment.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
|  | ||||
| class AttachmentInputDialog extends StatefulWidget { | ||||
|   final String? title; | ||||
|  | ||||
|   const AttachmentInputDialog({super.key, required this.title}); | ||||
|  | ||||
|   @override | ||||
|   State<AttachmentInputDialog> createState() => _AttachmentInputDialogState(); | ||||
| } | ||||
|  | ||||
| class _AttachmentInputDialogState extends State<AttachmentInputDialog> { | ||||
|   final _randomIdController = TextEditingController(); | ||||
|  | ||||
|   XFile? _thumbnailFile; | ||||
|  | ||||
|   void _pickImage() async { | ||||
|     final picker = ImagePicker(); | ||||
|     final result = await picker.pickImage(source: ImageSource.gallery); | ||||
|     if (result == null) return; | ||||
|     setState(() => _thumbnailFile = result); | ||||
|   } | ||||
|  | ||||
|   bool _isBusy = false; | ||||
|  | ||||
|   void _finishUp() async { | ||||
|     if (_isBusy) return; | ||||
|     setState(() => _isBusy = true); | ||||
|  | ||||
|     final attach = context.read<SnAttachmentProvider>(); | ||||
|  | ||||
|     if (_randomIdController.text.isNotEmpty) { | ||||
|       try { | ||||
|         final attachment = await attach.getOne(_randomIdController.text); | ||||
|         if (!mounted) return; | ||||
|         Navigator.pop(context, attachment); | ||||
|       } catch (err) { | ||||
|         if (!mounted) return; | ||||
|         context.showErrorDialog(err); | ||||
|       } | ||||
|     } else if (_thumbnailFile != null) { | ||||
|       try { | ||||
|         final attachment = await attach.directUploadOne( | ||||
|           (await _thumbnailFile!.readAsBytes()).buffer.asUint8List(), | ||||
|           _thumbnailFile!.path, | ||||
|           'interactive', | ||||
|           null, | ||||
|         ); | ||||
|         if (!mounted) return; | ||||
|         Navigator.pop(context, attachment); | ||||
|       } catch (err) { | ||||
|         if (!mounted) return; | ||||
|         context.showErrorDialog(err); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return AlertDialog( | ||||
|       title: Text(widget.title ?? 'attachmentInputDialog').tr(), | ||||
|       content: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         children: [ | ||||
|           Text('attachmentInputUseRandomId').tr().fontSize(14), | ||||
|           const Gap(8), | ||||
|           TextField( | ||||
|             controller: _randomIdController, | ||||
|             decoration: InputDecoration( | ||||
|               labelText: 'fieldAttachmentRandomId'.tr(), | ||||
|               border: const OutlineInputBorder(), | ||||
|             ), | ||||
|             onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|           ), | ||||
|           const Gap(24), | ||||
|           Text('attachmentInputNew').tr().fontSize(14), | ||||
|           Card( | ||||
|             child: ListTile( | ||||
|               contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), | ||||
|               leading: const Icon(Symbols.add_photo_alternate), | ||||
|               trailing: const Icon(Symbols.chevron_right), | ||||
|               title: Text('addAttachmentFromAlbum').tr(), | ||||
|               subtitle: _thumbnailFile == null ? Text('unset').tr() : Text('waitingForUpload').tr(), | ||||
|               onTap: () { | ||||
|                 _pickImage(); | ||||
|               }, | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|       actions: [ | ||||
|         TextButton( | ||||
|           child: Text('dialogDismiss').tr(), | ||||
|           onPressed: _isBusy ? null : () { | ||||
|             Navigator.pop(context); | ||||
|           }, | ||||
|         ), | ||||
|         TextButton( | ||||
|           onPressed: _isBusy ? null : () => _finishUp(), | ||||
|           child: Text('dialogConfirm').tr(), | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -10,6 +10,7 @@ import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/types/chat.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/attachment/attachment_list.dart'; | ||||
| import 'package:surface/widgets/context_menu.dart'; | ||||
| import 'package:surface/widgets/link_preview.dart'; | ||||
| import 'package:surface/widgets/markdown_content.dart'; | ||||
| import 'package:swipe_to/swipe_to.dart'; | ||||
| @@ -53,7 +54,7 @@ class ChatMessage extends StatelessWidget { | ||||
|       swipeSensitivity: 20, | ||||
|       onLeftSwipe: onReply != null ? (_) => onReply!(data) : null, | ||||
|       onRightSwipe: onEdit != null ? (_) => onEdit!(data) : null, | ||||
|       child: ContextMenuRegion( | ||||
|       child: ContextMenuArea( | ||||
|         contextMenu: ContextMenu( | ||||
|           entries: [ | ||||
|             MenuHeader(text: "eventResourceTag".tr(args: ['#${data.id}'])), | ||||
|   | ||||
| @@ -123,40 +123,6 @@ class ChatMessageInputState extends State<ChatMessageInput> { | ||||
|   } | ||||
|  | ||||
|   final List<PostWriteMedia> _attachments = List.empty(growable: true); | ||||
|   final _imagePicker = ImagePicker(); | ||||
|  | ||||
|   void _takeMedia(bool isVideo) async { | ||||
|     final result = isVideo | ||||
|         ? await _imagePicker.pickVideo(source: ImageSource.camera) | ||||
|         : await _imagePicker.pickImage(source: ImageSource.camera); | ||||
|     if (result == null) return; | ||||
|     _attachments.add( | ||||
|       PostWriteMedia.fromFile(result), | ||||
|     ); | ||||
|     setState(() {}); | ||||
|   } | ||||
|  | ||||
|   void _selectMedia() async { | ||||
|     final result = await _imagePicker.pickMultipleMedia(); | ||||
|     if (result.isEmpty) return; | ||||
|     _attachments.addAll( | ||||
|       result.map((e) => PostWriteMedia.fromFile(e)), | ||||
|     ); | ||||
|     setState(() {}); | ||||
|   } | ||||
|  | ||||
|   void _pasteMedia() async { | ||||
|     final imageBytes = await Pasteboard.image; | ||||
|     if (imageBytes == null) return; | ||||
|     _attachments.add( | ||||
|       PostWriteMedia.fromBytes( | ||||
|         imageBytes, | ||||
|         'attachmentPastedImage'.tr(), | ||||
|         PostWriteMediaType.image, | ||||
|       ), | ||||
|     ); | ||||
|     setState(() {}); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
| @@ -294,63 +260,12 @@ class ChatMessageInputState extends State<ChatMessageInput> { | ||||
|                 ), | ||||
|               ), | ||||
|               const Gap(8), | ||||
|               PopupMenuButton( | ||||
|                 icon: Icon( | ||||
|                   Symbols.add_photo_alternate, | ||||
|                   color: Theme.of(context).colorScheme.primary, | ||||
|                 ), | ||||
|                 itemBuilder: (context) => [ | ||||
|                   if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows) | ||||
|                     PopupMenuItem( | ||||
|                       child: Row( | ||||
|                         children: [ | ||||
|                           const Icon(Symbols.photo_camera), | ||||
|                           const Gap(16), | ||||
|                           Text('addAttachmentFromCameraPhoto').tr(), | ||||
|                         ], | ||||
|                       ), | ||||
|                       onTap: () { | ||||
|                         _takeMedia(false); | ||||
|                       }, | ||||
|                     ), | ||||
|                   if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows) | ||||
|                     PopupMenuItem( | ||||
|                       child: Row( | ||||
|                         children: [ | ||||
|                           const Icon(Symbols.videocam), | ||||
|                           const Gap(16), | ||||
|                           Text('addAttachmentFromCameraVideo').tr(), | ||||
|                         ], | ||||
|                       ), | ||||
|                       onTap: () { | ||||
|                         _takeMedia(true); | ||||
|                       }, | ||||
|                     ), | ||||
|                   PopupMenuItem( | ||||
|                     child: Row( | ||||
|                       children: [ | ||||
|                         const Icon(Symbols.photo_library), | ||||
|                         const Gap(16), | ||||
|                         Text('addAttachmentFromAlbum').tr(), | ||||
|                       ], | ||||
|                     ), | ||||
|                     onTap: () { | ||||
|                       _selectMedia(); | ||||
|                     }, | ||||
|                   ), | ||||
|                   PopupMenuItem( | ||||
|                     child: Row( | ||||
|                       children: [ | ||||
|                         const Icon(Symbols.content_paste), | ||||
|                         const Gap(16), | ||||
|                         Text('addAttachmentFromClipboard').tr(), | ||||
|                       ], | ||||
|                     ), | ||||
|                     onTap: () { | ||||
|                       _pasteMedia(); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ], | ||||
|               AddPostMediaButton( | ||||
|                 onAdd: (items) { | ||||
|                   setState(() { | ||||
|                     _attachments.addAll(items); | ||||
|                   }); | ||||
|                 }, | ||||
|               ), | ||||
|               IconButton( | ||||
|                 onPressed: _isBusy ? null : _sendMessage, | ||||
|   | ||||
							
								
								
									
										47
									
								
								lib/widgets/context_menu.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								lib/widgets/context_menu.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_animate/flutter_animate.dart'; | ||||
| import 'package:flutter_context_menu/flutter_context_menu.dart'; | ||||
| import 'package:responsive_framework/responsive_framework.dart'; | ||||
|  | ||||
| class ContextMenuArea extends StatelessWidget { | ||||
|   final ContextMenu contextMenu; | ||||
|   final Widget child; | ||||
|   final ValueChanged<dynamic>? onItemSelected; | ||||
|  | ||||
|   const ContextMenuArea({ | ||||
|     super.key, | ||||
|     required this.contextMenu, | ||||
|     required this.child, | ||||
|     this.onItemSelected, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     Offset mousePosition = Offset.zero; | ||||
|  | ||||
|     return Listener( | ||||
|       onPointerDown: (event) { | ||||
|         mousePosition = event.position; | ||||
|         final isCollapseDrawer = ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE); | ||||
|         if (!isCollapseDrawer) { | ||||
|           final isExpandDrawer = ResponsiveBreakpoints.of(context).largerThan(TABLET); | ||||
|           // Leave padding for side navigation | ||||
|           mousePosition = isExpandDrawer | ||||
|               ? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2) | ||||
|               : mousePosition.copyWith(dx: mousePosition.dx - 72 * 2); | ||||
|         } | ||||
|       }, | ||||
|       child: GestureDetector( | ||||
|         onLongPress: () => _showMenu(context, mousePosition), | ||||
|         onSecondaryTap: () => _showMenu(context, mousePosition), | ||||
|         child: child, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   void _showMenu(BuildContext context, Offset mousePosition) async { | ||||
|     final menu = contextMenu.copyWith(position: contextMenu.position ?? mousePosition); | ||||
|     final value = await showContextMenu(context, contextMenu: menu); | ||||
|     onItemSelected?.call(value); | ||||
|   } | ||||
| } | ||||
| @@ -6,15 +6,23 @@ import 'package:dismissible_page/dismissible_page.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:flutter_context_menu/flutter_context_menu.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:image_picker/image_picker.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:pasteboard/pasteboard.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/controllers/post_write_controller.dart'; | ||||
| import 'package:surface/providers/sn_attachment.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/types/attachment.dart'; | ||||
| import 'package:surface/widgets/attachment/attachment_input.dart'; | ||||
| import 'package:surface/widgets/attachment/attachment_zoom.dart'; | ||||
| import 'package:surface/widgets/context_menu.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/universal_image.dart'; | ||||
|  | ||||
| class PostMediaPendingList extends StatelessWidget { | ||||
|   final PostWriteMedia? thumbnail; | ||||
| @@ -70,6 +78,32 @@ class PostMediaPendingList extends StatelessWidget { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> _setThumbnail(BuildContext context, int idx) async { | ||||
|     if (idx == -1) { | ||||
|       // Thumbnail only can set on video or audio. And thumbnail of the post must be an image, so it's not possible to set thumbnail on the post thumbnail. | ||||
|       return; | ||||
|     } else if (attachments[idx].attachment == null) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     final thumbnail = await showDialog<SnAttachment?>( | ||||
|       context: context, | ||||
|       builder: (context) => AttachmentInputDialog( | ||||
|         title: 'attachmentSetThumbnail'.tr(), | ||||
|       ), | ||||
|     ); | ||||
|     if (thumbnail == null) return; | ||||
|     if (!context.mounted) return; | ||||
|  | ||||
|     final attach = context.read<SnAttachmentProvider>(); | ||||
|     final newAttach = await attach.updateOne(attachments[idx].attachment!.id, thumbnail.alt, metadata: { | ||||
|       ...attachments[idx].attachment!.metadata, | ||||
|       'thumbnail': thumbnail.rid, | ||||
|     }); | ||||
|  | ||||
|     onUpdate!(idx, PostWriteMedia(newAttach)); | ||||
|   } | ||||
|  | ||||
|   Future<void> _deleteAttachment(BuildContext context, int idx) async { | ||||
|     final media = idx == -1 ? thumbnail! : attachments[idx]; | ||||
|     if (media.attachment == null) return; | ||||
| @@ -87,9 +121,17 @@ class PostMediaPendingList extends StatelessWidget { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   ContextMenu _buildContextMenu(BuildContext context, int idx, PostWriteMedia media) { | ||||
|   ContextMenu _createContextMenu(BuildContext context, int idx, PostWriteMedia media) { | ||||
|     return ContextMenu( | ||||
|       entries: [ | ||||
|         if (media.attachment != null && media.type == PostWriteMediaType.video) | ||||
|           MenuItem( | ||||
|             label: 'attachmentSetThumbnail'.tr(), | ||||
|             icon: Symbols.image, | ||||
|             onSelected: () { | ||||
|               _setThumbnail(context, idx); | ||||
|             }, | ||||
|           ), | ||||
|         if (media.attachment == null && onUpload != null) | ||||
|           MenuItem( | ||||
|               label: 'attachmentUpload'.tr(), | ||||
| @@ -97,7 +139,10 @@ class PostMediaPendingList extends StatelessWidget { | ||||
|               onSelected: () { | ||||
|                 onUpload!(idx); | ||||
|               }), | ||||
|         if (media.attachment != null && onPostSetThumbnail != null && idx != -1) | ||||
|         if (media.attachment != null && | ||||
|             media.type == PostWriteMediaType.image && | ||||
|             onPostSetThumbnail != null && | ||||
|             idx != -1) | ||||
|           MenuItem( | ||||
|             label: 'attachmentSetAsPostThumbnail'.tr(), | ||||
|             icon: Symbols.gallery_thumbnail, | ||||
| @@ -105,7 +150,7 @@ class PostMediaPendingList extends StatelessWidget { | ||||
|               onPostSetThumbnail!(idx); | ||||
|             }, | ||||
|           ) | ||||
|         else if (media.attachment != null && onPostSetThumbnail != null) | ||||
|         else if (media.attachment != null && media.type == PostWriteMediaType.image && onPostSetThumbnail != null) | ||||
|           MenuItem( | ||||
|             label: 'attachmentUnsetAsPostThumbnail'.tr(), | ||||
|             icon: Symbols.cancel, | ||||
| @@ -138,6 +183,14 @@ class PostMediaPendingList extends StatelessWidget { | ||||
|             icon: Symbols.crop, | ||||
|             onSelected: () => _cropImage(context, idx), | ||||
|           ), | ||||
|         if (media.attachment != null) | ||||
|           MenuItem( | ||||
|             label: 'attachmentCopyRandomId'.tr(), | ||||
|             icon: Symbols.content_copy, | ||||
|             onSelected: () { | ||||
|               Clipboard.setData(ClipboardData(text: media.attachment!.rid)); | ||||
|             }, | ||||
|           ), | ||||
|         if (media.attachment != null && onRemove != null) | ||||
|           MenuItem( | ||||
|             label: 'delete'.tr(), | ||||
| @@ -168,48 +221,17 @@ class PostMediaPendingList extends StatelessWidget { | ||||
|   Widget build(BuildContext context) { | ||||
|     final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; | ||||
|  | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|  | ||||
|     return Container( | ||||
|       constraints: const BoxConstraints(maxHeight: 120), | ||||
|       child: Row( | ||||
|         children: [ | ||||
|           const Gap(8), | ||||
|           if (thumbnail != null) | ||||
|             ContextMenuRegion( | ||||
|               contextMenu: _buildContextMenu(context, -1, thumbnail!), | ||||
|               child: Container( | ||||
|                 decoration: BoxDecoration( | ||||
|                   border: Border.all( | ||||
|                     color: Theme.of(context).dividerColor, | ||||
|                     width: 1, | ||||
|                   ), | ||||
|                   borderRadius: BorderRadius.circular(8), | ||||
|                 ), | ||||
|                 child: ClipRRect( | ||||
|                   borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                   child: AspectRatio( | ||||
|                     aspectRatio: 1, | ||||
|                     child: switch (thumbnail!.type) { | ||||
|                       PostWriteMediaType.image => Container( | ||||
|                         color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                         child: LayoutBuilder(builder: (context, constraints) { | ||||
|                             return Image( | ||||
|                               image: thumbnail!.getImageProvider( | ||||
|                                 context, | ||||
|                                 width: (constraints.maxWidth * devicePixelRatio).round(), | ||||
|                                 height: (constraints.maxHeight * devicePixelRatio).round(), | ||||
|                               )!, | ||||
|                               fit: BoxFit.contain, | ||||
|                             ); | ||||
|                           }), | ||||
|                       ), | ||||
|                       _ => Container( | ||||
|                           color: Theme.of(context).colorScheme.surface, | ||||
|                           child: const Icon(Symbols.docs).center(), | ||||
|                         ), | ||||
|                     }, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ContextMenuArea( | ||||
|               contextMenu: _createContextMenu(context, -1, thumbnail!), | ||||
|               child: _PostMediaPendingItem(media: thumbnail!), | ||||
|             ), | ||||
|           if (thumbnail != null) | ||||
|             const VerticalDivider(width: 1, thickness: 1).padding( | ||||
| @@ -224,42 +246,9 @@ class PostMediaPendingList extends StatelessWidget { | ||||
|               itemCount: attachments.length, | ||||
|               itemBuilder: (context, idx) { | ||||
|                 final media = attachments[idx]; | ||||
|                 return ContextMenuRegion( | ||||
|                   contextMenu: _buildContextMenu(context, idx, media), | ||||
|                   child: Container( | ||||
|                     decoration: BoxDecoration( | ||||
|                       border: Border.all( | ||||
|                         color: Theme.of(context).dividerColor, | ||||
|                         width: 1, | ||||
|                       ), | ||||
|                       borderRadius: BorderRadius.circular(8), | ||||
|                     ), | ||||
|                     child: ClipRRect( | ||||
|                       borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                       child: AspectRatio( | ||||
|                         aspectRatio: 1, | ||||
|                         child: switch (media.type) { | ||||
|                           PostWriteMediaType.image => Container( | ||||
|                             color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                             child: LayoutBuilder(builder: (context, constraints) { | ||||
|                                 return Image( | ||||
|                                   image: media.getImageProvider( | ||||
|                                     context, | ||||
|                                     width: (constraints.maxWidth * devicePixelRatio).round(), | ||||
|                                     height: (constraints.maxHeight * devicePixelRatio).round(), | ||||
|                                   )!, | ||||
|                                   fit: BoxFit.contain, | ||||
|                                 ); | ||||
|                               }), | ||||
|                           ), | ||||
|                           _ => Container( | ||||
|                               color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                               child: const Icon(Symbols.docs).center(), | ||||
|                             ), | ||||
|                         }, | ||||
|                       ), | ||||
|                     ), | ||||
|                   ), | ||||
|                 return ContextMenuArea( | ||||
|                   contextMenu: _createContextMenu(context, idx, media), | ||||
|                   child: _PostMediaPendingItem(media: media), | ||||
|                 ); | ||||
|               }, | ||||
|             ), | ||||
| @@ -269,3 +258,218 @@ class PostMediaPendingList extends StatelessWidget { | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _PostMediaPendingItem extends StatelessWidget { | ||||
|   final PostWriteMedia media; | ||||
|  | ||||
|   const _PostMediaPendingItem({ | ||||
|     required this.media, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; | ||||
|  | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|  | ||||
|     return Container( | ||||
|       decoration: BoxDecoration( | ||||
|         border: Border.all( | ||||
|           color: Theme.of(context).dividerColor, | ||||
|           width: 1, | ||||
|         ), | ||||
|         borderRadius: BorderRadius.circular(8), | ||||
|       ), | ||||
|       child: ClipRRect( | ||||
|         borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|         child: AspectRatio( | ||||
|           aspectRatio: 1, | ||||
|           child: switch (media.type) { | ||||
|             PostWriteMediaType.image => Container( | ||||
|                 color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                 child: LayoutBuilder(builder: (context, constraints) { | ||||
|                   return Image( | ||||
|                     image: media.getImageProvider( | ||||
|                       context, | ||||
|                       width: (constraints.maxWidth * devicePixelRatio).round(), | ||||
|                       height: (constraints.maxHeight * devicePixelRatio).round(), | ||||
|                     )!, | ||||
|                     fit: BoxFit.contain, | ||||
|                   ); | ||||
|                 }), | ||||
|               ), | ||||
|             PostWriteMediaType.video => Container( | ||||
|                 color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                 child: media.attachment?.metadata['thumbnail'] != null | ||||
|                     ? AutoResizeUniversalImage(sn.getAttachmentUrl(media.attachment?.metadata['thumbnail'])) | ||||
|                     : const Icon(Symbols.videocam).center(), | ||||
|               ), | ||||
|             _ => Container( | ||||
|                 color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                 child: const Icon(Symbols.docs).center(), | ||||
|               ), | ||||
|           }, | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class AddPostMediaButton extends StatelessWidget { | ||||
|   final Function(Iterable<PostWriteMedia>) onAdd; | ||||
|  | ||||
|   const AddPostMediaButton({super.key, required this.onAdd}); | ||||
|  | ||||
|   void _takeMedia(bool isVideo) async { | ||||
|     final picker = ImagePicker(); | ||||
|     final result = isVideo | ||||
|         ? await picker.pickVideo(source: ImageSource.camera) | ||||
|         : await picker.pickImage(source: ImageSource.camera); | ||||
|     if (result == null) return; | ||||
|     onAdd([PostWriteMedia.fromFile(result)]); | ||||
|   } | ||||
|  | ||||
|   void _selectMedia() async { | ||||
|     final picker = ImagePicker(); | ||||
|     final result = await picker.pickMultipleMedia(); | ||||
|     if (result.isEmpty) return; | ||||
|     onAdd( | ||||
|       result.map((e) => PostWriteMedia.fromFile(e)), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   void _pasteMedia() async { | ||||
|     final imageBytes = await Pasteboard.image; | ||||
|     if (imageBytes == null) return; | ||||
|     onAdd([ | ||||
|       PostWriteMedia.fromBytes( | ||||
|         imageBytes, | ||||
|         'attachmentPastedImage'.tr(), | ||||
|         PostWriteMediaType.image, | ||||
|       ), | ||||
|     ]); | ||||
|   } | ||||
|  | ||||
|   void _linkRandomId(BuildContext context) async { | ||||
|     final randomIdController = TextEditingController(); | ||||
|     final randomId = await showDialog<String?>( | ||||
|       context: context, | ||||
|       builder: (context) => AlertDialog( | ||||
|         title: Text('addAttachmentFromRandomId').tr(), | ||||
|         content: Column( | ||||
|           mainAxisSize: MainAxisSize.min, | ||||
|           children: [ | ||||
|             TextField( | ||||
|               controller: randomIdController, | ||||
|               decoration: InputDecoration( | ||||
|                 labelText: 'fieldAttachmentRandomId'.tr(), | ||||
|                 border: const UnderlineInputBorder(), | ||||
|               ), | ||||
|             ), | ||||
|             const Gap(8), | ||||
|           ], | ||||
|         ), | ||||
|         actions: [ | ||||
|           TextButton( | ||||
|             child: Text('dialogDismiss').tr(), | ||||
|             onPressed: () { | ||||
|               Navigator.pop(context); | ||||
|             }, | ||||
|           ), | ||||
|           TextButton( | ||||
|             child: Text('dialogConfirm').tr(), | ||||
|             onPressed: () { | ||||
|               Navigator.pop(context, randomIdController.text); | ||||
|             }, | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|     WidgetsBinding.instance.addPostFrameCallback((_) { | ||||
|       randomIdController.dispose(); | ||||
|     }); | ||||
|     if (randomId == null || randomId.isEmpty) return; | ||||
|     if (!context.mounted) return; | ||||
|  | ||||
|     final attach = context.read<SnAttachmentProvider>(); | ||||
|     final attachment = await attach.getOne(randomId); | ||||
|  | ||||
|     onAdd([ | ||||
|       PostWriteMedia(attachment), | ||||
|     ]); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return PopupMenuButton( | ||||
|       icon: Icon( | ||||
|         Symbols.add_photo_alternate, | ||||
|         color: Theme.of(context).colorScheme.primary, | ||||
|       ), | ||||
|       itemBuilder: (context) => [ | ||||
|         if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows) | ||||
|           PopupMenuItem( | ||||
|             child: Row( | ||||
|               children: [ | ||||
|                 const Icon(Symbols.photo_camera), | ||||
|                 const Gap(16), | ||||
|                 Text('addAttachmentFromCameraPhoto').tr(), | ||||
|               ], | ||||
|             ), | ||||
|             onTap: () { | ||||
|               _takeMedia(false); | ||||
|             }, | ||||
|           ), | ||||
|         if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows) | ||||
|           PopupMenuItem( | ||||
|             child: Row( | ||||
|               children: [ | ||||
|                 const Icon(Symbols.videocam), | ||||
|                 const Gap(16), | ||||
|                 Text('addAttachmentFromCameraVideo').tr(), | ||||
|               ], | ||||
|             ), | ||||
|             onTap: () { | ||||
|               _takeMedia(true); | ||||
|             }, | ||||
|           ), | ||||
|         PopupMenuItem( | ||||
|           child: Row( | ||||
|             children: [ | ||||
|               const Icon(Symbols.photo_library), | ||||
|               const Gap(16), | ||||
|               Text('addAttachmentFromAlbum').tr(), | ||||
|             ], | ||||
|           ), | ||||
|           onTap: () { | ||||
|             _selectMedia(); | ||||
|           }, | ||||
|         ), | ||||
|         PopupMenuItem( | ||||
|           child: Row( | ||||
|             children: [ | ||||
|               const Icon(Symbols.link), | ||||
|               const Gap(16), | ||||
|               Text('addAttachmentFromRandomId').tr(), | ||||
|             ], | ||||
|           ), | ||||
|           onTap: () { | ||||
|             _linkRandomId(context); | ||||
|           }, | ||||
|         ), | ||||
|         PopupMenuItem( | ||||
|           child: Row( | ||||
|             children: [ | ||||
|               const Icon(Symbols.content_paste), | ||||
|               const Gap(16), | ||||
|               Text('addAttachmentFromClipboard').tr(), | ||||
|             ], | ||||
|           ), | ||||
|           onTap: () { | ||||
|             _pasteMedia(); | ||||
|           }, | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										16
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -753,10 +753,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: flutter_udid | ||||
|       sha256: be464dc5b1fb7ee894f6a32d65c086ca5e177fdcf9375ac08d77495b98150f84 | ||||
|       sha256: "166bee5989a58c66b8b62000ea65edccc7c8167bbafdbb08022638db330dd030" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.0.1" | ||||
|     version: "4.0.0" | ||||
|   flutter_web_plugins: | ||||
|     dependency: "direct main" | ||||
|     description: flutter | ||||
| @@ -766,10 +766,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_webrtc | ||||
|       sha256: "430859fb5b763d7556d06ef287cfca582e17d9a2dc36da26017f25a5c0b2523e" | ||||
|       sha256: "0e138a0a3bf6830c29c8439b17be0e222d0de27fa72f24e6aee4d34de72f22ef" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.12.4" | ||||
|     version: "0.12.5" | ||||
|   freezed: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
| @@ -934,10 +934,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: image_picker_android | ||||
|       sha256: fa8141602fde3f7e2f81dbf043613eb44dfa325fa0bcf93c0f142c9f7a2c193e | ||||
|       sha256: aa6f1280b670861ac45220cc95adc59bb6ae130259d36f980ccb62220dc5e59f | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.8.12+18" | ||||
|     version: "0.8.12+19" | ||||
|   image_picker_for_web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1086,10 +1086,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: livekit_client | ||||
|       sha256: "7802b5de1cae2ee3439db730d24d31c6dcbce173c5e6db2fc5774039a290bc2d" | ||||
|       sha256: a3ff529fe6745ee40cdedcd021d81c4a6ad946dd495e782596f2856eeeabc739 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.3.2" | ||||
|     version: "2.3.3" | ||||
|   logging: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|   | ||||
| @@ -80,7 +80,7 @@ dependencies: | ||||
|   firebase_core: ^3.8.0 | ||||
|   firebase_messaging: ^15.1.5 | ||||
|   firebase_analytics: ^11.3.5 | ||||
|   flutter_udid: ^3.0.0 | ||||
|   flutter_udid: ^4.0.0 | ||||
|   media_kit: ^1.1.11 | ||||
|   media_kit_video: ^1.2.5 | ||||
|   media_kit_libs_video: ^1.0.5 | ||||
|   | ||||
							
								
								
									
										221
									
								
								web/index.html
									
									
									
									
									
								
							
							
						
						
									
										221
									
								
								web/index.html
									
									
									
									
									
								
							| @@ -1,130 +1,133 @@ | ||||
| <!DOCTYPE html><html><head> | ||||
|   <!-- | ||||
|     If you are serving your web app in a path other than the root, change the | ||||
|     href value below to reflect the base path you are serving from. | ||||
| <!DOCTYPE html> | ||||
| <html lang="en" oncontextmenu="event.preventDefault();"> | ||||
| <head> | ||||
|     <!-- | ||||
|       If you are serving your web app in a path other than the root, change the | ||||
|       href value below to reflect the base path you are serving from. | ||||
|  | ||||
|     The path provided below has to start and end with a slash "/" in order for | ||||
|     it to work correctly. | ||||
|       The path provided below has to start and end with a slash "/" in order for | ||||
|       it to work correctly. | ||||
|  | ||||
|     For more details: | ||||
|     * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base | ||||
|       For more details: | ||||
|       * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base | ||||
|  | ||||
|     This is a placeholder for base href that will be replaced by the value of | ||||
|     the `--base-href` argument provided to `flutter build`. | ||||
|   --> | ||||
|   <base href="$FLUTTER_BASE_HREF"> | ||||
|       This is a placeholder for base href that will be replaced by the value of | ||||
|       the `--base-href` argument provided to `flutter build`. | ||||
|     --> | ||||
|     <base href="$FLUTTER_BASE_HREF"> | ||||
|  | ||||
|   <meta charset="UTF-8"> | ||||
|   <meta content="IE=Edge" http-equiv="X-UA-Compatible"> | ||||
|   <meta name="description" content="A new Flutter project."> | ||||
|     <meta charset="UTF-8"> | ||||
|     <meta content="IE=Edge" http-equiv="X-UA-Compatible"> | ||||
|     <meta name="description" content="A new Flutter project."> | ||||
|  | ||||
|   <!-- iOS meta tags & icons --> | ||||
|   <meta name="apple-mobile-web-app-capable" content="yes"> | ||||
|   <meta name="apple-mobile-web-app-status-bar-style" content="black"> | ||||
|   <meta name="apple-mobile-web-app-title" content="surface"> | ||||
|   <link rel="apple-touch-icon" href="icons/Icon-192.png"> | ||||
|     <!-- iOS meta tags & icons --> | ||||
|     <meta name="apple-mobile-web-app-capable" content="yes"> | ||||
|     <meta name="apple-mobile-web-app-status-bar-style" content="black"> | ||||
|     <meta name="apple-mobile-web-app-title" content="surface"> | ||||
|     <link rel="apple-touch-icon" href="icons/Icon-192.png"> | ||||
|  | ||||
|   <!-- Favicon --> | ||||
|   <link rel="icon" type="image/png" href="favicon.png"> | ||||
|     <!-- Favicon --> | ||||
|     <link rel="icon" type="image/png" href="favicon.png"> | ||||
|  | ||||
|   <title>Solian</title> | ||||
|   <link rel="manifest" href="manifest.json"> | ||||
|    | ||||
|    | ||||
|   <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"> | ||||
|    | ||||
|    | ||||
|    | ||||
|    | ||||
|   <style id="splash-screen-style"> | ||||
|     html { | ||||
|       height: 100% | ||||
|     } | ||||
|     <title>Solian</title> | ||||
|     <link rel="manifest" href="manifest.json"> | ||||
|  | ||||
|     body { | ||||
|       margin: 0; | ||||
|       min-height: 100%; | ||||
|       background-color: #ffffff; | ||||
|           background-size: 100% 100%; | ||||
|     } | ||||
|  | ||||
|     .center { | ||||
|       margin: 0; | ||||
|       position: absolute; | ||||
|       top: 50%; | ||||
|       left: 50%; | ||||
|       -ms-transform: translate(-50%, -50%); | ||||
|       transform: translate(-50%, -50%); | ||||
|     } | ||||
|     <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" | ||||
|           name="viewport"> | ||||
|  | ||||
|     .contain { | ||||
|       display:block; | ||||
|       width:100%; height:100%; | ||||
|       object-fit: contain; | ||||
|     } | ||||
|  | ||||
|     .stretch { | ||||
|       display:block; | ||||
|       width:100%; height:100%; | ||||
|     } | ||||
|     <style id="splash-screen-style"> | ||||
|         html { | ||||
|           height: 100% | ||||
|         } | ||||
|  | ||||
|     .cover { | ||||
|       display:block; | ||||
|       width:100%; height:100%; | ||||
|       object-fit: cover; | ||||
|     } | ||||
|         body { | ||||
|           margin: 0; | ||||
|           min-height: 100%; | ||||
|           background-color: #ffffff; | ||||
|               background-size: 100% 100%; | ||||
|         } | ||||
|  | ||||
|     .bottom { | ||||
|       position: absolute; | ||||
|       bottom: 0; | ||||
|       left: 50%; | ||||
|       -ms-transform: translate(-50%, 0); | ||||
|       transform: translate(-50%, 0); | ||||
|     } | ||||
|         .center { | ||||
|           margin: 0; | ||||
|           position: absolute; | ||||
|           top: 50%; | ||||
|           left: 50%; | ||||
|           -ms-transform: translate(-50%, -50%); | ||||
|           transform: translate(-50%, -50%); | ||||
|         } | ||||
|  | ||||
|     .bottomLeft { | ||||
|       position: absolute; | ||||
|       bottom: 0; | ||||
|       left: 0; | ||||
|     } | ||||
|         .contain { | ||||
|           display:block; | ||||
|           width:100%; height:100%; | ||||
|           object-fit: contain; | ||||
|         } | ||||
|  | ||||
|     .bottomRight { | ||||
|       position: absolute; | ||||
|       bottom: 0; | ||||
|       right: 0; | ||||
|     } | ||||
|         .stretch { | ||||
|           display:block; | ||||
|           width:100%; height:100%; | ||||
|         } | ||||
|  | ||||
|     @media (prefers-color-scheme: dark) { | ||||
|       body { | ||||
|         background-color: #000000; | ||||
|           } | ||||
|     } | ||||
|   </style> | ||||
|   <script id="splash-screen-script"> | ||||
|     function removeSplashFromWeb() { | ||||
|       document.getElementById("splash")?.remove(); | ||||
|       document.getElementById("splash-branding")?.remove(); | ||||
|       document.body.style.background = "transparent"; | ||||
|     } | ||||
|   </script> | ||||
|         .cover { | ||||
|           display:block; | ||||
|           width:100%; height:100%; | ||||
|           object-fit: cover; | ||||
|         } | ||||
|  | ||||
|         .bottom { | ||||
|           position: absolute; | ||||
|           bottom: 0; | ||||
|           left: 50%; | ||||
|           -ms-transform: translate(-50%, 0); | ||||
|           transform: translate(-50%, 0); | ||||
|         } | ||||
|  | ||||
|         .bottomLeft { | ||||
|           position: absolute; | ||||
|           bottom: 0; | ||||
|           left: 0; | ||||
|         } | ||||
|  | ||||
|         .bottomRight { | ||||
|           position: absolute; | ||||
|           bottom: 0; | ||||
|           right: 0; | ||||
|         } | ||||
|  | ||||
|         @media (prefers-color-scheme: dark) { | ||||
|           body { | ||||
|             background-color: #000000; | ||||
|               } | ||||
|         } | ||||
|     </style> | ||||
|     <script id="splash-screen-script"> | ||||
|         function removeSplashFromWeb() { | ||||
|           document.getElementById("splash")?.remove(); | ||||
|           document.getElementById("splash-branding")?.remove(); | ||||
|           document.body.style.background = "transparent"; | ||||
|         } | ||||
|     </script> | ||||
| </head> | ||||
| <body> | ||||
|   <picture id="splash-branding"> | ||||
|     <source srcset="splash/img/branding-1x.png 1x, splash/img/branding-2x.png 2x, splash/img/branding-3x.png 3x, splash/img/branding-4x.png 4x" media="(prefers-color-scheme: light)"> | ||||
|     <source srcset="splash/img/branding-dark-1x.png 1x, splash/img/branding-dark-2x.png 2x, splash/img/branding-dark-3x.png 3x, splash/img/branding-dark-4x.png 4x" media="(prefers-color-scheme: dark)"> | ||||
| <picture id="splash-branding"> | ||||
|     <source srcset="splash/img/branding-1x.png 1x, splash/img/branding-2x.png 2x, splash/img/branding-3x.png 3x, splash/img/branding-4x.png 4x" | ||||
|             media="(prefers-color-scheme: light)"> | ||||
|     <source srcset="splash/img/branding-dark-1x.png 1x, splash/img/branding-dark-2x.png 2x, splash/img/branding-dark-3x.png 3x, splash/img/branding-dark-4x.png 4x" | ||||
|             media="(prefers-color-scheme: dark)"> | ||||
|     <img class="bottom" aria-hidden="true" src="splash/img/branding-1x.png" alt=""> | ||||
|   </picture> | ||||
|   <picture id="splash"> | ||||
|       <source srcset="splash/img/light-1x.png 1x, splash/img/light-2x.png 2x, splash/img/light-3x.png 3x, splash/img/light-4x.png 4x" media="(prefers-color-scheme: light)"> | ||||
|       <source srcset="splash/img/dark-1x.png 1x, splash/img/dark-2x.png 2x, splash/img/dark-3x.png 3x, splash/img/dark-4x.png 4x" media="(prefers-color-scheme: dark)"> | ||||
|       <img class="center" aria-hidden="true" src="splash/img/light-1x.png" alt=""> | ||||
|   </picture> | ||||
|    | ||||
|    | ||||
|    | ||||
|    | ||||
|    | ||||
|   <script src="flutter_bootstrap.js" async=""></script> | ||||
| </picture> | ||||
| <picture id="splash"> | ||||
|     <source srcset="splash/img/light-1x.png 1x, splash/img/light-2x.png 2x, splash/img/light-3x.png 3x, splash/img/light-4x.png 4x" | ||||
|             media="(prefers-color-scheme: light)"> | ||||
|     <source srcset="splash/img/dark-1x.png 1x, splash/img/dark-2x.png 2x, splash/img/dark-3x.png 3x, splash/img/dark-4x.png 4x" | ||||
|             media="(prefers-color-scheme: dark)"> | ||||
|     <img class="center" aria-hidden="true" src="splash/img/light-1x.png" alt=""> | ||||
| </picture> | ||||
|  | ||||
|  | ||||
| </body></html> | ||||
| <script src="flutter_bootstrap.js" async=""></script> | ||||
|  | ||||
|  | ||||
| </body> | ||||
| </html> | ||||
		Reference in New Issue
	
	Block a user