Compare commits
	
		
			14 Commits
		
	
	
		
			2.3.2+63
			...
			f0a3bbe023
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| f0a3bbe023 | |||
| df81c84438 | |||
| 8b12395fca | |||
| cb2b71d194 | |||
| 7ed508e2bb | |||
| dad869967e | |||
| 2d5b3b554e | |||
| 74882116e3 | |||
| a97c3bce3a | |||
| 1aa70827dc | |||
| fe028860e9 | |||
| a2d2ce4d38 | |||
| 167c11b9eb | |||
| 8cb3933fcc | 
@@ -12,9 +12,9 @@ post {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
body:json {
 | 
					body:json {
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    "alias": "Meltdown",
 | 
					    "alias": "BaLoading",
 | 
				
			||||||
    "name": "Meltdown",
 | 
					    "name": "BaLoading",
 | 
				
			||||||
    "attachment_id": "IpDPHEbWDDCbBofX",
 | 
					    "attachment_id": "2JCI2uh21mKkfk9P",
 | 
				
			||||||
    "pack_id": 4
 | 
					    "pack_id": 3
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -154,9 +154,12 @@
 | 
				
			|||||||
  "fieldPublisherBelongToRealmUnset": "Unset Publisher Belongs to Realm",
 | 
					  "fieldPublisherBelongToRealmUnset": "Unset Publisher Belongs to Realm",
 | 
				
			||||||
  "writePostTypeStory": "Post a story",
 | 
					  "writePostTypeStory": "Post a story",
 | 
				
			||||||
  "writePostTypeArticle": "Write an article",
 | 
					  "writePostTypeArticle": "Write an article",
 | 
				
			||||||
 | 
					  "writePostTypeQuestion": "Ask a question",
 | 
				
			||||||
 | 
					  "writePostTypeVideo": "Post a video",
 | 
				
			||||||
  "fieldPostPublisher": "Post publisher",
 | 
					  "fieldPostPublisher": "Post publisher",
 | 
				
			||||||
  "fieldPostContent": "What happened?!",
 | 
					  "fieldPostContent": "What happened?!",
 | 
				
			||||||
  "fieldPostTitle": "Title",
 | 
					  "fieldPostTitle": "Title",
 | 
				
			||||||
 | 
					  "fieldPostQuestionReward": "Answer Rewards (Source Points)",
 | 
				
			||||||
  "fieldPostDescription": "Description",
 | 
					  "fieldPostDescription": "Description",
 | 
				
			||||||
  "fieldPostTags": "Tags",
 | 
					  "fieldPostTags": "Tags",
 | 
				
			||||||
  "fieldPostCategories": "Categories",
 | 
					  "fieldPostCategories": "Categories",
 | 
				
			||||||
@@ -166,9 +169,9 @@
 | 
				
			|||||||
  "postPosted": "Post has been posted.",
 | 
					  "postPosted": "Post has been posted.",
 | 
				
			||||||
  "postPublishedAt": "Published At",
 | 
					  "postPublishedAt": "Published At",
 | 
				
			||||||
  "postPublishedUntil": "Published Until",
 | 
					  "postPublishedUntil": "Published Until",
 | 
				
			||||||
  "postEditingNotice": "You're about to editing a post that posted {}.",
 | 
					  "postEditingNotice": "You're about to editing a post that posted by {}.",
 | 
				
			||||||
  "postReplyingNotice": "You're about to reply to a post that posted {}.",
 | 
					  "postReplyingNotice": "You're about to reply to a post that posted by {}.",
 | 
				
			||||||
  "postRepostingNotice": "You're about to repost a post that posted {}.",
 | 
					  "postRepostingNotice": "You're about to repost a post that posted by {}.",
 | 
				
			||||||
  "postReact": "React",
 | 
					  "postReact": "React",
 | 
				
			||||||
  "postReactions": "Reactions of Post",
 | 
					  "postReactions": "Reactions of Post",
 | 
				
			||||||
  "postReactionUpvote": {
 | 
					  "postReactionUpvote": {
 | 
				
			||||||
@@ -610,5 +613,11 @@
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  "aiThinkingProcess": "AI Thinking Process",
 | 
					  "aiThinkingProcess": "AI Thinking Process",
 | 
				
			||||||
  "accountSettingsApplied": "Account settings have been applied.",
 | 
					  "accountSettingsApplied": "Account settings have been applied.",
 | 
				
			||||||
  "trayMenuExit": "Exit"
 | 
					  "trayMenuExit": "Exit",
 | 
				
			||||||
 | 
					  "postQuestionUnanswered": "Unanswered Question",
 | 
				
			||||||
 | 
					  "postQuestionUnansweredWithReward": "Unanswered Question, reward source points {}",
 | 
				
			||||||
 | 
					  "postQuestionAnswered": "Answered Question",
 | 
				
			||||||
 | 
					  "postQuestionAnswerSelect": "Select as Answer",
 | 
				
			||||||
 | 
					  "postQuestionAnswerSelected": "Answer has been selected, reward has been applied.",
 | 
				
			||||||
 | 
					  "postVideoUpload": "Upload Video"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -138,9 +138,12 @@
 | 
				
			|||||||
  "fieldPublisherBelongToRealmUnset": "未设置发布者所属领域",
 | 
					  "fieldPublisherBelongToRealmUnset": "未设置发布者所属领域",
 | 
				
			||||||
  "writePostTypeStory": "发动态",
 | 
					  "writePostTypeStory": "发动态",
 | 
				
			||||||
  "writePostTypeArticle": "写文章",
 | 
					  "writePostTypeArticle": "写文章",
 | 
				
			||||||
 | 
					  "writePostTypeQuestion": "提问题",
 | 
				
			||||||
 | 
					  "writePostTypeVideo": "发视频",
 | 
				
			||||||
  "fieldPostPublisher": "帖子发布者",
 | 
					  "fieldPostPublisher": "帖子发布者",
 | 
				
			||||||
  "fieldPostContent": "发生什么事了?!",
 | 
					  "fieldPostContent": "发生什么事了?!",
 | 
				
			||||||
  "fieldPostTitle": "标题",
 | 
					  "fieldPostTitle": "标题",
 | 
				
			||||||
 | 
					  "fieldPostQuestionReward": "回答奖励源点",
 | 
				
			||||||
  "fieldPostDescription": "描述",
 | 
					  "fieldPostDescription": "描述",
 | 
				
			||||||
  "fieldPostTags": "标签",
 | 
					  "fieldPostTags": "标签",
 | 
				
			||||||
  "fieldPostCategories": "分类",
 | 
					  "fieldPostCategories": "分类",
 | 
				
			||||||
@@ -608,5 +611,12 @@
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  "aiThinkingProcess": "AI 思考过程",
 | 
					  "aiThinkingProcess": "AI 思考过程",
 | 
				
			||||||
  "accountSettingsApplied": "帐号设置已应用。",
 | 
					  "accountSettingsApplied": "帐号设置已应用。",
 | 
				
			||||||
  "trayMenuExit": "退出"
 | 
					  "trayMenuExit": "退出",
 | 
				
			||||||
 | 
					  "postQuestionUnanswered": "未解答的问题",
 | 
				
			||||||
 | 
					  "postQuestionUnansweredWithReward": "未解答的问题,悬赏源点 {}",
 | 
				
			||||||
 | 
					  "postQuestionAnswered": "已解答的问题",
 | 
				
			||||||
 | 
					  "postQuestionAnswerTitle": "精选解答",
 | 
				
			||||||
 | 
					  "postQuestionAnswerSelect": "选择解答",
 | 
				
			||||||
 | 
					  "postQuestionAnswerSelected": "解答已选择,奖励已发放。",
 | 
				
			||||||
 | 
					  "postVideoUpload": "上传视频"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -138,9 +138,11 @@
 | 
				
			|||||||
  "fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
 | 
					  "fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
 | 
				
			||||||
  "writePostTypeStory": "發動態",
 | 
					  "writePostTypeStory": "發動態",
 | 
				
			||||||
  "writePostTypeArticle": "寫文章",
 | 
					  "writePostTypeArticle": "寫文章",
 | 
				
			||||||
 | 
					  "writePostTypeQuestion": "提問題",
 | 
				
			||||||
  "fieldPostPublisher": "帖子發佈者",
 | 
					  "fieldPostPublisher": "帖子發佈者",
 | 
				
			||||||
  "fieldPostContent": "發生什麼事了?!",
 | 
					  "fieldPostContent": "發生什麼事了?!",
 | 
				
			||||||
  "fieldPostTitle": "標題",
 | 
					  "fieldPostTitle": "標題",
 | 
				
			||||||
 | 
					  "fieldPostQuestionReward": "回答獎勵源點",
 | 
				
			||||||
  "fieldPostDescription": "描述",
 | 
					  "fieldPostDescription": "描述",
 | 
				
			||||||
  "fieldPostTags": "標籤",
 | 
					  "fieldPostTags": "標籤",
 | 
				
			||||||
  "fieldPostCategories": "分類",
 | 
					  "fieldPostCategories": "分類",
 | 
				
			||||||
@@ -607,5 +609,12 @@
 | 
				
			|||||||
    "other": "{} 源點"
 | 
					    "other": "{} 源點"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "aiThinkingProcess": "AI 思考過程",
 | 
					  "aiThinkingProcess": "AI 思考過程",
 | 
				
			||||||
  "accountSettingsApplied": "帳號設置已應用。"
 | 
					  "accountSettingsApplied": "帳號設置已應用。",
 | 
				
			||||||
 | 
					  "trayMenuExit": "退出",
 | 
				
			||||||
 | 
					  "postQuestionUnanswered": "未解答的問題",
 | 
				
			||||||
 | 
					  "postQuestionUnansweredWithReward": "未解答的問題,懸賞源點 {}",
 | 
				
			||||||
 | 
					  "postQuestionAnswered": "已解答的問題",
 | 
				
			||||||
 | 
					  "postQuestionAnswerTitle": "精選解答",
 | 
				
			||||||
 | 
					  "postQuestionAnswerSelect": "選擇解答",
 | 
				
			||||||
 | 
					  "postQuestionAnswerSelected": "解答已選擇,獎勵已發放。"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -138,9 +138,11 @@
 | 
				
			|||||||
  "fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
 | 
					  "fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
 | 
				
			||||||
  "writePostTypeStory": "發動態",
 | 
					  "writePostTypeStory": "發動態",
 | 
				
			||||||
  "writePostTypeArticle": "寫文章",
 | 
					  "writePostTypeArticle": "寫文章",
 | 
				
			||||||
 | 
					  "writePostTypeQuestion": "提問題",
 | 
				
			||||||
  "fieldPostPublisher": "帖子發佈者",
 | 
					  "fieldPostPublisher": "帖子發佈者",
 | 
				
			||||||
  "fieldPostContent": "發生什麼事了?!",
 | 
					  "fieldPostContent": "發生什麼事了?!",
 | 
				
			||||||
  "fieldPostTitle": "標題",
 | 
					  "fieldPostTitle": "標題",
 | 
				
			||||||
 | 
					  "fieldPostQuestionReward": "回答獎勵源點",
 | 
				
			||||||
  "fieldPostDescription": "描述",
 | 
					  "fieldPostDescription": "描述",
 | 
				
			||||||
  "fieldPostTags": "標籤",
 | 
					  "fieldPostTags": "標籤",
 | 
				
			||||||
  "fieldPostCategories": "分類",
 | 
					  "fieldPostCategories": "分類",
 | 
				
			||||||
@@ -607,5 +609,12 @@
 | 
				
			|||||||
    "other": "{} 源點"
 | 
					    "other": "{} 源點"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "aiThinkingProcess": "AI 思考過程",
 | 
					  "aiThinkingProcess": "AI 思考過程",
 | 
				
			||||||
  "accountSettingsApplied": "帳號設置已應用。"
 | 
					  "accountSettingsApplied": "帳號設置已應用。",
 | 
				
			||||||
 | 
					  "trayMenuExit": "退出",
 | 
				
			||||||
 | 
					  "postQuestionUnanswered": "未解答的問題",
 | 
				
			||||||
 | 
					  "postQuestionUnansweredWithReward": "未解答的問題,懸賞源點 {}",
 | 
				
			||||||
 | 
					  "postQuestionAnswered": "已解答的問題",
 | 
				
			||||||
 | 
					  "postQuestionAnswerTitle": "精選解答",
 | 
				
			||||||
 | 
					  "postQuestionAnswerSelect": "選擇解答",
 | 
				
			||||||
 | 
					  "postQuestionAnswerSelected": "解答已選擇,獎勵已發放。"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,6 @@ PODS:
 | 
				
			|||||||
  - Alamofire (5.10.2)
 | 
					  - Alamofire (5.10.2)
 | 
				
			||||||
  - connectivity_plus (0.0.1):
 | 
					  - connectivity_plus (0.0.1):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
    - FlutterMacOS
 | 
					 | 
				
			||||||
  - croppy (0.0.1):
 | 
					  - croppy (0.0.1):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - device_info_plus (0.0.1):
 | 
					  - device_info_plus (0.0.1):
 | 
				
			||||||
@@ -180,7 +179,7 @@ PODS:
 | 
				
			|||||||
  - in_app_review (2.0.0):
 | 
					  - in_app_review (2.0.0):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - Kingfisher (8.2.0)
 | 
					  - Kingfisher (8.2.0)
 | 
				
			||||||
  - livekit_client (2.3.5):
 | 
					  - livekit_client (2.3.6):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
    - flutter_webrtc
 | 
					    - flutter_webrtc
 | 
				
			||||||
    - WebRTC-SDK (= 125.6422.06)
 | 
					    - WebRTC-SDK (= 125.6422.06)
 | 
				
			||||||
@@ -237,7 +236,7 @@ PODS:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
DEPENDENCIES:
 | 
					DEPENDENCIES:
 | 
				
			||||||
  - Alamofire
 | 
					  - Alamofire
 | 
				
			||||||
  - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
 | 
					  - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
 | 
				
			||||||
  - croppy (from `.symlinks/plugins/croppy/ios`)
 | 
					  - croppy (from `.symlinks/plugins/croppy/ios`)
 | 
				
			||||||
  - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
 | 
					  - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
 | 
				
			||||||
  - file_picker (from `.symlinks/plugins/file_picker/ios`)
 | 
					  - file_picker (from `.symlinks/plugins/file_picker/ios`)
 | 
				
			||||||
@@ -300,7 +299,7 @@ SPEC REPOS:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
EXTERNAL SOURCES:
 | 
					EXTERNAL SOURCES:
 | 
				
			||||||
  connectivity_plus:
 | 
					  connectivity_plus:
 | 
				
			||||||
    :path: ".symlinks/plugins/connectivity_plus/darwin"
 | 
					    :path: ".symlinks/plugins/connectivity_plus/ios"
 | 
				
			||||||
  croppy:
 | 
					  croppy:
 | 
				
			||||||
    :path: ".symlinks/plugins/croppy/ios"
 | 
					    :path: ".symlinks/plugins/croppy/ios"
 | 
				
			||||||
  device_info_plus:
 | 
					  device_info_plus:
 | 
				
			||||||
@@ -374,7 +373,7 @@ EXTERNAL SOURCES:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
SPEC CHECKSUMS:
 | 
					SPEC CHECKSUMS:
 | 
				
			||||||
  Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
 | 
					  Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
 | 
				
			||||||
  connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
 | 
					  connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
 | 
				
			||||||
  croppy: b6199bc8d56bd2e03cc11609d1c47ad9875c1321
 | 
					  croppy: b6199bc8d56bd2e03cc11609d1c47ad9875c1321
 | 
				
			||||||
  device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
 | 
					  device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
 | 
				
			||||||
  DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
 | 
					  DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
 | 
				
			||||||
@@ -404,7 +403,7 @@ SPEC CHECKSUMS:
 | 
				
			|||||||
  image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
 | 
					  image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
 | 
				
			||||||
  in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
 | 
					  in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
 | 
				
			||||||
  Kingfisher: 323e5c4ec7983aaace12af655a7b51a7f88a599d
 | 
					  Kingfisher: 323e5c4ec7983aaace12af655a7b51a7f88a599d
 | 
				
			||||||
  livekit_client: dcc5fd47ba69c98fc6baeb12e862c9d43807d976
 | 
					  livekit_client: 148b2cf67a09aaf475ba8e5bf1667fe10dc35f81
 | 
				
			||||||
  media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
 | 
					  media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
 | 
				
			||||||
  media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
 | 
					  media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
 | 
				
			||||||
  media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
 | 
					  media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -71,7 +71,7 @@ class ChatMessageController extends ChangeNotifier {
 | 
				
			|||||||
      resp.data as Map<String, dynamic>,
 | 
					      resp.data as Map<String, dynamic>,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _wsSubscription = _ws.stream.stream.listen((event) {
 | 
					    _wsSubscription = _ws.pk.stream.listen((event) {
 | 
				
			||||||
      switch (event.method) {
 | 
					      switch (event.method) {
 | 
				
			||||||
        case 'events.new':
 | 
					        case 'events.new':
 | 
				
			||||||
          if (event.payload?['channel_id'] != channel?.id) break;
 | 
					          if (event.payload?['channel_id'] != channel?.id) break;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -144,6 +144,8 @@ class PostWriteController extends ChangeNotifier {
 | 
				
			|||||||
  static const Map<String, String> kTitleMap = {
 | 
					  static const Map<String, String> kTitleMap = {
 | 
				
			||||||
    'stories': 'writePostTypeStory',
 | 
					    'stories': 'writePostTypeStory',
 | 
				
			||||||
    'articles': 'writePostTypeArticle',
 | 
					    'articles': 'writePostTypeArticle',
 | 
				
			||||||
 | 
					    'questions': 'writePostTypeQuestion',
 | 
				
			||||||
 | 
					    'videos': 'writePostTypeVideo',
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static const kAttachmentProgressWeight = 0.9;
 | 
					  static const kAttachmentProgressWeight = 0.9;
 | 
				
			||||||
@@ -153,6 +155,7 @@ class PostWriteController extends ChangeNotifier {
 | 
				
			|||||||
  final TextEditingController titleController = TextEditingController();
 | 
					  final TextEditingController titleController = TextEditingController();
 | 
				
			||||||
  final TextEditingController descriptionController = TextEditingController();
 | 
					  final TextEditingController descriptionController = TextEditingController();
 | 
				
			||||||
  final TextEditingController aliasController = TextEditingController();
 | 
					  final TextEditingController aliasController = TextEditingController();
 | 
				
			||||||
 | 
					  final TextEditingController rewardController = TextEditingController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bool _temporarySaveActive = false;
 | 
					  bool _temporarySaveActive = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -168,6 +171,7 @@ class PostWriteController extends ChangeNotifier {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
    contentController.addListener(() {
 | 
					    contentController.addListener(() {
 | 
				
			||||||
      _temporaryPlanSave();
 | 
					      _temporaryPlanSave();
 | 
				
			||||||
 | 
					      notifyListeners();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    if (doLoadFromTemporary) _temporaryLoad();
 | 
					    if (doLoadFromTemporary) _temporaryLoad();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -194,6 +198,7 @@ class PostWriteController extends ChangeNotifier {
 | 
				
			|||||||
  PostWriteMedia? thumbnail;
 | 
					  PostWriteMedia? thumbnail;
 | 
				
			||||||
  List<PostWriteMedia> attachments = List.empty(growable: true);
 | 
					  List<PostWriteMedia> attachments = List.empty(growable: true);
 | 
				
			||||||
  DateTime? publishedAt, publishedUntil;
 | 
					  DateTime? publishedAt, publishedUntil;
 | 
				
			||||||
 | 
					  SnAttachment? videoAttachment;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> fetchRelatedPost(
 | 
					  Future<void> fetchRelatedPost(
 | 
				
			||||||
    BuildContext context, {
 | 
					    BuildContext context, {
 | 
				
			||||||
@@ -214,6 +219,8 @@ class PostWriteController extends ChangeNotifier {
 | 
				
			|||||||
        descriptionController.text = post.body['description'] ?? '';
 | 
					        descriptionController.text = post.body['description'] ?? '';
 | 
				
			||||||
        contentController.text = post.body['content'] ?? '';
 | 
					        contentController.text = post.body['content'] ?? '';
 | 
				
			||||||
        aliasController.text = post.alias ?? '';
 | 
					        aliasController.text = post.alias ?? '';
 | 
				
			||||||
 | 
					        rewardController.text = post.body['reward']?.toString() ?? '';
 | 
				
			||||||
 | 
					        videoAttachment = post.preload?.video;
 | 
				
			||||||
        publishedAt = post.publishedAt;
 | 
					        publishedAt = post.publishedAt;
 | 
				
			||||||
        publishedUntil = post.publishedUntil;
 | 
					        publishedUntil = post.publishedUntil;
 | 
				
			||||||
        visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
 | 
					        visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
 | 
				
			||||||
@@ -347,6 +354,7 @@ class PostWriteController extends ChangeNotifier {
 | 
				
			|||||||
          if (aliasController.text.isNotEmpty) 'alias': aliasController.text,
 | 
					          if (aliasController.text.isNotEmpty) 'alias': aliasController.text,
 | 
				
			||||||
          if (titleController.text.isNotEmpty) 'title': titleController.text,
 | 
					          if (titleController.text.isNotEmpty) 'title': titleController.text,
 | 
				
			||||||
          if (descriptionController.text.isNotEmpty) 'description': descriptionController.text,
 | 
					          if (descriptionController.text.isNotEmpty) 'description': descriptionController.text,
 | 
				
			||||||
 | 
					          if (rewardController.text.isNotEmpty) 'reward': rewardController.text,
 | 
				
			||||||
          if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.toJson(),
 | 
					          if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.toJson(),
 | 
				
			||||||
          'attachments':
 | 
					          'attachments':
 | 
				
			||||||
              attachments.where((e) => e.attachment != null).map((e) => e.attachment!.toJson()).toList(growable: true),
 | 
					              attachments.where((e) => e.attachment != null).map((e) => e.attachment!.toJson()).toList(growable: true),
 | 
				
			||||||
@@ -375,6 +383,7 @@ class PostWriteController extends ChangeNotifier {
 | 
				
			|||||||
      aliasController.text = data['alias'] ?? '';
 | 
					      aliasController.text = data['alias'] ?? '';
 | 
				
			||||||
      titleController.text = data['title'] ?? '';
 | 
					      titleController.text = data['title'] ?? '';
 | 
				
			||||||
      descriptionController.text = data['description'] ?? '';
 | 
					      descriptionController.text = data['description'] ?? '';
 | 
				
			||||||
 | 
					      rewardController.text = data['reward']?.toString() ?? '';
 | 
				
			||||||
      if (data['thumbnail'] != null) thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
 | 
					      if (data['thumbnail'] != null) thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
 | 
				
			||||||
      attachments
 | 
					      attachments
 | 
				
			||||||
          .addAll(data['attachments'].map((ele) => PostWriteMedia(SnAttachment.fromJson(ele))).cast<PostWriteMedia>());
 | 
					          .addAll(data['attachments'].map((ele) => PostWriteMedia(SnAttachment.fromJson(ele))).cast<PostWriteMedia>());
 | 
				
			||||||
@@ -473,6 +482,8 @@ class PostWriteController extends ChangeNotifier {
 | 
				
			|||||||
    progress = kAttachmentProgressWeight;
 | 
					    progress = kAttachmentProgressWeight;
 | 
				
			||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final reward = double.tryParse(rewardController.text);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Posting the content
 | 
					    // Posting the content
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      final baseProgressVal = progress!;
 | 
					      final baseProgressVal = progress!;
 | 
				
			||||||
@@ -498,6 +509,8 @@ class PostWriteController extends ChangeNotifier {
 | 
				
			|||||||
          if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
 | 
					          if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
 | 
				
			||||||
          if (replyingPost != null) 'reply_to': replyingPost!.id,
 | 
					          if (replyingPost != null) 'reply_to': replyingPost!.id,
 | 
				
			||||||
          if (repostingPost != null) 'repost_to': repostingPost!.id,
 | 
					          if (repostingPost != null) 'repost_to': repostingPost!.id,
 | 
				
			||||||
 | 
					          if (reward != null) 'reward': reward,
 | 
				
			||||||
 | 
					          if (videoAttachment != null) 'video': videoAttachment!.rid,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        onSendProgress: (count, total) {
 | 
					        onSendProgress: (count, total) {
 | 
				
			||||||
          progress = baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
 | 
					          progress = baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
 | 
				
			||||||
@@ -624,6 +637,11 @@ class PostWriteController extends ChangeNotifier {
 | 
				
			|||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void setVideoAttachment(SnAttachment? value) {
 | 
				
			||||||
 | 
					    videoAttachment = value;
 | 
				
			||||||
 | 
					    notifyListeners();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void reset() {
 | 
					  void reset() {
 | 
				
			||||||
    publishedAt = null;
 | 
					    publishedAt = null;
 | 
				
			||||||
    publishedUntil = null;
 | 
					    publishedUntil = null;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -77,7 +77,7 @@ class NotificationProvider extends ChangeNotifier {
 | 
				
			|||||||
  List<SnNotification> notifications = List.empty(growable: true);
 | 
					  List<SnNotification> notifications = List.empty(growable: true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void listen() {
 | 
					  void listen() {
 | 
				
			||||||
    _ws.stream.stream.listen((event) {
 | 
					    _ws.pk.stream.listen((event) {
 | 
				
			||||||
      if (event.method == 'notifications.new') {
 | 
					      if (event.method == 'notifications.new') {
 | 
				
			||||||
        final notification = SnNotification.fromJson(event.payload!);
 | 
					        final notification = SnNotification.fromJson(event.payload!);
 | 
				
			||||||
        if (showingCount < 0) showingCount = 0;
 | 
					        if (showingCount < 0) showingCount = 0;
 | 
				
			||||||
@@ -103,10 +103,10 @@ class NotificationProvider extends ChangeNotifier {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  void updateTray() {
 | 
					  void updateTray() {
 | 
				
			||||||
    if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
 | 
					    if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
 | 
				
			||||||
    if (notifications.isEmpty) {
 | 
					    if (showingTrayCount == 0) {
 | 
				
			||||||
      trayManager.setTitle('');
 | 
					      trayManager.setTitle('');
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      trayManager.setTitle(' ${notifications.length.toString()}');
 | 
					      trayManager.setTitle(' $showingTrayCount');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,6 +23,9 @@ class SnPostContentProvider {
 | 
				
			|||||||
      if (out[i].body['thumbnail'] != null) {
 | 
					      if (out[i].body['thumbnail'] != null) {
 | 
				
			||||||
        rids.add(out[i].body['thumbnail']);
 | 
					        rids.add(out[i].body['thumbnail']);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      if (out[i].body['video'] != null) {
 | 
				
			||||||
 | 
					        rids.add(out[i].body['video']);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      if (out[i].repostTo != null) {
 | 
					      if (out[i].repostTo != null) {
 | 
				
			||||||
        out[i] = out[i].copyWith(
 | 
					        out[i] = out[i].copyWith(
 | 
				
			||||||
          repostTo: await _preloadRelatedDataSingle(out[i].repostTo!),
 | 
					          repostTo: await _preloadRelatedDataSingle(out[i].repostTo!),
 | 
				
			||||||
@@ -36,6 +39,7 @@ class SnPostContentProvider {
 | 
				
			|||||||
        preload: SnPostPreload(
 | 
					        preload: SnPostPreload(
 | 
				
			||||||
          thumbnail: attachments.where((ele) => ele?.rid == out[i].body['thumbnail']).firstOrNull,
 | 
					          thumbnail: attachments.where((ele) => ele?.rid == out[i].body['thumbnail']).firstOrNull,
 | 
				
			||||||
          attachments: attachments.where((ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false).toList(),
 | 
					          attachments: attachments.where((ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false).toList(),
 | 
				
			||||||
 | 
					          video: attachments.where((ele) => ele?.rid == out[i].body['video']).firstOrNull,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -53,6 +57,9 @@ class SnPostContentProvider {
 | 
				
			|||||||
    if (out.body['thumbnail'] != null) {
 | 
					    if (out.body['thumbnail'] != null) {
 | 
				
			||||||
      rids.add(out.body['thumbnail']);
 | 
					      rids.add(out.body['thumbnail']);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if (out.body['video'] != null) {
 | 
				
			||||||
 | 
					      rids.add(out.body['video']);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    if (out.repostTo != null) {
 | 
					    if (out.repostTo != null) {
 | 
				
			||||||
      out = out.copyWith(
 | 
					      out = out.copyWith(
 | 
				
			||||||
        repostTo: await _preloadRelatedDataSingle(out.repostTo!),
 | 
					        repostTo: await _preloadRelatedDataSingle(out.repostTo!),
 | 
				
			||||||
@@ -64,6 +71,7 @@ class SnPostContentProvider {
 | 
				
			|||||||
      preload: SnPostPreload(
 | 
					      preload: SnPostPreload(
 | 
				
			||||||
        thumbnail: attachments.where((ele) => ele?.rid == out.body['thumbnail']).firstOrNull,
 | 
					        thumbnail: attachments.where((ele) => ele?.rid == out.body['thumbnail']).firstOrNull,
 | 
				
			||||||
        attachments: attachments.where((ele) => out.body['attachments']?.contains(ele?.rid) ?? false).toList(),
 | 
					        attachments: attachments.where((ele) => out.body['attachments']?.contains(ele?.rid) ?? false).toList(),
 | 
				
			||||||
 | 
					        video: attachments.where((ele) => ele?.rid == out.body['video']).firstOrNull,
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,7 +18,8 @@ class WebSocketProvider extends ChangeNotifier {
 | 
				
			|||||||
  late final SnNetworkProvider _sn;
 | 
					  late final SnNetworkProvider _sn;
 | 
				
			||||||
  late final UserProvider _ua;
 | 
					  late final UserProvider _ua;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  StreamController<WebSocketPackage> stream = StreamController.broadcast();
 | 
					  StreamController<WebSocketPackage> pk = StreamController.broadcast();
 | 
				
			||||||
 | 
					  Stream<dynamic>? _wsStream;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  WebSocketProvider(BuildContext context) {
 | 
					  WebSocketProvider(BuildContext context) {
 | 
				
			||||||
    _sn = context.read<SnNetworkProvider>();
 | 
					    _sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
@@ -59,6 +60,7 @@ class WebSocketProvider extends ChangeNotifier {
 | 
				
			|||||||
    try {
 | 
					    try {
 | 
				
			||||||
      conn = WebSocketChannel.connect(uri);
 | 
					      conn = WebSocketChannel.connect(uri);
 | 
				
			||||||
      await conn!.ready;
 | 
					      await conn!.ready;
 | 
				
			||||||
 | 
					      _wsStream = conn!.stream.asBroadcastStream();
 | 
				
			||||||
      listen();
 | 
					      listen();
 | 
				
			||||||
      log('[WebSocket] Connected to server!');
 | 
					      log('[WebSocket] Connected to server!');
 | 
				
			||||||
      isConnected = true;
 | 
					      isConnected = true;
 | 
				
			||||||
@@ -93,11 +95,12 @@ class WebSocketProvider extends ChangeNotifier {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void listen() {
 | 
					  void listen() {
 | 
				
			||||||
    conn?.stream.listen(
 | 
					    if (_wsStream == null) return;
 | 
				
			||||||
 | 
					    _wsStream!.listen(
 | 
				
			||||||
      (event) {
 | 
					      (event) {
 | 
				
			||||||
        final packet = WebSocketPackage.fromJson(jsonDecode(event));
 | 
					        final packet = WebSocketPackage.fromJson(jsonDecode(event));
 | 
				
			||||||
        log('Websocket incoming message: ${packet.method} ${packet.message}');
 | 
					        log('Websocket incoming message: ${packet.method} ${packet.message}');
 | 
				
			||||||
        stream.sink.add(packet);
 | 
					        pk.sink.add(packet);
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      onDone: () {
 | 
					      onDone: () {
 | 
				
			||||||
        isConnected = false;
 | 
					        isConnected = false;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -192,11 +192,6 @@ final _appRoutes = [
 | 
				
			|||||||
      child: const RealmScreen(),
 | 
					      child: const RealmScreen(),
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    routes: [
 | 
					    routes: [
 | 
				
			||||||
      GoRoute(
 | 
					 | 
				
			||||||
        path: '/:alias',
 | 
					 | 
				
			||||||
        name: 'realmDetail',
 | 
					 | 
				
			||||||
        builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!),
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      GoRoute(
 | 
					      GoRoute(
 | 
				
			||||||
        path: '/manage',
 | 
					        path: '/manage',
 | 
				
			||||||
        name: 'realmManage',
 | 
					        name: 'realmManage',
 | 
				
			||||||
@@ -204,6 +199,11 @@ final _appRoutes = [
 | 
				
			|||||||
          editingRealmAlias: state.uri.queryParameters['editing'],
 | 
					          editingRealmAlias: state.uri.queryParameters['editing'],
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
 | 
					      GoRoute(
 | 
				
			||||||
 | 
					        path: '/:alias',
 | 
				
			||||||
 | 
					        name: 'realmDetail',
 | 
				
			||||||
 | 
					        builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
  ),
 | 
					  ),
 | 
				
			||||||
  GoRoute(path: '/news', name: 'news', builder: (context, state) => const NewsScreen(), routes: [
 | 
					  GoRoute(path: '/news', name: 'news', builder: (context, state) => const NewsScreen(), routes: [
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -155,12 +155,16 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
				
			|||||||
                text: TextSpan(children: [
 | 
					                text: TextSpan(children: [
 | 
				
			||||||
                  TextSpan(
 | 
					                  TextSpan(
 | 
				
			||||||
                    text: 'call'.tr(),
 | 
					                    text: 'call'.tr(),
 | 
				
			||||||
                    style: Theme.of(context).textTheme.titleLarge!.copyWith(color: Colors.white),
 | 
					                    style: Theme.of(context).textTheme.titleLarge!.copyWith(
 | 
				
			||||||
 | 
					                          color: Theme.of(context).appBarTheme.foregroundColor,
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  const TextSpan(text: '\n'),
 | 
					                  const TextSpan(text: '\n'),
 | 
				
			||||||
                  TextSpan(
 | 
					                  TextSpan(
 | 
				
			||||||
                    text: call.lastDuration.toString(),
 | 
					                    text: call.lastDuration.toString(),
 | 
				
			||||||
                    style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.white),
 | 
					                    style: Theme.of(context).textTheme.bodySmall!.copyWith(
 | 
				
			||||||
 | 
					                          color: Theme.of(context).appBarTheme.foregroundColor,
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ]),
 | 
					                ]),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					import 'package:collection/collection.dart';
 | 
				
			||||||
import 'package:dio/dio.dart';
 | 
					import 'package:dio/dio.dart';
 | 
				
			||||||
import 'package:dropdown_button2/dropdown_button2.dart';
 | 
					import 'package:dropdown_button2/dropdown_button2.dart';
 | 
				
			||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
@@ -17,6 +18,7 @@ import 'package:uuid/uuid.dart';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class ChatManageScreen extends StatefulWidget {
 | 
					class ChatManageScreen extends StatefulWidget {
 | 
				
			||||||
  final String? editingChannelAlias;
 | 
					  final String? editingChannelAlias;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const ChatManageScreen({super.key, this.editingChannelAlias});
 | 
					  const ChatManageScreen({super.key, this.editingChannelAlias});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@@ -33,6 +35,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
  List<SnRealm>? _realms;
 | 
					  List<SnRealm>? _realms;
 | 
				
			||||||
  SnRealm? _belongToRealm;
 | 
					  SnRealm? _belongToRealm;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  SnChannel? _editingChannel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> _fetchRealms() async {
 | 
					  Future<void> _fetchRealms() async {
 | 
				
			||||||
    setState(() => _isBusy = true);
 | 
					    setState(() => _isBusy = true);
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
@@ -41,6 +45,9 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
      _realms = List<SnRealm>.from(
 | 
					      _realms = List<SnRealm>.from(
 | 
				
			||||||
        resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
 | 
					        resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					      if (_editingChannel != null) {
 | 
				
			||||||
 | 
					        _belongToRealm = _realms?.firstWhereOrNull((e) => e.id == _editingChannel!.realmId);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    } catch (err) {
 | 
					    } catch (err) {
 | 
				
			||||||
      if (mounted) context.showErrorDialog(err);
 | 
					      if (mounted) context.showErrorDialog(err);
 | 
				
			||||||
    } finally {
 | 
					    } finally {
 | 
				
			||||||
@@ -48,8 +55,6 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  SnChannel? _editingChannel;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> _fetchChannel() async {
 | 
					  Future<void> _fetchChannel() async {
 | 
				
			||||||
    setState(() => _isBusy = true);
 | 
					    setState(() => _isBusy = true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -124,9 +129,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return AppScaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      appBar: AppBar(
 | 
					      appBar: AppBar(
 | 
				
			||||||
        title: widget.editingChannelAlias != null
 | 
					        title: widget.editingChannelAlias != null ? Text('screenChatManage').tr() : Text('screenChatNew').tr(),
 | 
				
			||||||
            ? Text('screenChatManage').tr()
 | 
					 | 
				
			||||||
            : Text('screenChatNew').tr(),
 | 
					 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      body: SingleChildScrollView(
 | 
					      body: SingleChildScrollView(
 | 
				
			||||||
        child: Column(
 | 
					        child: Column(
 | 
				
			||||||
@@ -138,8 +141,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
                leadingPadding: const EdgeInsets.only(left: 10, right: 20),
 | 
					                leadingPadding: const EdgeInsets.only(left: 10, right: 20),
 | 
				
			||||||
                dividerColor: Colors.transparent,
 | 
					                dividerColor: Colors.transparent,
 | 
				
			||||||
                content: Text(
 | 
					                content: Text(
 | 
				
			||||||
                  'channelEditingNotice'
 | 
					                  'channelEditingNotice'.tr(args: ['#${_editingChannel!.alias}']),
 | 
				
			||||||
                      .tr(args: ['#${_editingChannel!.alias}']),
 | 
					 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                actions: [
 | 
					                actions: [
 | 
				
			||||||
                  TextButton(
 | 
					                  TextButton(
 | 
				
			||||||
@@ -162,6 +164,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
                items: [
 | 
					                items: [
 | 
				
			||||||
                  ...(_realms?.map(
 | 
					                  ...(_realms?.map(
 | 
				
			||||||
                        (SnRealm item) => DropdownMenuItem<SnRealm>(
 | 
					                        (SnRealm item) => DropdownMenuItem<SnRealm>(
 | 
				
			||||||
 | 
					                          enabled: _editingChannel == null || _editingChannel?.realmId == item.id,
 | 
				
			||||||
                          value: item,
 | 
					                          value: item,
 | 
				
			||||||
                          child: Row(
 | 
					                          child: Row(
 | 
				
			||||||
                            children: [
 | 
					                            children: [
 | 
				
			||||||
@@ -179,15 +182,12 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
                                  mainAxisSize: MainAxisSize.min,
 | 
					                                  mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
                                  crossAxisAlignment: CrossAxisAlignment.start,
 | 
					                                  crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
                                  children: [
 | 
					                                  children: [
 | 
				
			||||||
                                    Text(item.name).textStyle(Theme.of(context)
 | 
					                                    Text(item.name).textStyle(Theme.of(context).textTheme.bodyMedium!),
 | 
				
			||||||
                                        .textTheme
 | 
					 | 
				
			||||||
                                        .bodyMedium!),
 | 
					 | 
				
			||||||
                                    Text(
 | 
					                                    Text(
 | 
				
			||||||
                                      item.description,
 | 
					                                      item.description,
 | 
				
			||||||
                                      maxLines: 1,
 | 
					                                      maxLines: 1,
 | 
				
			||||||
                                      overflow: TextOverflow.ellipsis,
 | 
					                                      overflow: TextOverflow.ellipsis,
 | 
				
			||||||
                                    ).textStyle(
 | 
					                                    ).textStyle(Theme.of(context).textTheme.bodySmall!),
 | 
				
			||||||
                                        Theme.of(context).textTheme.bodySmall!),
 | 
					 | 
				
			||||||
                                  ],
 | 
					                                  ],
 | 
				
			||||||
                                ),
 | 
					                                ),
 | 
				
			||||||
                              ),
 | 
					                              ),
 | 
				
			||||||
@@ -197,14 +197,14 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
                      ) ??
 | 
					                      ) ??
 | 
				
			||||||
                      []),
 | 
					                      []),
 | 
				
			||||||
                  DropdownMenuItem<SnRealm>(
 | 
					                  DropdownMenuItem<SnRealm>(
 | 
				
			||||||
 | 
					                    enabled: _editingChannel == null,
 | 
				
			||||||
                    value: null,
 | 
					                    value: null,
 | 
				
			||||||
                    child: Row(
 | 
					                    child: Row(
 | 
				
			||||||
                      children: [
 | 
					                      children: [
 | 
				
			||||||
                        CircleAvatar(
 | 
					                        CircleAvatar(
 | 
				
			||||||
                          radius: 16,
 | 
					                          radius: 16,
 | 
				
			||||||
                          backgroundColor: Colors.transparent,
 | 
					                          backgroundColor: Colors.transparent,
 | 
				
			||||||
                          foregroundColor:
 | 
					                          foregroundColor: Theme.of(context).colorScheme.onSurface,
 | 
				
			||||||
                              Theme.of(context).colorScheme.onSurface,
 | 
					 | 
				
			||||||
                          child: const Icon(Symbols.clear),
 | 
					                          child: const Icon(Symbols.clear),
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                        const Gap(12),
 | 
					                        const Gap(12),
 | 
				
			||||||
@@ -213,9 +213,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
                            mainAxisSize: MainAxisSize.min,
 | 
					                            mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
                            crossAxisAlignment: CrossAxisAlignment.start,
 | 
					                            crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
                            children: [
 | 
					                            children: [
 | 
				
			||||||
                              Text('fieldChatBelongToRealmUnset')
 | 
					                              Text('fieldChatBelongToRealmUnset').tr().textStyle(
 | 
				
			||||||
                                  .tr()
 | 
					 | 
				
			||||||
                                  .textStyle(
 | 
					 | 
				
			||||||
                                    Theme.of(context).textTheme.bodyMedium!,
 | 
					                                    Theme.of(context).textTheme.bodyMedium!,
 | 
				
			||||||
                                  ),
 | 
					                                  ),
 | 
				
			||||||
                            ],
 | 
					                            ],
 | 
				
			||||||
@@ -231,10 +229,10 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
                },
 | 
					                },
 | 
				
			||||||
                buttonStyleData: const ButtonStyleData(
 | 
					                buttonStyleData: const ButtonStyleData(
 | 
				
			||||||
                  padding: EdgeInsets.only(right: 16),
 | 
					                  padding: EdgeInsets.only(right: 16),
 | 
				
			||||||
                  height: 60,
 | 
					                  height: 48,
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                menuItemStyleData: const MenuItemStyleData(
 | 
					                menuItemStyleData: const MenuItemStyleData(
 | 
				
			||||||
                  height: 60,
 | 
					                  height: 48,
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
@@ -250,8 +248,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
                    helperText: 'fieldChatAliasHint'.tr(),
 | 
					                    helperText: 'fieldChatAliasHint'.tr(),
 | 
				
			||||||
                    helperMaxLines: 2,
 | 
					                    helperMaxLines: 2,
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  onTapOutside: (_) =>
 | 
					                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
                      FocusManager.instance.primaryFocus?.unfocus(),
 | 
					 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                const Gap(4),
 | 
					                const Gap(4),
 | 
				
			||||||
                TextField(
 | 
					                TextField(
 | 
				
			||||||
@@ -260,8 +257,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
                    border: const UnderlineInputBorder(),
 | 
					                    border: const UnderlineInputBorder(),
 | 
				
			||||||
                    labelText: 'fieldChatName'.tr(),
 | 
					                    labelText: 'fieldChatName'.tr(),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  onTapOutside: (_) =>
 | 
					                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
                      FocusManager.instance.primaryFocus?.unfocus(),
 | 
					 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                const Gap(4),
 | 
					                const Gap(4),
 | 
				
			||||||
                TextField(
 | 
					                TextField(
 | 
				
			||||||
@@ -272,8 +268,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
                    border: const UnderlineInputBorder(),
 | 
					                    border: const UnderlineInputBorder(),
 | 
				
			||||||
                    labelText: 'fieldChatDescription'.tr(),
 | 
					                    labelText: 'fieldChatDescription'.tr(),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  onTapOutside: (_) =>
 | 
					                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
                      FocusManager.instance.primaryFocus?.unfocus(),
 | 
					 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                const Gap(12),
 | 
					                const Gap(12),
 | 
				
			||||||
                Row(
 | 
					                Row(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -206,7 +206,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final ws = context.read<WebSocketProvider>();
 | 
					    final ws = context.read<WebSocketProvider>();
 | 
				
			||||||
    _wsSubscription = ws.stream.stream.listen((event) {
 | 
					    _wsSubscription = ws.pk.stream.listen((event) {
 | 
				
			||||||
      switch (event.method) {
 | 
					      switch (event.method) {
 | 
				
			||||||
        case 'calls.new':
 | 
					        case 'calls.new':
 | 
				
			||||||
          final payload = SnChatCall.fromJson(event.payload!);
 | 
					          final payload = SnChatCall.fromJson(event.payload!);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,3 @@
 | 
				
			|||||||
import 'package:animations/animations.dart';
 | 
					 | 
				
			||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
 | 
					import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
 | 
				
			||||||
@@ -7,10 +6,8 @@ import 'package:go_router/go_router.dart';
 | 
				
			|||||||
import 'package:material_symbols_icons/symbols.dart';
 | 
					import 'package:material_symbols_icons/symbols.dart';
 | 
				
			||||||
import 'package:provider/provider.dart';
 | 
					import 'package:provider/provider.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
import 'package:surface/providers/config.dart';
 | 
					 | 
				
			||||||
import 'package:surface/providers/post.dart';
 | 
					import 'package:surface/providers/post.dart';
 | 
				
			||||||
import 'package:surface/providers/sn_network.dart';
 | 
					import 'package:surface/providers/sn_network.dart';
 | 
				
			||||||
import 'package:surface/screens/post/post_detail.dart';
 | 
					 | 
				
			||||||
import 'package:surface/types/post.dart';
 | 
					import 'package:surface/types/post.dart';
 | 
				
			||||||
import 'package:surface/widgets/app_bar_leading.dart';
 | 
					import 'package:surface/widgets/app_bar_leading.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
@@ -97,8 +94,6 @@ class _ExploreScreenState extends State<ExploreScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    final cfg = context.read<ConfigProvider>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return AppScaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      floatingActionButtonLocation: ExpandableFab.location,
 | 
					      floatingActionButtonLocation: ExpandableFab.location,
 | 
				
			||||||
      floatingActionButton: ExpandableFab(
 | 
					      floatingActionButton: ExpandableFab(
 | 
				
			||||||
@@ -166,6 +161,48 @@ class _ExploreScreenState extends State<ExploreScreen> {
 | 
				
			|||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
 | 
					          Row(
 | 
				
			||||||
 | 
					            children: [
 | 
				
			||||||
 | 
					              Text('writePostTypeQuestion').tr(),
 | 
				
			||||||
 | 
					              const Gap(20),
 | 
				
			||||||
 | 
					              FloatingActionButton(
 | 
				
			||||||
 | 
					                heroTag: null,
 | 
				
			||||||
 | 
					                tooltip: 'writePostTypeQuestion'.tr(),
 | 
				
			||||||
 | 
					                onPressed: () {
 | 
				
			||||||
 | 
					                  GoRouter.of(context).pushNamed('postEditor', pathParameters: {
 | 
				
			||||||
 | 
					                    'mode': 'questions',
 | 
				
			||||||
 | 
					                  }).then((value) {
 | 
				
			||||||
 | 
					                    if (value == true) {
 | 
				
			||||||
 | 
					                      _refreshPosts();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  });
 | 
				
			||||||
 | 
					                  _fabKey.currentState!.toggle();
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                child: const Icon(Symbols.question_answer),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          Row(
 | 
				
			||||||
 | 
					            children: [
 | 
				
			||||||
 | 
					              Text('writePostTypeVideo').tr(),
 | 
				
			||||||
 | 
					              const Gap(20),
 | 
				
			||||||
 | 
					              FloatingActionButton(
 | 
				
			||||||
 | 
					                heroTag: null,
 | 
				
			||||||
 | 
					                tooltip: 'writePostTypeVideo'.tr(),
 | 
				
			||||||
 | 
					                onPressed: () {
 | 
				
			||||||
 | 
					                  GoRouter.of(context).pushNamed('postEditor', pathParameters: {
 | 
				
			||||||
 | 
					                    'mode': 'videos',
 | 
				
			||||||
 | 
					                  }).then((value) {
 | 
				
			||||||
 | 
					                    if (value == true) {
 | 
				
			||||||
 | 
					                      _refreshPosts();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  });
 | 
				
			||||||
 | 
					                  _fabKey.currentState!.toggle();
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                child: const Icon(Symbols.video_call),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      body: RefreshIndicator(
 | 
					      body: RefreshIndicator(
 | 
				
			||||||
@@ -225,10 +262,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
 | 
				
			|||||||
              onFetchData: _fetchPosts,
 | 
					              onFetchData: _fetchPosts,
 | 
				
			||||||
              itemBuilder: (context, idx) {
 | 
					              itemBuilder: (context, idx) {
 | 
				
			||||||
                return Center(
 | 
					                return Center(
 | 
				
			||||||
                  child: OpenContainer(
 | 
					                  child: OpenablePostItem(
 | 
				
			||||||
                    closedBuilder: (_, __) => Container(
 | 
					 | 
				
			||||||
                      constraints: const BoxConstraints(maxWidth: 640),
 | 
					 | 
				
			||||||
                      child: PostItem(
 | 
					 | 
				
			||||||
                    data: _posts[idx],
 | 
					                    data: _posts[idx],
 | 
				
			||||||
                    maxWidth: 640,
 | 
					                    maxWidth: 640,
 | 
				
			||||||
                    onChanged: (data) {
 | 
					                    onChanged: (data) {
 | 
				
			||||||
@@ -238,22 +272,6 @@ class _ExploreScreenState extends State<ExploreScreen> {
 | 
				
			|||||||
                      _refreshPosts();
 | 
					                      _refreshPosts();
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                    openBuilder: (_, close) => PostDetailScreen(
 | 
					 | 
				
			||||||
                      slug: _posts[idx].id.toString(),
 | 
					 | 
				
			||||||
                      preload: _posts[idx],
 | 
					 | 
				
			||||||
                      onBack: close,
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                    openColor: Colors.transparent,
 | 
					 | 
				
			||||||
                    openElevation: 0,
 | 
					 | 
				
			||||||
                    transitionType: ContainerTransitionType.fade,
 | 
					 | 
				
			||||||
                    closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(
 | 
					 | 
				
			||||||
                          cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
 | 
					 | 
				
			||||||
                        ),
 | 
					 | 
				
			||||||
                    closedShape: const RoundedRectangleBorder(
 | 
					 | 
				
			||||||
                      borderRadius: BorderRadius.all(Radius.circular(16)),
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
              separatorBuilder: (_, __) => const Gap(8),
 | 
					              separatorBuilder: (_, __) => const Gap(8),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -183,7 +183,7 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
 | 
				
			|||||||
            if (_data != null)
 | 
					            if (_data != null)
 | 
				
			||||||
              PostCommentSliverList(
 | 
					              PostCommentSliverList(
 | 
				
			||||||
                key: _childListKey,
 | 
					                key: _childListKey,
 | 
				
			||||||
                parentPostId: _data!.id,
 | 
					                parentPost: _data!,
 | 
				
			||||||
                maxWidth: 640,
 | 
					                maxWidth: 640,
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
 | 
					            SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,32 +1,38 @@
 | 
				
			|||||||
import 'dart:io';
 | 
					import 'dart:io';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:collection/collection.dart';
 | 
					import 'package:collection/collection.dart';
 | 
				
			||||||
import 'package:dropdown_button2/dropdown_button2.dart';
 | 
					 | 
				
			||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/foundation.dart';
 | 
					import 'package:flutter/foundation.dart';
 | 
				
			||||||
import 'package:flutter/gestures.dart';
 | 
					import 'package:flutter/gestures.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter/services.dart';
 | 
					import 'package:flutter/services.dart';
 | 
				
			||||||
 | 
					import 'package:flutter_context_menu/flutter_context_menu.dart';
 | 
				
			||||||
import 'package:gap/gap.dart';
 | 
					import 'package:gap/gap.dart';
 | 
				
			||||||
import 'package:go_router/go_router.dart';
 | 
					 | 
				
			||||||
import 'package:hotkey_manager/hotkey_manager.dart';
 | 
					import 'package:hotkey_manager/hotkey_manager.dart';
 | 
				
			||||||
import 'package:material_symbols_icons/symbols.dart';
 | 
					import 'package:material_symbols_icons/symbols.dart';
 | 
				
			||||||
import 'package:pasteboard/pasteboard.dart';
 | 
					import 'package:pasteboard/pasteboard.dart';
 | 
				
			||||||
 | 
					import 'package:responsive_framework/responsive_framework.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
import 'package:surface/controllers/post_write_controller.dart';
 | 
					import 'package:surface/controllers/post_write_controller.dart';
 | 
				
			||||||
import 'package:surface/providers/config.dart';
 | 
					import 'package:surface/providers/config.dart';
 | 
				
			||||||
 | 
					import 'package:surface/providers/sn_attachment.dart';
 | 
				
			||||||
import 'package:surface/providers/sn_network.dart';
 | 
					import 'package:surface/providers/sn_network.dart';
 | 
				
			||||||
 | 
					import 'package:surface/types/attachment.dart';
 | 
				
			||||||
import 'package:surface/types/post.dart';
 | 
					import 'package:surface/types/post.dart';
 | 
				
			||||||
import 'package:surface/widgets/account/account_image.dart';
 | 
					import 'package:surface/widgets/account/account_image.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/attachment/attachment_item.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/attachment/pending_attachment_alt.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/attachment/pending_attachment_boost.dart';
 | 
				
			||||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
					import 'package:surface/widgets/loading_indicator.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/markdown_content.dart';
 | 
				
			||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
import 'package:surface/widgets/post/post_item.dart';
 | 
					 | 
				
			||||||
import 'package:surface/widgets/post/post_media_pending_list.dart';
 | 
					import 'package:surface/widgets/post/post_media_pending_list.dart';
 | 
				
			||||||
import 'package:surface/widgets/post/post_meta_editor.dart';
 | 
					import 'package:surface/widgets/post/post_meta_editor.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
import 'package:provider/provider.dart';
 | 
					import 'package:provider/provider.dart';
 | 
				
			||||||
 | 
					import 'package:uuid/uuid.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import '../../types/attachment.dart';
 | 
					import '../../widgets/attachment/attachment_input.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PostEditorExtra {
 | 
					class PostEditorExtra {
 | 
				
			||||||
  final String? text;
 | 
					  final String? text;
 | 
				
			||||||
@@ -124,6 +130,16 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _showPublisherPopup() {
 | 
				
			||||||
 | 
					    showModalBottomSheet(
 | 
				
			||||||
 | 
					      context: context,
 | 
				
			||||||
 | 
					      builder: (context) => _PostPublisherPopup(
 | 
				
			||||||
 | 
					        controller: _writeController,
 | 
				
			||||||
 | 
					        publishers: _publishers,
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  void dispose() {
 | 
					  void dispose() {
 | 
				
			||||||
    _writeController.dispose();
 | 
					    _writeController.dispose();
 | 
				
			||||||
@@ -197,174 +213,50 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
 | 
				
			|||||||
          ),
 | 
					          ),
 | 
				
			||||||
          body: Column(
 | 
					          body: Column(
 | 
				
			||||||
            children: [
 | 
					            children: [
 | 
				
			||||||
              DropdownButtonHideUnderline(
 | 
					              if (_writeController.editingPost != null)
 | 
				
			||||||
                child: DropdownButton2<SnPublisher>(
 | 
					                Container(
 | 
				
			||||||
                  isExpanded: true,
 | 
					                  padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20, right: 20),
 | 
				
			||||||
                  hint: Text(
 | 
					                  decoration: BoxDecoration(
 | 
				
			||||||
                    'fieldPostPublisher',
 | 
					                    border: Border(
 | 
				
			||||||
                    style: TextStyle(
 | 
					                      bottom: BorderSide(
 | 
				
			||||||
                      fontSize: 14,
 | 
					                        color: Theme.of(context).dividerColor,
 | 
				
			||||||
                      color: Theme.of(context).hintColor,
 | 
					                        width: 1 / MediaQuery.of(context).devicePixelRatio,
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  ).tr(),
 | 
					 | 
				
			||||||
                  items: <DropdownMenuItem<SnPublisher>>[
 | 
					 | 
				
			||||||
                    ...(_publishers?.map(
 | 
					 | 
				
			||||||
                          (item) => DropdownMenuItem<SnPublisher>(
 | 
					 | 
				
			||||||
                            enabled: _writeController.editingPost == null,
 | 
					 | 
				
			||||||
                            value: item,
 | 
					 | 
				
			||||||
                  child: Row(
 | 
					                  child: Row(
 | 
				
			||||||
 | 
					                    crossAxisAlignment: CrossAxisAlignment.center,
 | 
				
			||||||
                    children: [
 | 
					                    children: [
 | 
				
			||||||
                                AccountImage(content: item.avatar, radius: 16),
 | 
					                      const Icon(Icons.edit, size: 16),
 | 
				
			||||||
                                const Gap(8),
 | 
					                      const Gap(10),
 | 
				
			||||||
                                Expanded(
 | 
					                      Text('postEditingNotice').tr(args: ['@${_writeController.editingPost!.publisher.name}']),
 | 
				
			||||||
                                  child: Column(
 | 
					 | 
				
			||||||
                                    mainAxisSize: MainAxisSize.min,
 | 
					 | 
				
			||||||
                                    crossAxisAlignment: CrossAxisAlignment.start,
 | 
					 | 
				
			||||||
                                    children: [
 | 
					 | 
				
			||||||
                                      Text(item.nick).textStyle(Theme.of(context).textTheme.bodyMedium!),
 | 
					 | 
				
			||||||
                                      Text('@${item.name}')
 | 
					 | 
				
			||||||
                                          .textStyle(Theme.of(context).textTheme.bodySmall!)
 | 
					 | 
				
			||||||
                                          .fontSize(12),
 | 
					 | 
				
			||||||
                    ],
 | 
					                    ],
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                              ],
 | 
					 | 
				
			||||||
                            ),
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                        ) ??
 | 
					 | 
				
			||||||
                        []),
 | 
					 | 
				
			||||||
                    DropdownMenuItem<SnPublisher>(
 | 
					 | 
				
			||||||
                      value: null,
 | 
					 | 
				
			||||||
                      child: Row(
 | 
					 | 
				
			||||||
                        children: [
 | 
					 | 
				
			||||||
                          CircleAvatar(
 | 
					 | 
				
			||||||
                            radius: 16,
 | 
					 | 
				
			||||||
                            backgroundColor: Colors.transparent,
 | 
					 | 
				
			||||||
                            foregroundColor: Theme.of(context).colorScheme.onSurface,
 | 
					 | 
				
			||||||
                            child: const Icon(Symbols.add),
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                          const Gap(8),
 | 
					 | 
				
			||||||
                          Expanded(
 | 
					 | 
				
			||||||
                            child: Column(
 | 
					 | 
				
			||||||
                              mainAxisSize: MainAxisSize.min,
 | 
					 | 
				
			||||||
                              crossAxisAlignment: CrossAxisAlignment.start,
 | 
					 | 
				
			||||||
                              children: [
 | 
					 | 
				
			||||||
                                Text('publishersNew').tr().textStyle(Theme.of(context).textTheme.bodyMedium!),
 | 
					 | 
				
			||||||
                              ],
 | 
					 | 
				
			||||||
                            ),
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                        ],
 | 
					 | 
				
			||||||
                      ),
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                  ],
 | 
					 | 
				
			||||||
                  value: _writeController.publisher,
 | 
					 | 
				
			||||||
                  onChanged: (SnPublisher? value) {
 | 
					 | 
				
			||||||
                    if (value == null) {
 | 
					 | 
				
			||||||
                      GoRouter.of(context).pushNamed('accountPublisherNew').then((value) {
 | 
					 | 
				
			||||||
                        if (value == true) {
 | 
					 | 
				
			||||||
                          _publishers = null;
 | 
					 | 
				
			||||||
                          _fetchPublishers();
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                      });
 | 
					 | 
				
			||||||
                    } else {
 | 
					 | 
				
			||||||
                      _writeController.setPublisher(value);
 | 
					 | 
				
			||||||
                      final config = context.read<ConfigProvider>();
 | 
					 | 
				
			||||||
                      config.prefs.setInt('int_last_publisher_id', value.id);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                  buttonStyleData: const ButtonStyleData(
 | 
					 | 
				
			||||||
                    padding: EdgeInsets.only(right: 16),
 | 
					 | 
				
			||||||
                    height: 48,
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                  menuItemStyleData: const MenuItemStyleData(
 | 
					 | 
				
			||||||
                    height: 48,
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              const Divider(height: 1),
 | 
					 | 
				
			||||||
              Expanded(
 | 
					              Expanded(
 | 
				
			||||||
                child: Stack(
 | 
					                child: Stack(
 | 
				
			||||||
                  children: [
 | 
					                  children: [
 | 
				
			||||||
                    SingleChildScrollView(
 | 
					                    SingleChildScrollView(
 | 
				
			||||||
                      padding: EdgeInsets.only(bottom: 160),
 | 
					                      padding: EdgeInsets.only(bottom: 160),
 | 
				
			||||||
                      child: Column(
 | 
					                      child: switch (_writeController.mode) {
 | 
				
			||||||
                        children: [
 | 
					                        'stories' => _PostStoryEditor(
 | 
				
			||||||
                          // Replying Notice
 | 
					                            controller: _writeController,
 | 
				
			||||||
                          if (_writeController.replyingPost != null)
 | 
					                            onTapPublisher: _showPublisherPopup,
 | 
				
			||||||
                            Column(
 | 
					 | 
				
			||||||
                              children: [
 | 
					 | 
				
			||||||
                                ExpansionTile(
 | 
					 | 
				
			||||||
                                  minTileHeight: 48,
 | 
					 | 
				
			||||||
                                  leading: const Icon(Symbols.reply).padding(left: 4),
 | 
					 | 
				
			||||||
                                  title: Text('postReplyingNotice')
 | 
					 | 
				
			||||||
                                      .fontSize(15)
 | 
					 | 
				
			||||||
                                      .tr(args: ['@${_writeController.replyingPost!.publisher.name}']),
 | 
					 | 
				
			||||||
                                  children: <Widget>[PostItem(data: _writeController.replyingPost!)],
 | 
					 | 
				
			||||||
                          ),
 | 
					                          ),
 | 
				
			||||||
                                const Divider(height: 1),
 | 
					                        'articles' => _PostArticleEditor(
 | 
				
			||||||
                              ],
 | 
					                            controller: _writeController,
 | 
				
			||||||
 | 
					                            onTapPublisher: _showPublisherPopup,
 | 
				
			||||||
                          ),
 | 
					                          ),
 | 
				
			||||||
                          // Reposting Notice
 | 
					                        'questions' => _PostQuestionEditor(
 | 
				
			||||||
                          if (_writeController.repostingPost != null)
 | 
					                            controller: _writeController,
 | 
				
			||||||
                            Column(
 | 
					                            onTapPublisher: _showPublisherPopup,
 | 
				
			||||||
                              children: [
 | 
					 | 
				
			||||||
                                ExpansionTile(
 | 
					 | 
				
			||||||
                                  minTileHeight: 48,
 | 
					 | 
				
			||||||
                                  leading: const Icon(Symbols.forward).padding(left: 4),
 | 
					 | 
				
			||||||
                                  title: Text('postRepostingNotice')
 | 
					 | 
				
			||||||
                                      .fontSize(15)
 | 
					 | 
				
			||||||
                                      .tr(args: ['@${_writeController.repostingPost!.publisher.name}']),
 | 
					 | 
				
			||||||
                                  children: <Widget>[
 | 
					 | 
				
			||||||
                                    PostItem(
 | 
					 | 
				
			||||||
                                      data: _writeController.repostingPost!,
 | 
					 | 
				
			||||||
                                    )
 | 
					 | 
				
			||||||
                                  ],
 | 
					 | 
				
			||||||
                          ),
 | 
					                          ),
 | 
				
			||||||
                                const Divider(height: 1),
 | 
					                        'videos' => _PostVideoEditor(
 | 
				
			||||||
                              ],
 | 
					                            controller: _writeController,
 | 
				
			||||||
                            ),
 | 
					                            onTapPublisher: _showPublisherPopup,
 | 
				
			||||||
                          // Editing Notice
 | 
					 | 
				
			||||||
                          if (_writeController.editingPost != null)
 | 
					 | 
				
			||||||
                            Column(
 | 
					 | 
				
			||||||
                              children: [
 | 
					 | 
				
			||||||
                                ExpansionTile(
 | 
					 | 
				
			||||||
                                  minTileHeight: 48,
 | 
					 | 
				
			||||||
                                  leading: const Icon(Symbols.edit_note).padding(left: 4),
 | 
					 | 
				
			||||||
                                  title: Text('postEditingNotice')
 | 
					 | 
				
			||||||
                                      .fontSize(15)
 | 
					 | 
				
			||||||
                                      .tr(args: ['@${_writeController.editingPost!.publisher.name}']),
 | 
					 | 
				
			||||||
                                  children: <Widget>[PostItem(data: _writeController.editingPost!)],
 | 
					 | 
				
			||||||
                                ),
 | 
					 | 
				
			||||||
                                const Divider(height: 1),
 | 
					 | 
				
			||||||
                              ],
 | 
					 | 
				
			||||||
                            ),
 | 
					 | 
				
			||||||
                          // Content Input Area
 | 
					 | 
				
			||||||
                          Container(
 | 
					 | 
				
			||||||
                            constraints: const BoxConstraints(maxWidth: 640),
 | 
					 | 
				
			||||||
                            child: TextField(
 | 
					 | 
				
			||||||
                              controller: _writeController.contentController,
 | 
					 | 
				
			||||||
                              maxLines: null,
 | 
					 | 
				
			||||||
                              decoration: InputDecoration(
 | 
					 | 
				
			||||||
                                hintText: 'fieldPostContent'.tr(),
 | 
					 | 
				
			||||||
                                hintStyle: TextStyle(fontSize: 14),
 | 
					 | 
				
			||||||
                                isCollapsed: true,
 | 
					 | 
				
			||||||
                                contentPadding: const EdgeInsets.symmetric(
 | 
					 | 
				
			||||||
                                  horizontal: 16,
 | 
					 | 
				
			||||||
                                ),
 | 
					 | 
				
			||||||
                                border: InputBorder.none,
 | 
					 | 
				
			||||||
                              ),
 | 
					 | 
				
			||||||
                              onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
					 | 
				
			||||||
                            ),
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                        ]
 | 
					 | 
				
			||||||
                            .expandIndexed(
 | 
					 | 
				
			||||||
                              (idx, ele) => [
 | 
					 | 
				
			||||||
                                if (idx != 0 || _writeController.isRelatedNull) const Gap(8),
 | 
					 | 
				
			||||||
                                ele,
 | 
					 | 
				
			||||||
                              ],
 | 
					 | 
				
			||||||
                            )
 | 
					 | 
				
			||||||
                            .toList(),
 | 
					 | 
				
			||||||
                          ),
 | 
					                          ),
 | 
				
			||||||
 | 
					                        _ => const Placeholder(),
 | 
				
			||||||
 | 
					                      },
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                    if (_writeController.attachments.isNotEmpty || _writeController.thumbnail != null)
 | 
					                    if (_writeController.attachments.isNotEmpty || _writeController.thumbnail != null)
 | 
				
			||||||
                      Positioned(
 | 
					                      Positioned(
 | 
				
			||||||
@@ -508,3 +400,525 @@ class _PostEditorActionScrollBehavior extends MaterialScrollBehavior {
 | 
				
			|||||||
        PointerDeviceKind.mouse,
 | 
					        PointerDeviceKind.mouse,
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _PostPublisherPopup extends StatelessWidget {
 | 
				
			||||||
 | 
					  final PostWriteController controller;
 | 
				
			||||||
 | 
					  final List<SnPublisher>? publishers;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const _PostPublisherPopup({required this.controller, this.publishers});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return Column(
 | 
				
			||||||
 | 
					      crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					      children: [
 | 
				
			||||||
 | 
					        Row(
 | 
				
			||||||
 | 
					          crossAxisAlignment: CrossAxisAlignment.center,
 | 
				
			||||||
 | 
					          children: [
 | 
				
			||||||
 | 
					            const Icon(Symbols.face, size: 24),
 | 
				
			||||||
 | 
					            const Gap(16),
 | 
				
			||||||
 | 
					            Text('accountPublishers', style: Theme.of(context).textTheme.titleLarge).tr(),
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        ).padding(horizontal: 20, top: 16, bottom: 12),
 | 
				
			||||||
 | 
					        Expanded(
 | 
				
			||||||
 | 
					          child: ListView.builder(
 | 
				
			||||||
 | 
					            itemCount: publishers?.length ?? 0,
 | 
				
			||||||
 | 
					            itemBuilder: (context, idx) {
 | 
				
			||||||
 | 
					              final publisher = publishers![idx];
 | 
				
			||||||
 | 
					              return ListTile(
 | 
				
			||||||
 | 
					                title: Text(publisher.nick),
 | 
				
			||||||
 | 
					                subtitle: Text('@${publisher.name}'),
 | 
				
			||||||
 | 
					                leading: AccountImage(content: publisher.avatar, radius: 18),
 | 
				
			||||||
 | 
					                onTap: () {
 | 
				
			||||||
 | 
					                  controller.setPublisher(publisher);
 | 
				
			||||||
 | 
					                  Navigator.pop(context, true);
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _PostStoryEditor extends StatelessWidget {
 | 
				
			||||||
 | 
					  final PostWriteController controller;
 | 
				
			||||||
 | 
					  final Function? onTapPublisher;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const _PostStoryEditor({required this.controller, this.onTapPublisher});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return Container(
 | 
				
			||||||
 | 
					      padding: const EdgeInsets.symmetric(horizontal: 12),
 | 
				
			||||||
 | 
					      constraints: const BoxConstraints(maxWidth: 640),
 | 
				
			||||||
 | 
					      child: Row(
 | 
				
			||||||
 | 
					        crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					        children: [
 | 
				
			||||||
 | 
					          Material(
 | 
				
			||||||
 | 
					            elevation: 2,
 | 
				
			||||||
 | 
					            borderRadius: const BorderRadius.all(Radius.circular(24)),
 | 
				
			||||||
 | 
					            child: GestureDetector(
 | 
				
			||||||
 | 
					              onTap: () {
 | 
				
			||||||
 | 
					                onTapPublisher?.call();
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              child: AccountImage(
 | 
				
			||||||
 | 
					                content: controller.publisher?.avatar,
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          Expanded(
 | 
				
			||||||
 | 
					            child: Column(
 | 
				
			||||||
 | 
					              children: [
 | 
				
			||||||
 | 
					                const Gap(6),
 | 
				
			||||||
 | 
					                TextField(
 | 
				
			||||||
 | 
					                  controller: controller.titleController,
 | 
				
			||||||
 | 
					                  decoration: InputDecoration.collapsed(
 | 
				
			||||||
 | 
					                    hintText: 'fieldPostTitle'.tr(),
 | 
				
			||||||
 | 
					                    border: InputBorder.none,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  style: Theme.of(context).textTheme.titleLarge,
 | 
				
			||||||
 | 
					                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
 | 
					                ).padding(horizontal: 16),
 | 
				
			||||||
 | 
					                const Gap(8),
 | 
				
			||||||
 | 
					                TextField(
 | 
				
			||||||
 | 
					                  controller: controller.contentController,
 | 
				
			||||||
 | 
					                  maxLines: null,
 | 
				
			||||||
 | 
					                  decoration: InputDecoration(
 | 
				
			||||||
 | 
					                    hintText: 'fieldPostContent'.tr(),
 | 
				
			||||||
 | 
					                    hintStyle: TextStyle(fontSize: 14),
 | 
				
			||||||
 | 
					                    isCollapsed: true,
 | 
				
			||||||
 | 
					                    contentPadding: const EdgeInsets.symmetric(
 | 
				
			||||||
 | 
					                      horizontal: 16,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    border: InputBorder.none,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      ).padding(bottom: 8),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _PostArticleEditor extends StatelessWidget {
 | 
				
			||||||
 | 
					  final PostWriteController controller;
 | 
				
			||||||
 | 
					  final Function? onTapPublisher;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const _PostArticleEditor({required this.controller, this.onTapPublisher});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    final editorWidgets = <Widget>[
 | 
				
			||||||
 | 
					      Material(
 | 
				
			||||||
 | 
					        color: Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
				
			||||||
 | 
					        child: InkWell(
 | 
				
			||||||
 | 
					          child: Row(
 | 
				
			||||||
 | 
					            children: [
 | 
				
			||||||
 | 
					              AccountImage(content: controller.publisher?.avatar, radius: 20),
 | 
				
			||||||
 | 
					              const Gap(8),
 | 
				
			||||||
 | 
					              Expanded(
 | 
				
			||||||
 | 
					                child: Column(
 | 
				
			||||||
 | 
					                  crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					                  children: [
 | 
				
			||||||
 | 
					                    Text(controller.publisher?.nick ?? 'loading'.tr()).bold(),
 | 
				
			||||||
 | 
					                    Text('@${controller.publisher?.name}'),
 | 
				
			||||||
 | 
					                  ],
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					          ).padding(horizontal: 12, vertical: 8),
 | 
				
			||||||
 | 
					          onTap: () {
 | 
				
			||||||
 | 
					            onTapPublisher?.call();
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      const Gap(16),
 | 
				
			||||||
 | 
					      TextField(
 | 
				
			||||||
 | 
					        controller: controller.titleController,
 | 
				
			||||||
 | 
					        decoration: InputDecoration.collapsed(
 | 
				
			||||||
 | 
					          hintText: 'fieldPostTitle'.tr(),
 | 
				
			||||||
 | 
					          border: InputBorder.none,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        style: Theme.of(context).textTheme.titleLarge,
 | 
				
			||||||
 | 
					        onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
 | 
					      ).padding(horizontal: 16),
 | 
				
			||||||
 | 
					      const Gap(8),
 | 
				
			||||||
 | 
					      TextField(
 | 
				
			||||||
 | 
					        controller: controller.descriptionController,
 | 
				
			||||||
 | 
					        decoration: InputDecoration.collapsed(
 | 
				
			||||||
 | 
					          hintText: 'fieldPostDescription'.tr(),
 | 
				
			||||||
 | 
					          border: InputBorder.none,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        maxLines: null,
 | 
				
			||||||
 | 
					        keyboardType: TextInputType.multiline,
 | 
				
			||||||
 | 
					        style: Theme.of(context).textTheme.bodyLarge,
 | 
				
			||||||
 | 
					        onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
 | 
					      ).padding(horizontal: 16),
 | 
				
			||||||
 | 
					      const Gap(4),
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (ResponsiveBreakpoints.of(context).largerThan(MOBILE)) {
 | 
				
			||||||
 | 
					      return Container(
 | 
				
			||||||
 | 
					        constraints: const BoxConstraints(maxWidth: 640 * 2 + 8),
 | 
				
			||||||
 | 
					        child: Column(
 | 
				
			||||||
 | 
					          children: [
 | 
				
			||||||
 | 
					            ...editorWidgets,
 | 
				
			||||||
 | 
					            Row(
 | 
				
			||||||
 | 
					              crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					              children: [
 | 
				
			||||||
 | 
					                Expanded(
 | 
				
			||||||
 | 
					                  child: TextField(
 | 
				
			||||||
 | 
					                    controller: controller.contentController,
 | 
				
			||||||
 | 
					                    maxLines: null,
 | 
				
			||||||
 | 
					                    decoration: InputDecoration(
 | 
				
			||||||
 | 
					                      hintText: 'fieldPostContent'.tr(),
 | 
				
			||||||
 | 
					                      hintStyle: TextStyle(fontSize: 14),
 | 
				
			||||||
 | 
					                      isCollapsed: true,
 | 
				
			||||||
 | 
					                      contentPadding: const EdgeInsets.symmetric(
 | 
				
			||||||
 | 
					                        horizontal: 16,
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                      border: InputBorder.none,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                const Gap(8),
 | 
				
			||||||
 | 
					                Expanded(
 | 
				
			||||||
 | 
					                  child: MarkdownTextContent(
 | 
				
			||||||
 | 
					                    content: controller.contentController.text,
 | 
				
			||||||
 | 
					                  ).padding(horizontal: 24),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Column(
 | 
				
			||||||
 | 
					      children: [
 | 
				
			||||||
 | 
					        ...editorWidgets,
 | 
				
			||||||
 | 
					        Container(
 | 
				
			||||||
 | 
					          padding: const EdgeInsets.only(top: 8),
 | 
				
			||||||
 | 
					          constraints: const BoxConstraints(maxWidth: 640),
 | 
				
			||||||
 | 
					          child: TextField(
 | 
				
			||||||
 | 
					            controller: controller.contentController,
 | 
				
			||||||
 | 
					            maxLines: null,
 | 
				
			||||||
 | 
					            decoration: InputDecoration(
 | 
				
			||||||
 | 
					              hintText: 'fieldPostContent'.tr(),
 | 
				
			||||||
 | 
					              hintStyle: TextStyle(fontSize: 14),
 | 
				
			||||||
 | 
					              isCollapsed: true,
 | 
				
			||||||
 | 
					              contentPadding: const EdgeInsets.symmetric(
 | 
				
			||||||
 | 
					                horizontal: 16,
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              border: InputBorder.none,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _PostQuestionEditor extends StatelessWidget {
 | 
				
			||||||
 | 
					  final PostWriteController controller;
 | 
				
			||||||
 | 
					  final Function? onTapPublisher;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const _PostQuestionEditor({required this.controller, this.onTapPublisher});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return Container(
 | 
				
			||||||
 | 
					      padding: const EdgeInsets.symmetric(horizontal: 12),
 | 
				
			||||||
 | 
					      constraints: const BoxConstraints(maxWidth: 640),
 | 
				
			||||||
 | 
					      child: Row(
 | 
				
			||||||
 | 
					        crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					        children: [
 | 
				
			||||||
 | 
					          Material(
 | 
				
			||||||
 | 
					            elevation: 1,
 | 
				
			||||||
 | 
					            borderRadius: const BorderRadius.all(Radius.circular(24)),
 | 
				
			||||||
 | 
					            child: GestureDetector(
 | 
				
			||||||
 | 
					              onTap: () {
 | 
				
			||||||
 | 
					                onTapPublisher?.call();
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              child: AccountImage(
 | 
				
			||||||
 | 
					                content: controller.publisher?.avatar,
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          Expanded(
 | 
				
			||||||
 | 
					            child: Column(
 | 
				
			||||||
 | 
					              children: [
 | 
				
			||||||
 | 
					                const Gap(6),
 | 
				
			||||||
 | 
					                TextField(
 | 
				
			||||||
 | 
					                  controller: controller.titleController,
 | 
				
			||||||
 | 
					                  decoration: InputDecoration.collapsed(
 | 
				
			||||||
 | 
					                    hintText: 'fieldPostTitle'.tr(),
 | 
				
			||||||
 | 
					                    border: InputBorder.none,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  style: Theme.of(context).textTheme.titleLarge,
 | 
				
			||||||
 | 
					                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
 | 
					                ).padding(horizontal: 16),
 | 
				
			||||||
 | 
					                const Gap(8),
 | 
				
			||||||
 | 
					                TextField(
 | 
				
			||||||
 | 
					                  controller: controller.rewardController,
 | 
				
			||||||
 | 
					                  decoration: InputDecoration(
 | 
				
			||||||
 | 
					                    hintText: 'fieldPostQuestionReward'.tr(),
 | 
				
			||||||
 | 
					                    suffixText: 'walletCurrencyShort'.tr(),
 | 
				
			||||||
 | 
					                    border: InputBorder.none,
 | 
				
			||||||
 | 
					                    isCollapsed: true,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
 | 
					                ).padding(horizontal: 16),
 | 
				
			||||||
 | 
					                const Gap(8),
 | 
				
			||||||
 | 
					                TextField(
 | 
				
			||||||
 | 
					                  controller: controller.contentController,
 | 
				
			||||||
 | 
					                  maxLines: null,
 | 
				
			||||||
 | 
					                  decoration: InputDecoration(
 | 
				
			||||||
 | 
					                    hintText: 'fieldPostContent'.tr(),
 | 
				
			||||||
 | 
					                    hintStyle: TextStyle(fontSize: 14),
 | 
				
			||||||
 | 
					                    isCollapsed: true,
 | 
				
			||||||
 | 
					                    contentPadding: const EdgeInsets.symmetric(
 | 
				
			||||||
 | 
					                      horizontal: 16,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    border: InputBorder.none,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      ).padding(top: 8),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _PostVideoEditor extends StatelessWidget {
 | 
				
			||||||
 | 
					  final PostWriteController controller;
 | 
				
			||||||
 | 
					  final Function? onTapPublisher;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const _PostVideoEditor({required this.controller, this.onTapPublisher});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _selectVideo(BuildContext context) async {
 | 
				
			||||||
 | 
					    final video = await showDialog<SnAttachment?>(
 | 
				
			||||||
 | 
					      context: context,
 | 
				
			||||||
 | 
					      builder: (context) => AttachmentInputDialog(
 | 
				
			||||||
 | 
					        title: 'postVideoUpload'.tr(),
 | 
				
			||||||
 | 
					        pool: 'interactive',
 | 
				
			||||||
 | 
					        mediaType: SnMediaType.video,
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    if (!context.mounted) return;
 | 
				
			||||||
 | 
					    if (video == null) return;
 | 
				
			||||||
 | 
					    controller.setVideoAttachment(video);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _setAlt(BuildContext context) async {
 | 
				
			||||||
 | 
					    if (controller.videoAttachment == null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final result = await showDialog<SnAttachment?>(
 | 
				
			||||||
 | 
					      context: context,
 | 
				
			||||||
 | 
					      builder: (context) => PendingAttachmentAltDialog(media: PostWriteMedia(controller.videoAttachment)),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    if (result == null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    controller.setVideoAttachment(result);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _createBoost(BuildContext context) async {
 | 
				
			||||||
 | 
					    if (controller.videoAttachment == null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final result = await showDialog<SnAttachmentBoost?>(
 | 
				
			||||||
 | 
					      context: context,
 | 
				
			||||||
 | 
					      builder: (context) => PendingAttachmentBoostDialog(media: PostWriteMedia(controller.videoAttachment)),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    if (result == null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final newAttach = controller.videoAttachment!.copyWith(
 | 
				
			||||||
 | 
					      boosts: [...controller.videoAttachment!.boosts, result],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    controller.setVideoAttachment(newAttach);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _setThumbnail(BuildContext context) async {
 | 
				
			||||||
 | 
					    if (controller.videoAttachment == null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final thumbnail = await showDialog<SnAttachment?>(
 | 
				
			||||||
 | 
					      context: context,
 | 
				
			||||||
 | 
					      builder: (context) => AttachmentInputDialog(
 | 
				
			||||||
 | 
					        title: 'attachmentSetThumbnail'.tr(),
 | 
				
			||||||
 | 
					        pool: 'interactive',
 | 
				
			||||||
 | 
					        analyzeNow: true,
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    if (thumbnail == null) return;
 | 
				
			||||||
 | 
					    if (!context.mounted) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final attach = context.read<SnAttachmentProvider>();
 | 
				
			||||||
 | 
					      final newAttach = await attach.updateOne(
 | 
				
			||||||
 | 
					        controller.videoAttachment!,
 | 
				
			||||||
 | 
					        thumbnailId: thumbnail.id,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      controller.setVideoAttachment(newAttach);
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      if (!context.mounted) return;
 | 
				
			||||||
 | 
					      context.showErrorDialog(err);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _deleteAttachment(BuildContext context) async {
 | 
				
			||||||
 | 
					    if (controller.videoAttachment == null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					      await sn.client.delete('/cgi/uc/attachments/${controller.videoAttachment!.id}');
 | 
				
			||||||
 | 
					      controller.setVideoAttachment(null);
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      if (!context.mounted) return;
 | 
				
			||||||
 | 
					      context.showErrorDialog(err);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return Column(
 | 
				
			||||||
 | 
					      children: [
 | 
				
			||||||
 | 
					        Material(
 | 
				
			||||||
 | 
					          color: Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
				
			||||||
 | 
					          child: InkWell(
 | 
				
			||||||
 | 
					            child: Row(
 | 
				
			||||||
 | 
					              children: [
 | 
				
			||||||
 | 
					                AccountImage(content: controller.publisher?.avatar, radius: 20),
 | 
				
			||||||
 | 
					                const Gap(8),
 | 
				
			||||||
 | 
					                Expanded(
 | 
				
			||||||
 | 
					                  child: Column(
 | 
				
			||||||
 | 
					                    crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					                    children: [
 | 
				
			||||||
 | 
					                      Text(controller.publisher?.nick ?? 'loading'.tr()).bold(),
 | 
				
			||||||
 | 
					                      Text('@${controller.publisher?.name}'),
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
 | 
					            ).padding(horizontal: 12, vertical: 8),
 | 
				
			||||||
 | 
					            onTap: () {
 | 
				
			||||||
 | 
					              onTapPublisher?.call();
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        const Gap(16),
 | 
				
			||||||
 | 
					        TextField(
 | 
				
			||||||
 | 
					          controller: controller.titleController,
 | 
				
			||||||
 | 
					          decoration: InputDecoration.collapsed(
 | 
				
			||||||
 | 
					            hintText: 'fieldPostTitle'.tr(),
 | 
				
			||||||
 | 
					            border: InputBorder.none,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          style: Theme.of(context).textTheme.titleLarge,
 | 
				
			||||||
 | 
					          onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
 | 
					        ).padding(horizontal: 16),
 | 
				
			||||||
 | 
					        const Gap(8),
 | 
				
			||||||
 | 
					        TextField(
 | 
				
			||||||
 | 
					          controller: controller.descriptionController,
 | 
				
			||||||
 | 
					          decoration: InputDecoration.collapsed(
 | 
				
			||||||
 | 
					            hintText: 'fieldPostDescription'.tr(),
 | 
				
			||||||
 | 
					            border: InputBorder.none,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          maxLines: null,
 | 
				
			||||||
 | 
					          keyboardType: TextInputType.multiline,
 | 
				
			||||||
 | 
					          style: Theme.of(context).textTheme.bodyLarge,
 | 
				
			||||||
 | 
					          onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
 | 
					        ).padding(horizontal: 16),
 | 
				
			||||||
 | 
					        const Gap(12),
 | 
				
			||||||
 | 
					        Container(
 | 
				
			||||||
 | 
					          margin: const EdgeInsets.only(left: 16, right: 16),
 | 
				
			||||||
 | 
					          decoration: BoxDecoration(
 | 
				
			||||||
 | 
					            borderRadius: BorderRadius.circular(16),
 | 
				
			||||||
 | 
					            border: Border.all(color: Theme.of(context).dividerColor),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          child: ContextMenuRegion(
 | 
				
			||||||
 | 
					            contextMenu: ContextMenu(
 | 
				
			||||||
 | 
					              entries: [
 | 
				
			||||||
 | 
					                MenuItem(
 | 
				
			||||||
 | 
					                  label: 'attachmentSetAlt'.tr(),
 | 
				
			||||||
 | 
					                  icon: Symbols.description,
 | 
				
			||||||
 | 
					                  onSelected: () {
 | 
				
			||||||
 | 
					                    _setAlt(context);
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                MenuItem(
 | 
				
			||||||
 | 
					                  label: 'attachmentBoost'.tr(),
 | 
				
			||||||
 | 
					                  icon: Symbols.bolt,
 | 
				
			||||||
 | 
					                  onSelected: () {
 | 
				
			||||||
 | 
					                    _createBoost(context);
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                MenuItem(
 | 
				
			||||||
 | 
					                  label: 'attachmentSetThumbnail'.tr(),
 | 
				
			||||||
 | 
					                  icon: Symbols.image,
 | 
				
			||||||
 | 
					                  onSelected: () {
 | 
				
			||||||
 | 
					                    _setThumbnail(context);
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                MenuItem(
 | 
				
			||||||
 | 
					                  label: 'attachmentCopyRandomId'.tr(),
 | 
				
			||||||
 | 
					                  icon: Symbols.content_copy,
 | 
				
			||||||
 | 
					                  onSelected: () {
 | 
				
			||||||
 | 
					                    Clipboard.setData(ClipboardData(text: controller.videoAttachment!.rid));
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                MenuItem(
 | 
				
			||||||
 | 
					                  label: 'delete'.tr(),
 | 
				
			||||||
 | 
					                  icon: Symbols.delete,
 | 
				
			||||||
 | 
					                  onSelected: () => _deleteAttachment(context),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                MenuItem(
 | 
				
			||||||
 | 
					                  label: 'unlink'.tr(),
 | 
				
			||||||
 | 
					                  icon: Symbols.link_off,
 | 
				
			||||||
 | 
					                  onSelected: () {
 | 
				
			||||||
 | 
					                    controller.setVideoAttachment(null);
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            child: InkWell(
 | 
				
			||||||
 | 
					              borderRadius: BorderRadius.circular(16),
 | 
				
			||||||
 | 
					              onTap: controller.videoAttachment != null ? () => _selectVideo(context) : null,
 | 
				
			||||||
 | 
					              child: AspectRatio(
 | 
				
			||||||
 | 
					                aspectRatio: 16 / 9,
 | 
				
			||||||
 | 
					                child: controller.videoAttachment == null
 | 
				
			||||||
 | 
					                    ? Center(
 | 
				
			||||||
 | 
					                        child: Row(
 | 
				
			||||||
 | 
					                          mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
 | 
					                          crossAxisAlignment: CrossAxisAlignment.center,
 | 
				
			||||||
 | 
					                          mainAxisAlignment: MainAxisAlignment.center,
 | 
				
			||||||
 | 
					                          children: [
 | 
				
			||||||
 | 
					                            const Icon(Icons.add),
 | 
				
			||||||
 | 
					                            const Gap(4),
 | 
				
			||||||
 | 
					                            Text('postVideoUpload'.tr()),
 | 
				
			||||||
 | 
					                          ],
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      )
 | 
				
			||||||
 | 
					                    : ClipRRect(
 | 
				
			||||||
 | 
					                        borderRadius: BorderRadius.circular(16),
 | 
				
			||||||
 | 
					                        child: AttachmentItem(
 | 
				
			||||||
 | 
					                          data: controller.videoAttachment!,
 | 
				
			||||||
 | 
					                          heroTag: const Uuid().v4(),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,6 @@
 | 
				
			|||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:gap/gap.dart';
 | 
					import 'package:gap/gap.dart';
 | 
				
			||||||
import 'package:go_router/go_router.dart';
 | 
					 | 
				
			||||||
import 'package:material_symbols_icons/symbols.dart';
 | 
					import 'package:material_symbols_icons/symbols.dart';
 | 
				
			||||||
import 'package:provider/provider.dart';
 | 
					import 'package:provider/provider.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
@@ -134,7 +133,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
 | 
				
			|||||||
      body: Stack(
 | 
					      body: Stack(
 | 
				
			||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          InfiniteList(
 | 
					          InfiniteList(
 | 
				
			||||||
            padding: const EdgeInsets.only(top: 100),
 | 
					            padding: const EdgeInsets.only(top: 100 + 8),
 | 
				
			||||||
            itemCount: _posts.length,
 | 
					            itemCount: _posts.length,
 | 
				
			||||||
            isLoading: _isBusy,
 | 
					            isLoading: _isBusy,
 | 
				
			||||||
            hasReachedMax: _postCount != null && _posts.length >= _postCount!,
 | 
					            hasReachedMax: _postCount != null && _posts.length >= _postCount!,
 | 
				
			||||||
@@ -142,8 +141,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
 | 
				
			|||||||
              _fetchPosts();
 | 
					              _fetchPosts();
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            itemBuilder: (context, idx) {
 | 
					            itemBuilder: (context, idx) {
 | 
				
			||||||
              return GestureDetector(
 | 
					              return OpenablePostItem(
 | 
				
			||||||
                child: PostItem(
 | 
					 | 
				
			||||||
                data: _posts[idx],
 | 
					                data: _posts[idx],
 | 
				
			||||||
                maxWidth: 640,
 | 
					                maxWidth: 640,
 | 
				
			||||||
                onChanged: (data) {
 | 
					                onChanged: (data) {
 | 
				
			||||||
@@ -152,17 +150,9 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
 | 
				
			|||||||
                onDeleted: () {
 | 
					                onDeleted: () {
 | 
				
			||||||
                  _refreshPosts();
 | 
					                  _refreshPosts();
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
                onTap: () {
 | 
					 | 
				
			||||||
                  GoRouter.of(context).pushNamed(
 | 
					 | 
				
			||||||
                    'postDetail',
 | 
					 | 
				
			||||||
                    pathParameters: {'slug': _posts[idx].id.toString()},
 | 
					 | 
				
			||||||
                    extra: _posts[idx],
 | 
					 | 
				
			||||||
              );
 | 
					              );
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
              );
 | 
					            separatorBuilder: (_, __) => const Gap(8),
 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            separatorBuilder: (context, index) => const Divider(height: 1),
 | 
					 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          Positioned(
 | 
					          Positioned(
 | 
				
			||||||
            top: 16,
 | 
					            top: 16,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -597,25 +597,16 @@ class _PublisherPostList extends StatelessWidget {
 | 
				
			|||||||
      hasReachedMax: postCount != null && posts.length >= postCount!,
 | 
					      hasReachedMax: postCount != null && posts.length >= postCount!,
 | 
				
			||||||
      onFetchData: fetchPosts,
 | 
					      onFetchData: fetchPosts,
 | 
				
			||||||
      itemBuilder: (context, idx) {
 | 
					      itemBuilder: (context, idx) {
 | 
				
			||||||
        return GestureDetector(
 | 
					        return OpenablePostItem(
 | 
				
			||||||
          child: PostItem(
 | 
					 | 
				
			||||||
          data: posts[idx],
 | 
					          data: posts[idx],
 | 
				
			||||||
          maxWidth: 640,
 | 
					          maxWidth: 640,
 | 
				
			||||||
          onChanged: (data) {
 | 
					          onChanged: (data) {
 | 
				
			||||||
            onChanged(idx, data);
 | 
					            onChanged(idx, data);
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          onDeleted: onDeleted,
 | 
					          onDeleted: onDeleted,
 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
          onTap: () {
 | 
					 | 
				
			||||||
            GoRouter.of(context).pushNamed(
 | 
					 | 
				
			||||||
              'postDetail',
 | 
					 | 
				
			||||||
              pathParameters: {'slug': posts[idx].id.toString()},
 | 
					 | 
				
			||||||
              extra: posts[idx],
 | 
					 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
        );
 | 
					      separatorBuilder: (_, __) => const Gap(8),
 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      separatorBuilder: (context, index) => const Divider(height: 1),
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -89,6 +89,7 @@ class SnPostPreload with _$SnPostPreload {
 | 
				
			|||||||
  const factory SnPostPreload({
 | 
					  const factory SnPostPreload({
 | 
				
			||||||
    required SnAttachment? thumbnail,
 | 
					    required SnAttachment? thumbnail,
 | 
				
			||||||
    required List<SnAttachment?>? attachments,
 | 
					    required List<SnAttachment?>? attachments,
 | 
				
			||||||
 | 
					    required SnAttachment? video,
 | 
				
			||||||
  }) = _SnPostPreload;
 | 
					  }) = _SnPostPreload;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory SnPostPreload.fromJson(Map<String, Object?> json) =>
 | 
					  factory SnPostPreload.fromJson(Map<String, Object?> json) =>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1567,6 +1567,7 @@ SnPostPreload _$SnPostPreloadFromJson(Map<String, dynamic> json) {
 | 
				
			|||||||
mixin _$SnPostPreload {
 | 
					mixin _$SnPostPreload {
 | 
				
			||||||
  SnAttachment? get thumbnail => throw _privateConstructorUsedError;
 | 
					  SnAttachment? get thumbnail => throw _privateConstructorUsedError;
 | 
				
			||||||
  List<SnAttachment?>? get attachments => throw _privateConstructorUsedError;
 | 
					  List<SnAttachment?>? get attachments => throw _privateConstructorUsedError;
 | 
				
			||||||
 | 
					  SnAttachment? get video => throw _privateConstructorUsedError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Serializes this SnPostPreload to a JSON map.
 | 
					  /// Serializes this SnPostPreload to a JSON map.
 | 
				
			||||||
  Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
 | 
					  Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
 | 
				
			||||||
@@ -1584,9 +1585,13 @@ abstract class $SnPostPreloadCopyWith<$Res> {
 | 
				
			|||||||
          SnPostPreload value, $Res Function(SnPostPreload) then) =
 | 
					          SnPostPreload value, $Res Function(SnPostPreload) then) =
 | 
				
			||||||
      _$SnPostPreloadCopyWithImpl<$Res, SnPostPreload>;
 | 
					      _$SnPostPreloadCopyWithImpl<$Res, SnPostPreload>;
 | 
				
			||||||
  @useResult
 | 
					  @useResult
 | 
				
			||||||
  $Res call({SnAttachment? thumbnail, List<SnAttachment?>? attachments});
 | 
					  $Res call(
 | 
				
			||||||
 | 
					      {SnAttachment? thumbnail,
 | 
				
			||||||
 | 
					      List<SnAttachment?>? attachments,
 | 
				
			||||||
 | 
					      SnAttachment? video});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  $SnAttachmentCopyWith<$Res>? get thumbnail;
 | 
					  $SnAttachmentCopyWith<$Res>? get thumbnail;
 | 
				
			||||||
 | 
					  $SnAttachmentCopyWith<$Res>? get video;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// @nodoc
 | 
					/// @nodoc
 | 
				
			||||||
@@ -1606,6 +1611,7 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload>
 | 
				
			|||||||
  $Res call({
 | 
					  $Res call({
 | 
				
			||||||
    Object? thumbnail = freezed,
 | 
					    Object? thumbnail = freezed,
 | 
				
			||||||
    Object? attachments = freezed,
 | 
					    Object? attachments = freezed,
 | 
				
			||||||
 | 
					    Object? video = freezed,
 | 
				
			||||||
  }) {
 | 
					  }) {
 | 
				
			||||||
    return _then(_value.copyWith(
 | 
					    return _then(_value.copyWith(
 | 
				
			||||||
      thumbnail: freezed == thumbnail
 | 
					      thumbnail: freezed == thumbnail
 | 
				
			||||||
@@ -1616,6 +1622,10 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload>
 | 
				
			|||||||
          ? _value.attachments
 | 
					          ? _value.attachments
 | 
				
			||||||
          : attachments // ignore: cast_nullable_to_non_nullable
 | 
					          : attachments // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
              as List<SnAttachment?>?,
 | 
					              as List<SnAttachment?>?,
 | 
				
			||||||
 | 
					      video: freezed == video
 | 
				
			||||||
 | 
					          ? _value.video
 | 
				
			||||||
 | 
					          : video // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					              as SnAttachment?,
 | 
				
			||||||
    ) as $Val);
 | 
					    ) as $Val);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1632,6 +1642,20 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload>
 | 
				
			|||||||
      return _then(_value.copyWith(thumbnail: value) as $Val);
 | 
					      return _then(_value.copyWith(thumbnail: value) as $Val);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Create a copy of SnPostPreload
 | 
				
			||||||
 | 
					  /// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  @pragma('vm:prefer-inline')
 | 
				
			||||||
 | 
					  $SnAttachmentCopyWith<$Res>? get video {
 | 
				
			||||||
 | 
					    if (_value.video == null) {
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return $SnAttachmentCopyWith<$Res>(_value.video!, (value) {
 | 
				
			||||||
 | 
					      return _then(_value.copyWith(video: value) as $Val);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// @nodoc
 | 
					/// @nodoc
 | 
				
			||||||
@@ -1642,10 +1666,15 @@ abstract class _$$SnPostPreloadImplCopyWith<$Res>
 | 
				
			|||||||
      __$$SnPostPreloadImplCopyWithImpl<$Res>;
 | 
					      __$$SnPostPreloadImplCopyWithImpl<$Res>;
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  @useResult
 | 
					  @useResult
 | 
				
			||||||
  $Res call({SnAttachment? thumbnail, List<SnAttachment?>? attachments});
 | 
					  $Res call(
 | 
				
			||||||
 | 
					      {SnAttachment? thumbnail,
 | 
				
			||||||
 | 
					      List<SnAttachment?>? attachments,
 | 
				
			||||||
 | 
					      SnAttachment? video});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  $SnAttachmentCopyWith<$Res>? get thumbnail;
 | 
					  $SnAttachmentCopyWith<$Res>? get thumbnail;
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  $SnAttachmentCopyWith<$Res>? get video;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// @nodoc
 | 
					/// @nodoc
 | 
				
			||||||
@@ -1663,6 +1692,7 @@ class __$$SnPostPreloadImplCopyWithImpl<$Res>
 | 
				
			|||||||
  $Res call({
 | 
					  $Res call({
 | 
				
			||||||
    Object? thumbnail = freezed,
 | 
					    Object? thumbnail = freezed,
 | 
				
			||||||
    Object? attachments = freezed,
 | 
					    Object? attachments = freezed,
 | 
				
			||||||
 | 
					    Object? video = freezed,
 | 
				
			||||||
  }) {
 | 
					  }) {
 | 
				
			||||||
    return _then(_$SnPostPreloadImpl(
 | 
					    return _then(_$SnPostPreloadImpl(
 | 
				
			||||||
      thumbnail: freezed == thumbnail
 | 
					      thumbnail: freezed == thumbnail
 | 
				
			||||||
@@ -1673,6 +1703,10 @@ class __$$SnPostPreloadImplCopyWithImpl<$Res>
 | 
				
			|||||||
          ? _value._attachments
 | 
					          ? _value._attachments
 | 
				
			||||||
          : attachments // ignore: cast_nullable_to_non_nullable
 | 
					          : attachments // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
              as List<SnAttachment?>?,
 | 
					              as List<SnAttachment?>?,
 | 
				
			||||||
 | 
					      video: freezed == video
 | 
				
			||||||
 | 
					          ? _value.video
 | 
				
			||||||
 | 
					          : video // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					              as SnAttachment?,
 | 
				
			||||||
    ));
 | 
					    ));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1682,7 +1716,8 @@ class __$$SnPostPreloadImplCopyWithImpl<$Res>
 | 
				
			|||||||
class _$SnPostPreloadImpl implements _SnPostPreload {
 | 
					class _$SnPostPreloadImpl implements _SnPostPreload {
 | 
				
			||||||
  const _$SnPostPreloadImpl(
 | 
					  const _$SnPostPreloadImpl(
 | 
				
			||||||
      {required this.thumbnail,
 | 
					      {required this.thumbnail,
 | 
				
			||||||
      required final List<SnAttachment?>? attachments})
 | 
					      required final List<SnAttachment?>? attachments,
 | 
				
			||||||
 | 
					      required this.video})
 | 
				
			||||||
      : _attachments = attachments;
 | 
					      : _attachments = attachments;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory _$SnPostPreloadImpl.fromJson(Map<String, dynamic> json) =>
 | 
					  factory _$SnPostPreloadImpl.fromJson(Map<String, dynamic> json) =>
 | 
				
			||||||
@@ -1700,9 +1735,12 @@ class _$SnPostPreloadImpl implements _SnPostPreload {
 | 
				
			|||||||
    return EqualUnmodifiableListView(value);
 | 
					    return EqualUnmodifiableListView(value);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  final SnAttachment? video;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() {
 | 
					  String toString() {
 | 
				
			||||||
    return 'SnPostPreload(thumbnail: $thumbnail, attachments: $attachments)';
 | 
					    return 'SnPostPreload(thumbnail: $thumbnail, attachments: $attachments, video: $video)';
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@@ -1713,13 +1751,14 @@ class _$SnPostPreloadImpl implements _SnPostPreload {
 | 
				
			|||||||
            (identical(other.thumbnail, thumbnail) ||
 | 
					            (identical(other.thumbnail, thumbnail) ||
 | 
				
			||||||
                other.thumbnail == thumbnail) &&
 | 
					                other.thumbnail == thumbnail) &&
 | 
				
			||||||
            const DeepCollectionEquality()
 | 
					            const DeepCollectionEquality()
 | 
				
			||||||
                .equals(other._attachments, _attachments));
 | 
					                .equals(other._attachments, _attachments) &&
 | 
				
			||||||
 | 
					            (identical(other.video, video) || other.video == video));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @JsonKey(includeFromJson: false, includeToJson: false)
 | 
					  @JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  int get hashCode => Object.hash(runtimeType, thumbnail,
 | 
					  int get hashCode => Object.hash(runtimeType, thumbnail,
 | 
				
			||||||
      const DeepCollectionEquality().hash(_attachments));
 | 
					      const DeepCollectionEquality().hash(_attachments), video);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Create a copy of SnPostPreload
 | 
					  /// Create a copy of SnPostPreload
 | 
				
			||||||
  /// with the given fields replaced by the non-null parameter values.
 | 
					  /// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
@@ -1740,7 +1779,8 @@ class _$SnPostPreloadImpl implements _SnPostPreload {
 | 
				
			|||||||
abstract class _SnPostPreload implements SnPostPreload {
 | 
					abstract class _SnPostPreload implements SnPostPreload {
 | 
				
			||||||
  const factory _SnPostPreload(
 | 
					  const factory _SnPostPreload(
 | 
				
			||||||
      {required final SnAttachment? thumbnail,
 | 
					      {required final SnAttachment? thumbnail,
 | 
				
			||||||
      required final List<SnAttachment?>? attachments}) = _$SnPostPreloadImpl;
 | 
					      required final List<SnAttachment?>? attachments,
 | 
				
			||||||
 | 
					      required final SnAttachment? video}) = _$SnPostPreloadImpl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory _SnPostPreload.fromJson(Map<String, dynamic> json) =
 | 
					  factory _SnPostPreload.fromJson(Map<String, dynamic> json) =
 | 
				
			||||||
      _$SnPostPreloadImpl.fromJson;
 | 
					      _$SnPostPreloadImpl.fromJson;
 | 
				
			||||||
@@ -1749,6 +1789,8 @@ abstract class _SnPostPreload implements SnPostPreload {
 | 
				
			|||||||
  SnAttachment? get thumbnail;
 | 
					  SnAttachment? get thumbnail;
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  List<SnAttachment?>? get attachments;
 | 
					  List<SnAttachment?>? get attachments;
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  SnAttachment? get video;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Create a copy of SnPostPreload
 | 
					  /// Create a copy of SnPostPreload
 | 
				
			||||||
  /// with the given fields replaced by the non-null parameter values.
 | 
					  /// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -165,12 +165,16 @@ _$SnPostPreloadImpl _$$SnPostPreloadImplFromJson(Map<String, dynamic> json) =>
 | 
				
			|||||||
              ? null
 | 
					              ? null
 | 
				
			||||||
              : SnAttachment.fromJson(e as Map<String, dynamic>))
 | 
					              : SnAttachment.fromJson(e as Map<String, dynamic>))
 | 
				
			||||||
          .toList(),
 | 
					          .toList(),
 | 
				
			||||||
 | 
					      video: json['video'] == null
 | 
				
			||||||
 | 
					          ? null
 | 
				
			||||||
 | 
					          : SnAttachment.fromJson(json['video'] as Map<String, dynamic>),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Map<String, dynamic> _$$SnPostPreloadImplToJson(_$SnPostPreloadImpl instance) =>
 | 
					Map<String, dynamic> _$$SnPostPreloadImplToJson(_$SnPostPreloadImpl instance) =>
 | 
				
			||||||
    <String, dynamic>{
 | 
					    <String, dynamic>{
 | 
				
			||||||
      'thumbnail': instance.thumbnail?.toJson(),
 | 
					      'thumbnail': instance.thumbnail?.toJson(),
 | 
				
			||||||
      'attachments': instance.attachments?.map((e) => e?.toJson()).toList(),
 | 
					      'attachments': instance.attachments?.map((e) => e?.toJson()).toList(),
 | 
				
			||||||
 | 
					      'video': instance.video?.toJson(),
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
_$SnBodyImpl _$$SnBodyImplFromJson(Map<String, dynamic> json) => _$SnBodyImpl(
 | 
					_$SnBodyImpl _$$SnBodyImplFromJson(Map<String, dynamic> json) => _$SnBodyImpl(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,12 @@
 | 
				
			|||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:gap/gap.dart';
 | 
				
			||||||
 | 
					import 'package:material_symbols_icons/symbols.dart';
 | 
				
			||||||
import 'package:provider/provider.dart';
 | 
					import 'package:provider/provider.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
import 'package:surface/providers/sn_network.dart';
 | 
					import 'package:surface/providers/sn_network.dart';
 | 
				
			||||||
import 'package:surface/providers/user_directory.dart';
 | 
					import 'package:surface/providers/user_directory.dart';
 | 
				
			||||||
 | 
					import 'package:surface/providers/userinfo.dart';
 | 
				
			||||||
import 'package:surface/types/account.dart';
 | 
					import 'package:surface/types/account.dart';
 | 
				
			||||||
import 'package:surface/widgets/account/account_image.dart';
 | 
					import 'package:surface/widgets/account/account_image.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -47,10 +50,19 @@ class _AccountSelectState extends State<AccountSelect> {
 | 
				
			|||||||
  Future<void> _getFriends() async {
 | 
					  Future<void> _getFriends() async {
 | 
				
			||||||
    final sn = context.read<SnNetworkProvider>();
 | 
					    final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
    final resp = await sn.client.get('/cgi/id/users/me/relations?status=1');
 | 
					    final resp = await sn.client.get('/cgi/id/users/me/relations?status=1');
 | 
				
			||||||
 | 
					    if (!mounted) return;
 | 
				
			||||||
 | 
					    final ua = context.read<UserProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setState(() {
 | 
					    setState(() {
 | 
				
			||||||
      _relativeUsers.addAll(
 | 
					      _relativeUsers.addAll(
 | 
				
			||||||
        resp.data?.map((e) => SnRelationship.fromJson(e)) ?? [],
 | 
					        resp.data?.map((e) {
 | 
				
			||||||
 | 
					          final rel = SnRelationship.fromJson(e);
 | 
				
			||||||
 | 
					          if (rel.relatedId == ua.user?.id) {
 | 
				
			||||||
 | 
					            return rel.account!;
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            return rel.related!;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }).cast<SnAccount>(),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -96,10 +108,14 @@ class _AccountSelectState extends State<AccountSelect> {
 | 
				
			|||||||
      child: Column(
 | 
					      child: Column(
 | 
				
			||||||
        crossAxisAlignment: CrossAxisAlignment.start,
 | 
					        crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          Text(
 | 
					          Row(
 | 
				
			||||||
            widget.title,
 | 
					            crossAxisAlignment: CrossAxisAlignment.center,
 | 
				
			||||||
            style: Theme.of(context).textTheme.headlineSmall,
 | 
					            children: [
 | 
				
			||||||
          ).padding(left: 24, right: 24, top: 16, bottom: 16),
 | 
					              const Icon(Symbols.group, size: 24),
 | 
				
			||||||
 | 
					              const Gap(16),
 | 
				
			||||||
 | 
					              Text(widget.title, style: Theme.of(context).textTheme.titleLarge),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					          ).padding(horizontal: 20, top: 16, bottom: 12),
 | 
				
			||||||
          Container(
 | 
					          Container(
 | 
				
			||||||
            color: Theme.of(context).colorScheme.secondaryContainer,
 | 
					            color: Theme.of(context).colorScheme.secondaryContainer,
 | 
				
			||||||
            padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
 | 
					            padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
 | 
				
			||||||
@@ -117,13 +133,9 @@ class _AccountSelectState extends State<AccountSelect> {
 | 
				
			|||||||
          ),
 | 
					          ),
 | 
				
			||||||
          Expanded(
 | 
					          Expanded(
 | 
				
			||||||
            child: ListView.builder(
 | 
					            child: ListView.builder(
 | 
				
			||||||
              itemCount: _pendingUsers.isEmpty
 | 
					              itemCount: _pendingUsers.isEmpty ? _relativeUsers.length : _pendingUsers.length,
 | 
				
			||||||
                  ? _relativeUsers.length
 | 
					 | 
				
			||||||
                  : _pendingUsers.length,
 | 
					 | 
				
			||||||
              itemBuilder: (context, index) {
 | 
					              itemBuilder: (context, index) {
 | 
				
			||||||
                var user = _pendingUsers.isEmpty
 | 
					                var user = _pendingUsers.isEmpty ? _relativeUsers[index] : _pendingUsers[index];
 | 
				
			||||||
                    ? _relativeUsers[index]
 | 
					 | 
				
			||||||
                    : _pendingUsers[index];
 | 
					 | 
				
			||||||
                return ListTile(
 | 
					                return ListTile(
 | 
				
			||||||
                  title: Text(user.nick),
 | 
					                  title: Text(user.nick),
 | 
				
			||||||
                  subtitle: Text(user.name),
 | 
					                  subtitle: Text(user.name),
 | 
				
			||||||
@@ -142,8 +154,7 @@ class _AccountSelectState extends State<AccountSelect> {
 | 
				
			|||||||
                          }
 | 
					                          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                          setState(() {
 | 
					                          setState(() {
 | 
				
			||||||
                            final idx = _selectedUsers
 | 
					                            final idx = _selectedUsers.indexWhere((x) => x.id == user.id);
 | 
				
			||||||
                                .indexWhere((x) => x.id == user.id);
 | 
					 | 
				
			||||||
                            if (idx != -1) {
 | 
					                            if (idx != -1) {
 | 
				
			||||||
                              _selectedUsers.removeAt(idx);
 | 
					                              _selectedUsers.removeAt(idx);
 | 
				
			||||||
                            } else {
 | 
					                            } else {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,12 +6,22 @@ import 'package:material_symbols_icons/symbols.dart';
 | 
				
			|||||||
import 'package:provider/provider.dart';
 | 
					import 'package:provider/provider.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
import 'package:surface/providers/sn_attachment.dart';
 | 
					import 'package:surface/providers/sn_attachment.dart';
 | 
				
			||||||
 | 
					import 'package:surface/types/attachment.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AttachmentInputDialog extends StatefulWidget {
 | 
					class AttachmentInputDialog extends StatefulWidget {
 | 
				
			||||||
  final String? title;
 | 
					  final String? title;
 | 
				
			||||||
  final bool? analyzeNow;
 | 
					  final bool? analyzeNow;
 | 
				
			||||||
  const AttachmentInputDialog({super.key, required this.title, this.analyzeNow = false});
 | 
					  final SnMediaType? mediaType;
 | 
				
			||||||
 | 
					  final String pool;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const AttachmentInputDialog({
 | 
				
			||||||
 | 
					    super.key,
 | 
				
			||||||
 | 
					    required this.title,
 | 
				
			||||||
 | 
					    required this.pool,
 | 
				
			||||||
 | 
					    this.analyzeNow = false,
 | 
				
			||||||
 | 
					    this.mediaType = SnMediaType.image,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  State<AttachmentInputDialog> createState() => _AttachmentInputDialogState();
 | 
					  State<AttachmentInputDialog> createState() => _AttachmentInputDialogState();
 | 
				
			||||||
@@ -20,13 +30,18 @@ final bool? analyzeNow;
 | 
				
			|||||||
class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
 | 
					class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
 | 
				
			||||||
  final _randomIdController = TextEditingController();
 | 
					  final _randomIdController = TextEditingController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  XFile? _thumbnailFile;
 | 
					  XFile? _file;
 | 
				
			||||||
 | 
					  double? _progress;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _pickImage() async {
 | 
					  void _pickMedia() async {
 | 
				
			||||||
    final picker = ImagePicker();
 | 
					    final picker = ImagePicker();
 | 
				
			||||||
    final result = await picker.pickImage(source: ImageSource.gallery);
 | 
					    final result = switch (widget.mediaType) {
 | 
				
			||||||
 | 
					      SnMediaType.image => await picker.pickImage(source: ImageSource.gallery),
 | 
				
			||||||
 | 
					      SnMediaType.video => await picker.pickVideo(source: ImageSource.gallery),
 | 
				
			||||||
 | 
					      _ => await picker.pickMedia(),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
    if (result == null) return;
 | 
					    if (result == null) return;
 | 
				
			||||||
    setState(() => _thumbnailFile = result);
 | 
					    setState(() => _file = result);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bool _isBusy = false;
 | 
					  bool _isBusy = false;
 | 
				
			||||||
@@ -46,15 +61,20 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
 | 
				
			|||||||
        if (!mounted) return;
 | 
					        if (!mounted) return;
 | 
				
			||||||
        context.showErrorDialog(err);
 | 
					        context.showErrorDialog(err);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } else if (_thumbnailFile != null) {
 | 
					    } else if (_file != null) {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        final attachment = await attach.directUploadOne(
 | 
					        final place = await attach.chunkedUploadInitialize(await _file!.length(), _file!.name, widget.pool, null);
 | 
				
			||||||
          (await _thumbnailFile!.readAsBytes()).buffer.asUint8List(),
 | 
					
 | 
				
			||||||
          _thumbnailFile!.path,
 | 
					        final attachment = await attach.chunkedUploadParts(
 | 
				
			||||||
          'interactive',
 | 
					          _file!,
 | 
				
			||||||
          null,
 | 
					          place.$1,
 | 
				
			||||||
 | 
					          place.$2,
 | 
				
			||||||
          analyzeNow: widget.analyzeNow ?? false,
 | 
					          analyzeNow: widget.analyzeNow ?? false,
 | 
				
			||||||
 | 
					          onProgress: (value) {
 | 
				
			||||||
 | 
					            setState(() => _progress = value);
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!mounted) return;
 | 
					        if (!mounted) return;
 | 
				
			||||||
        Navigator.pop(context, attachment);
 | 
					        Navigator.pop(context, attachment);
 | 
				
			||||||
      } catch (err) {
 | 
					      } catch (err) {
 | 
				
			||||||
@@ -67,7 +87,7 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
 | 
				
			|||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return AlertDialog(
 | 
					    return AlertDialog(
 | 
				
			||||||
      title: Text(widget.title ?? 'attachmentInputDialog').tr(),
 | 
					      title: Text(widget.title ?? 'attachmentInputDialog'.tr()),
 | 
				
			||||||
      content: Column(
 | 
					      content: Column(
 | 
				
			||||||
        crossAxisAlignment: CrossAxisAlignment.start,
 | 
					        crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
        mainAxisSize: MainAxisSize.min,
 | 
					        mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
@@ -86,22 +106,33 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
 | 
				
			|||||||
          const Gap(24),
 | 
					          const Gap(24),
 | 
				
			||||||
          Text('attachmentInputNew').tr().fontSize(14),
 | 
					          Text('attachmentInputNew').tr().fontSize(14),
 | 
				
			||||||
          Card(
 | 
					          Card(
 | 
				
			||||||
            child: ListTile(
 | 
					            child: Column(
 | 
				
			||||||
 | 
					              children: [
 | 
				
			||||||
 | 
					                ListTile(
 | 
				
			||||||
                  contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
 | 
					                  contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
 | 
				
			||||||
                  leading: const Icon(Symbols.add_photo_alternate),
 | 
					                  leading: const Icon(Symbols.add_photo_alternate),
 | 
				
			||||||
                  trailing: const Icon(Symbols.chevron_right),
 | 
					                  trailing: const Icon(Symbols.chevron_right),
 | 
				
			||||||
                  title: Text('addAttachmentFromAlbum').tr(),
 | 
					                  title: Text('addAttachmentFromAlbum').tr(),
 | 
				
			||||||
              subtitle: _thumbnailFile == null ? Text('unset').tr() : Text('waitingForUpload').tr(),
 | 
					                  subtitle: _file == null ? Text('unset').tr() : Text('waitingForUpload').tr(),
 | 
				
			||||||
                  onTap: () {
 | 
					                  onTap: () {
 | 
				
			||||||
                _pickImage();
 | 
					                    _pickMedia();
 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          if (_isBusy)
 | 
				
			||||||
 | 
					            LinearProgressIndicator(
 | 
				
			||||||
 | 
					              value: _progress,
 | 
				
			||||||
 | 
					              borderRadius: BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
 | 
					            ).padding(top: 16),
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      actions: [
 | 
					      actions: [
 | 
				
			||||||
        TextButton(
 | 
					        TextButton(
 | 
				
			||||||
          onPressed: _isBusy ? null : () {
 | 
					          onPressed: _isBusy
 | 
				
			||||||
 | 
					              ? null
 | 
				
			||||||
 | 
					              : () {
 | 
				
			||||||
                  Navigator.pop(context);
 | 
					                  Navigator.pop(context);
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
          child: Text('dialogDismiss').tr(),
 | 
					          child: Text('dialogDismiss').tr(),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -203,6 +203,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
 | 
				
			|||||||
  void dispose() {
 | 
					  void dispose() {
 | 
				
			||||||
    _contentController.dispose();
 | 
					    _contentController.dispose();
 | 
				
			||||||
    _focusNode.dispose();
 | 
					    _focusNode.dispose();
 | 
				
			||||||
 | 
					    _dismissEmojiPicker();
 | 
				
			||||||
    if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) hotKeyManager.unregister(_pasteHotKey);
 | 
					    if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) hotKeyManager.unregister(_pasteHotKey);
 | 
				
			||||||
    super.dispose();
 | 
					    super.dispose();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -336,6 +337,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
 | 
				
			|||||||
                        : 'fieldChatMessage'.tr(args: [widget.controller.channel?.name ?? 'loading'.tr()]),
 | 
					                        : 'fieldChatMessage'.tr(args: [widget.controller.channel?.name ?? 'loading'.tr()]),
 | 
				
			||||||
                    border: InputBorder.none,
 | 
					                    border: InputBorder.none,
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
 | 
					                  textInputAction: TextInputAction.send,
 | 
				
			||||||
                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
					                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
                  onSubmitted: (_) {
 | 
					                  onSubmitted: (_) {
 | 
				
			||||||
                    if (_isBusy) return;
 | 
					                    if (_isBusy) return;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,17 +8,23 @@ import 'package:styled_widget/styled_widget.dart';
 | 
				
			|||||||
import 'package:surface/providers/post.dart';
 | 
					import 'package:surface/providers/post.dart';
 | 
				
			||||||
import 'package:surface/providers/userinfo.dart';
 | 
					import 'package:surface/providers/userinfo.dart';
 | 
				
			||||||
import 'package:surface/types/post.dart';
 | 
					import 'package:surface/types/post.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
import 'package:surface/widgets/post/post_item.dart';
 | 
					import 'package:surface/widgets/post/post_item.dart';
 | 
				
			||||||
import 'package:surface/widgets/post/post_mini_editor.dart';
 | 
					import 'package:surface/widgets/post/post_mini_editor.dart';
 | 
				
			||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
 | 
					import 'package:very_good_infinite_list/very_good_infinite_list.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import '../../providers/sn_network.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PostCommentSliverList extends StatefulWidget {
 | 
					class PostCommentSliverList extends StatefulWidget {
 | 
				
			||||||
  final int parentPostId;
 | 
					  final SnPost parentPost;
 | 
				
			||||||
  final double? maxWidth;
 | 
					  final double? maxWidth;
 | 
				
			||||||
 | 
					  final Function(SnPost)? onSelectAnswer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const PostCommentSliverList({
 | 
					  const PostCommentSliverList({
 | 
				
			||||||
    super.key,
 | 
					    super.key,
 | 
				
			||||||
    required this.parentPostId,
 | 
					    required this.parentPost,
 | 
				
			||||||
    this.maxWidth,
 | 
					    this.maxWidth,
 | 
				
			||||||
 | 
					    this.onSelectAnswer,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@@ -37,7 +43,7 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
 | 
				
			|||||||
    setState(() => _isBusy = true);
 | 
					    setState(() => _isBusy = true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final pt = context.read<SnPostContentProvider>();
 | 
					    final pt = context.read<SnPostContentProvider>();
 | 
				
			||||||
    final result = await pt.listPostReplies(widget.parentPostId);
 | 
					    final result = await pt.listPostReplies(widget.parentPost.id);
 | 
				
			||||||
    final List<SnPost> out = result.$1;
 | 
					    final List<SnPost> out = result.$1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!mounted) return;
 | 
					    if (!mounted) return;
 | 
				
			||||||
@@ -48,6 +54,21 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
 | 
				
			|||||||
    if (mounted) setState(() => _isBusy = false);
 | 
					    if (mounted) setState(() => _isBusy = false);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _selectAnswer(SnPost answer) async {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					      await sn.client.put('/cgi/co/questions/${widget.parentPost.id}/answer', data: {
 | 
				
			||||||
 | 
					        'publisher': answer.publisherId,
 | 
				
			||||||
 | 
					        'answer_id': answer.id,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      if (!mounted) return;
 | 
				
			||||||
 | 
					      await refresh();
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      if (!mounted) return;
 | 
				
			||||||
 | 
					      context.showErrorDialog(err);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> refresh() async {
 | 
					  Future<void> refresh() async {
 | 
				
			||||||
    _posts.clear();
 | 
					    _posts.clear();
 | 
				
			||||||
    _fetchPosts();
 | 
					    _fetchPosts();
 | 
				
			||||||
@@ -71,6 +92,7 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
 | 
				
			|||||||
          child: PostItem(
 | 
					          child: PostItem(
 | 
				
			||||||
            data: _posts[idx],
 | 
					            data: _posts[idx],
 | 
				
			||||||
            maxWidth: widget.maxWidth,
 | 
					            maxWidth: widget.maxWidth,
 | 
				
			||||||
 | 
					            onSelectAnswer: widget.parentPost.type == 'question' ? () => _selectAnswer(_posts[idx]) : null,
 | 
				
			||||||
            onChanged: (data) {
 | 
					            onChanged: (data) {
 | 
				
			||||||
              setState(() => _posts[idx] = data);
 | 
					              setState(() => _posts[idx] = data);
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
@@ -94,11 +116,12 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PostCommentListPopup extends StatefulWidget {
 | 
					class PostCommentListPopup extends StatefulWidget {
 | 
				
			||||||
  final int postId;
 | 
					  final SnPost post;
 | 
				
			||||||
  final int commentCount;
 | 
					  final int commentCount;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const PostCommentListPopup({
 | 
					  const PostCommentListPopup({
 | 
				
			||||||
    super.key,
 | 
					    super.key,
 | 
				
			||||||
    required this.postId,
 | 
					    required this.post,
 | 
				
			||||||
    this.commentCount = 0,
 | 
					    this.commentCount = 0,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -122,9 +145,7 @@ class _PostCommentListPopupState extends State<PostCommentListPopup> {
 | 
				
			|||||||
          children: [
 | 
					          children: [
 | 
				
			||||||
            const Icon(Symbols.comment, size: 24),
 | 
					            const Icon(Symbols.comment, size: 24),
 | 
				
			||||||
            const Gap(16),
 | 
					            const Gap(16),
 | 
				
			||||||
            Text('postCommentsDetailed')
 | 
					            Text('postCommentsDetailed').plural(widget.commentCount).textStyle(Theme.of(context).textTheme.titleLarge!),
 | 
				
			||||||
                .plural(widget.commentCount)
 | 
					 | 
				
			||||||
                .textStyle(Theme.of(context).textTheme.titleLarge!),
 | 
					 | 
				
			||||||
          ],
 | 
					          ],
 | 
				
			||||||
        ).padding(horizontal: 20, top: 16, bottom: 12),
 | 
					        ).padding(horizontal: 20, top: 16, bottom: 12),
 | 
				
			||||||
        Expanded(
 | 
					        Expanded(
 | 
				
			||||||
@@ -143,7 +164,7 @@ class _PostCommentListPopupState extends State<PostCommentListPopup> {
 | 
				
			|||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                    child: PostMiniEditor(
 | 
					                    child: PostMiniEditor(
 | 
				
			||||||
                      postReplyId: widget.postId,
 | 
					                      postReplyId: widget.post.id,
 | 
				
			||||||
                      onPost: () {
 | 
					                      onPost: () {
 | 
				
			||||||
                        _childListKey.currentState!.refresh();
 | 
					                        _childListKey.currentState!.refresh();
 | 
				
			||||||
                      },
 | 
					                      },
 | 
				
			||||||
@@ -151,8 +172,8 @@ class _PostCommentListPopupState extends State<PostCommentListPopup> {
 | 
				
			|||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              PostCommentSliverList(
 | 
					              PostCommentSliverList(
 | 
				
			||||||
 | 
					                parentPost: widget.post,
 | 
				
			||||||
                key: _childListKey,
 | 
					                key: _childListKey,
 | 
				
			||||||
                parentPostId: widget.postId,
 | 
					 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
import 'dart:io';
 | 
					import 'dart:io';
 | 
				
			||||||
import 'dart:math' as math;
 | 
					import 'dart:math' as math;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:animations/animations.dart';
 | 
				
			||||||
import 'package:dio/dio.dart';
 | 
					import 'package:dio/dio.dart';
 | 
				
			||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:file_saver/file_saver.dart';
 | 
					import 'package:file_saver/file_saver.dart';
 | 
				
			||||||
@@ -22,10 +23,12 @@ import 'package:styled_widget/styled_widget.dart';
 | 
				
			|||||||
import 'package:surface/providers/config.dart';
 | 
					import 'package:surface/providers/config.dart';
 | 
				
			||||||
import 'package:surface/providers/sn_network.dart';
 | 
					import 'package:surface/providers/sn_network.dart';
 | 
				
			||||||
import 'package:surface/providers/userinfo.dart';
 | 
					import 'package:surface/providers/userinfo.dart';
 | 
				
			||||||
 | 
					import 'package:surface/screens/post/post_detail.dart';
 | 
				
			||||||
import 'package:surface/types/attachment.dart';
 | 
					import 'package:surface/types/attachment.dart';
 | 
				
			||||||
import 'package:surface/types/post.dart';
 | 
					import 'package:surface/types/post.dart';
 | 
				
			||||||
import 'package:surface/types/reaction.dart';
 | 
					import 'package:surface/types/reaction.dart';
 | 
				
			||||||
import 'package:surface/widgets/account/account_image.dart';
 | 
					import 'package:surface/widgets/account/account_image.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/attachment/attachment_item.dart';
 | 
				
			||||||
import 'package:surface/widgets/attachment/attachment_list.dart';
 | 
					import 'package:surface/widgets/attachment/attachment_list.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
import 'package:surface/widgets/link_preview.dart';
 | 
					import 'package:surface/widgets/link_preview.dart';
 | 
				
			||||||
@@ -38,6 +41,65 @@ import 'package:surface/widgets/post/publisher_popover.dart';
 | 
				
			|||||||
import 'package:surface/widgets/universal_image.dart';
 | 
					import 'package:surface/widgets/universal_image.dart';
 | 
				
			||||||
import 'package:xml/xml.dart';
 | 
					import 'package:xml/xml.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class OpenablePostItem extends StatelessWidget {
 | 
				
			||||||
 | 
					  final SnPost data;
 | 
				
			||||||
 | 
					  final bool showReactions;
 | 
				
			||||||
 | 
					  final bool showComments;
 | 
				
			||||||
 | 
					  final bool showMenu;
 | 
				
			||||||
 | 
					  final bool showFullPost;
 | 
				
			||||||
 | 
					  final double? maxWidth;
 | 
				
			||||||
 | 
					  final Function(SnPost data)? onChanged;
 | 
				
			||||||
 | 
					  final Function()? onDeleted;
 | 
				
			||||||
 | 
					  final Function()? onSelectAnswer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const OpenablePostItem({
 | 
				
			||||||
 | 
					    super.key,
 | 
				
			||||||
 | 
					    required this.data,
 | 
				
			||||||
 | 
					    this.showReactions = true,
 | 
				
			||||||
 | 
					    this.showComments = true,
 | 
				
			||||||
 | 
					    this.showMenu = true,
 | 
				
			||||||
 | 
					    this.showFullPost = false,
 | 
				
			||||||
 | 
					    this.maxWidth,
 | 
				
			||||||
 | 
					    this.onChanged,
 | 
				
			||||||
 | 
					    this.onDeleted,
 | 
				
			||||||
 | 
					    this.onSelectAnswer,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    final cfg = context.read<ConfigProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return OpenContainer(
 | 
				
			||||||
 | 
					      closedBuilder: (_, __) => Container(
 | 
				
			||||||
 | 
					        constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
 | 
				
			||||||
 | 
					        child: PostItem(
 | 
				
			||||||
 | 
					          data: data,
 | 
				
			||||||
 | 
					          maxWidth: maxWidth,
 | 
				
			||||||
 | 
					          showComments: showComments,
 | 
				
			||||||
 | 
					          showFullPost: showFullPost,
 | 
				
			||||||
 | 
					          onChanged: onChanged,
 | 
				
			||||||
 | 
					          onDeleted: onDeleted,
 | 
				
			||||||
 | 
					          onSelectAnswer: onSelectAnswer,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      openBuilder: (_, close) => PostDetailScreen(
 | 
				
			||||||
 | 
					        slug: data.id.toString(),
 | 
				
			||||||
 | 
					        preload: data,
 | 
				
			||||||
 | 
					        onBack: close,
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      openColor: Colors.transparent,
 | 
				
			||||||
 | 
					      openElevation: 0,
 | 
				
			||||||
 | 
					      transitionType: ContainerTransitionType.fade,
 | 
				
			||||||
 | 
					      closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(
 | 
				
			||||||
 | 
					            cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					      closedShape: const RoundedRectangleBorder(
 | 
				
			||||||
 | 
					        borderRadius: BorderRadius.all(Radius.circular(16)),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PostItem extends StatelessWidget {
 | 
					class PostItem extends StatelessWidget {
 | 
				
			||||||
  final SnPost data;
 | 
					  final SnPost data;
 | 
				
			||||||
  final bool showReactions;
 | 
					  final bool showReactions;
 | 
				
			||||||
@@ -47,6 +109,7 @@ class PostItem extends StatelessWidget {
 | 
				
			|||||||
  final double? maxWidth;
 | 
					  final double? maxWidth;
 | 
				
			||||||
  final Function(SnPost data)? onChanged;
 | 
					  final Function(SnPost data)? onChanged;
 | 
				
			||||||
  final Function()? onDeleted;
 | 
					  final Function()? onDeleted;
 | 
				
			||||||
 | 
					  final Function()? onSelectAnswer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const PostItem({
 | 
					  const PostItem({
 | 
				
			||||||
    super.key,
 | 
					    super.key,
 | 
				
			||||||
@@ -58,6 +121,7 @@ class PostItem extends StatelessWidget {
 | 
				
			|||||||
    this.maxWidth,
 | 
					    this.maxWidth,
 | 
				
			||||||
    this.onChanged,
 | 
					    this.onChanged,
 | 
				
			||||||
    this.onDeleted,
 | 
					    this.onDeleted,
 | 
				
			||||||
 | 
					    this.onSelectAnswer,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _onChanged(SnPost data) {
 | 
					  void _onChanged(SnPost data) {
 | 
				
			||||||
@@ -142,10 +206,12 @@ class PostItem extends StatelessWidget {
 | 
				
			|||||||
              isRelativeDate: !showFullPost,
 | 
					              isRelativeDate: !showFullPost,
 | 
				
			||||||
              onShare: () => _doShare(context),
 | 
					              onShare: () => _doShare(context),
 | 
				
			||||||
              onShareImage: () => _doShareViaPicture(context),
 | 
					              onShareImage: () => _doShareViaPicture(context),
 | 
				
			||||||
 | 
					              onSelectAnswer: onSelectAnswer,
 | 
				
			||||||
              onDeleted: () {
 | 
					              onDeleted: () {
 | 
				
			||||||
                if (onDeleted != null) {}
 | 
					                if (onDeleted != null) {}
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
            ).padding(horizontal: 12, top: 8, bottom: 8),
 | 
					            ).padding(horizontal: 12, top: 8, bottom: 8),
 | 
				
			||||||
 | 
					            if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
 | 
				
			||||||
            Container(
 | 
					            Container(
 | 
				
			||||||
              width: double.infinity,
 | 
					              width: double.infinity,
 | 
				
			||||||
              margin: const EdgeInsets.only(bottom: 4, left: 12, right: 12),
 | 
					              margin: const EdgeInsets.only(bottom: 4, left: 12, right: 12),
 | 
				
			||||||
@@ -224,10 +290,13 @@ class PostItem extends StatelessWidget {
 | 
				
			|||||||
                showMenu: showMenu,
 | 
					                showMenu: showMenu,
 | 
				
			||||||
                onShare: () => _doShare(context),
 | 
					                onShare: () => _doShare(context),
 | 
				
			||||||
                onShareImage: () => _doShareViaPicture(context),
 | 
					                onShareImage: () => _doShareViaPicture(context),
 | 
				
			||||||
 | 
					                onSelectAnswer: onSelectAnswer,
 | 
				
			||||||
                onDeleted: () {
 | 
					                onDeleted: () {
 | 
				
			||||||
                  if (onDeleted != null) onDeleted!();
 | 
					                  if (onDeleted != null) onDeleted!();
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
              ).padding(horizontal: 12, vertical: 8),
 | 
					              ).padding(horizontal: 12, vertical: 8),
 | 
				
			||||||
 | 
					              if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
 | 
				
			||||||
 | 
					              if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8),
 | 
				
			||||||
              if (data.body['title'] != null || data.body['description'] != null)
 | 
					              if (data.body['title'] != null || data.body['description'] != null)
 | 
				
			||||||
                _PostHeadline(
 | 
					                _PostHeadline(
 | 
				
			||||||
                  data: data,
 | 
					                  data: data,
 | 
				
			||||||
@@ -333,6 +402,7 @@ class PostShareImageWidget extends StatelessWidget {
 | 
				
			|||||||
            showMenu: false,
 | 
					            showMenu: false,
 | 
				
			||||||
            isRelativeDate: false,
 | 
					            isRelativeDate: false,
 | 
				
			||||||
          ).padding(horizontal: 16, bottom: 8),
 | 
					          ).padding(horizontal: 16, bottom: 8),
 | 
				
			||||||
 | 
					          if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8),
 | 
				
			||||||
          _PostHeadline(
 | 
					          _PostHeadline(
 | 
				
			||||||
            data: data,
 | 
					            data: data,
 | 
				
			||||||
            isEnlarge: data.type == 'article',
 | 
					            isEnlarge: data.type == 'article',
 | 
				
			||||||
@@ -438,6 +508,30 @@ class PostShareImageWidget extends StatelessWidget {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _PostQuestionHint extends StatelessWidget {
 | 
				
			||||||
 | 
					  final SnPost data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const _PostQuestionHint({required this.data});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return Row(
 | 
				
			||||||
 | 
					      children: [
 | 
				
			||||||
 | 
					        Icon(data.body['answer'] == null ? Symbols.help : Symbols.check_circle, size: 20),
 | 
				
			||||||
 | 
					        const Gap(4),
 | 
				
			||||||
 | 
					        if (data.body['answer'] == null && data.body['reward']?.toDouble() != null)
 | 
				
			||||||
 | 
					          Text('postQuestionUnansweredWithReward'.tr(args: [
 | 
				
			||||||
 | 
					            '${data.body['reward']}',
 | 
				
			||||||
 | 
					          ])).opacity(0.75)
 | 
				
			||||||
 | 
					        else if (data.body['answer'] == null)
 | 
				
			||||||
 | 
					          Text('postQuestionUnanswered'.tr()).opacity(0.75)
 | 
				
			||||||
 | 
					        else
 | 
				
			||||||
 | 
					          Text('postQuestionAnswered'.tr()).opacity(0.75),
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    ).opacity(0.75);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _PostBottomAction extends StatelessWidget {
 | 
					class _PostBottomAction extends StatelessWidget {
 | 
				
			||||||
  final SnPost data;
 | 
					  final SnPost data;
 | 
				
			||||||
  final bool showComments;
 | 
					  final bool showComments;
 | 
				
			||||||
@@ -529,7 +623,7 @@ class _PostBottomAction extends StatelessWidget {
 | 
				
			|||||||
                      context: context,
 | 
					                      context: context,
 | 
				
			||||||
                      useRootNavigator: true,
 | 
					                      useRootNavigator: true,
 | 
				
			||||||
                      builder: (context) => PostCommentListPopup(
 | 
					                      builder: (context) => PostCommentListPopup(
 | 
				
			||||||
                        postId: data.id,
 | 
					                        post: data,
 | 
				
			||||||
                        commentCount: data.metric.replyCount,
 | 
					                        commentCount: data.metric.replyCount,
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                    );
 | 
					                    );
 | 
				
			||||||
@@ -652,6 +746,7 @@ class _PostContentHeader extends StatelessWidget {
 | 
				
			|||||||
  final bool showMenu;
 | 
					  final bool showMenu;
 | 
				
			||||||
  final Function onDeleted;
 | 
					  final Function onDeleted;
 | 
				
			||||||
  final Function() onShare, onShareImage;
 | 
					  final Function() onShare, onShareImage;
 | 
				
			||||||
 | 
					  final Function()? onSelectAnswer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const _PostContentHeader({
 | 
					  const _PostContentHeader({
 | 
				
			||||||
    required this.data,
 | 
					    required this.data,
 | 
				
			||||||
@@ -662,6 +757,7 @@ class _PostContentHeader extends StatelessWidget {
 | 
				
			|||||||
    required this.onDeleted,
 | 
					    required this.onDeleted,
 | 
				
			||||||
    required this.onShare,
 | 
					    required this.onShare,
 | 
				
			||||||
    required this.onShareImage,
 | 
					    required this.onShareImage,
 | 
				
			||||||
 | 
					    this.onSelectAnswer,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> _deletePost(BuildContext context) async {
 | 
					  Future<void> _deletePost(BuildContext context) async {
 | 
				
			||||||
@@ -760,6 +856,20 @@ class _PostContentHeader extends StatelessWidget {
 | 
				
			|||||||
              visualDensity: VisualDensity(horizontal: -4, vertical: -4),
 | 
					              visualDensity: VisualDensity(horizontal: -4, vertical: -4),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            itemBuilder: (BuildContext context) => <PopupMenuEntry>[
 | 
					            itemBuilder: (BuildContext context) => <PopupMenuEntry>[
 | 
				
			||||||
 | 
					              if (isAuthor && onSelectAnswer != null)
 | 
				
			||||||
 | 
					                PopupMenuItem(
 | 
				
			||||||
 | 
					                  child: Row(
 | 
				
			||||||
 | 
					                    children: [
 | 
				
			||||||
 | 
					                      const Icon(Symbols.check_circle),
 | 
				
			||||||
 | 
					                      const Gap(16),
 | 
				
			||||||
 | 
					                      Text('postQuestionAnswerSelect').tr(),
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  onTap: () {
 | 
				
			||||||
 | 
					                    onSelectAnswer?.call();
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              if (isAuthor && onSelectAnswer != null) PopupMenuDivider(),
 | 
				
			||||||
              if (isAuthor)
 | 
					              if (isAuthor)
 | 
				
			||||||
                PopupMenuItem(
 | 
					                PopupMenuItem(
 | 
				
			||||||
                  child: Row(
 | 
					                  child: Row(
 | 
				
			||||||
@@ -833,7 +943,7 @@ class _PostContentHeader extends StatelessWidget {
 | 
				
			|||||||
                onTap: () {
 | 
					                onTap: () {
 | 
				
			||||||
                  showModalBottomSheet(
 | 
					                  showModalBottomSheet(
 | 
				
			||||||
                    context: context,
 | 
					                    context: context,
 | 
				
			||||||
                    builder: (context) => _PostGetInsightSheet(postId: data.id),
 | 
					                    builder: (context) => _PostGetInsightPopup(postId: data.id),
 | 
				
			||||||
                  );
 | 
					                  );
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
@@ -1139,8 +1249,18 @@ class _PostFeaturedComment extends StatefulWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
 | 
					class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
 | 
				
			||||||
  SnPost? _featuredComment;
 | 
					  SnPost? _featuredComment;
 | 
				
			||||||
 | 
					  bool _isAnswer = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> _fetchComments() async {
 | 
					  Future<void> _fetchComments() async {
 | 
				
			||||||
 | 
					    // If this is a answered question, fetch the answer instead
 | 
				
			||||||
 | 
					    if (widget.data.type == 'question' && widget.data.body['answer'] != null) {
 | 
				
			||||||
 | 
					      final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					      final resp = await sn.client.get('/cgi/co/posts/${widget.data.body['answer']}');
 | 
				
			||||||
 | 
					      _isAnswer = true;
 | 
				
			||||||
 | 
					      setState(() => _featuredComment = SnPost.fromJson(resp.data));
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      final sn = context.read<SnNetworkProvider>();
 | 
					      final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
      final resp = await sn.client.get('/cgi/co/posts/${widget.data.id}/replies/featured', queryParameters: {
 | 
					      final resp = await sn.client.get('/cgi/co/posts/${widget.data.id}/replies/featured', queryParameters: {
 | 
				
			||||||
@@ -1166,13 +1286,15 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
 | 
				
			|||||||
    if (widget.data.metric.replyCount == 0) return const SizedBox.shrink();
 | 
					    if (widget.data.metric.replyCount == 0) return const SizedBox.shrink();
 | 
				
			||||||
    if (_featuredComment == null) return const SizedBox.shrink();
 | 
					    if (_featuredComment == null) return const SizedBox.shrink();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return AnimateWidgetExtensions(Container(
 | 
					    return AnimateWidgetExtensions(Container(
 | 
				
			||||||
      constraints: BoxConstraints(maxWidth: widget.maxWidth ?? double.infinity),
 | 
					      constraints: BoxConstraints(maxWidth: widget.maxWidth ?? double.infinity),
 | 
				
			||||||
      margin: const EdgeInsets.only(top: 8),
 | 
					      margin: const EdgeInsets.only(top: 8),
 | 
				
			||||||
      width: double.infinity,
 | 
					      width: double.infinity,
 | 
				
			||||||
      child: Material(
 | 
					      child: Material(
 | 
				
			||||||
        borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
					        borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
        color: Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
					        color: _isAnswer ? Colors.green.withOpacity(0.5) : Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
				
			||||||
        child: InkWell(
 | 
					        child: InkWell(
 | 
				
			||||||
          borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
					          borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
          onTap: () {
 | 
					          onTap: () {
 | 
				
			||||||
@@ -1180,7 +1302,7 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
 | 
				
			|||||||
              context: context,
 | 
					              context: context,
 | 
				
			||||||
              useRootNavigator: true,
 | 
					              useRootNavigator: true,
 | 
				
			||||||
              builder: (context) => PostCommentListPopup(
 | 
					              builder: (context) => PostCommentListPopup(
 | 
				
			||||||
                postId: widget.data.id,
 | 
					                post: widget.data,
 | 
				
			||||||
                commentCount: widget.data.metric.replyCount,
 | 
					                commentCount: widget.data.metric.replyCount,
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
@@ -1188,7 +1310,18 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
 | 
				
			|||||||
          child: Column(
 | 
					          child: Column(
 | 
				
			||||||
            crossAxisAlignment: CrossAxisAlignment.start,
 | 
					            crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
            children: [
 | 
					            children: [
 | 
				
			||||||
              Text('postFeaturedComment', style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 16)).tr(),
 | 
					              Row(
 | 
				
			||||||
 | 
					                crossAxisAlignment: CrossAxisAlignment.center,
 | 
				
			||||||
 | 
					                children: [
 | 
				
			||||||
 | 
					                  const Gap(2),
 | 
				
			||||||
 | 
					                  Icon(_isAnswer ? Symbols.task_alt : Symbols.prompt_suggestion, size: 20),
 | 
				
			||||||
 | 
					                  const Gap(10),
 | 
				
			||||||
 | 
					                  Text(
 | 
				
			||||||
 | 
					                    _isAnswer ? 'postQuestionAnswerTitle' : 'postFeaturedComment',
 | 
				
			||||||
 | 
					                    style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 15),
 | 
				
			||||||
 | 
					                  ).tr(),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
              const Gap(4),
 | 
					              const Gap(4),
 | 
				
			||||||
              Row(
 | 
					              Row(
 | 
				
			||||||
                crossAxisAlignment: CrossAxisAlignment.center,
 | 
					                crossAxisAlignment: CrossAxisAlignment.center,
 | 
				
			||||||
@@ -1196,7 +1329,7 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
 | 
				
			|||||||
                  CircleAvatar(
 | 
					                  CircleAvatar(
 | 
				
			||||||
                    radius: 12,
 | 
					                    radius: 12,
 | 
				
			||||||
                    backgroundImage: UniversalImage.provider(
 | 
					                    backgroundImage: UniversalImage.provider(
 | 
				
			||||||
                      _featuredComment!.publisher.avatar,
 | 
					                      sn.getAttachmentUrl(_featuredComment!.publisher.avatar),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  const Gap(8),
 | 
					                  const Gap(8),
 | 
				
			||||||
@@ -1292,16 +1425,16 @@ class _PostAbuseReportDialogState extends State<_PostAbuseReportDialog> {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _PostGetInsightSheet extends StatefulWidget {
 | 
					class _PostGetInsightPopup extends StatefulWidget {
 | 
				
			||||||
  final int postId;
 | 
					  final int postId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const _PostGetInsightSheet({required this.postId});
 | 
					  const _PostGetInsightPopup({required this.postId});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  State<_PostGetInsightSheet> createState() => _PostGetInsightSheetState();
 | 
					  State<_PostGetInsightPopup> createState() => _PostGetInsightPopupState();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _PostGetInsightSheetState extends State<_PostGetInsightSheet> {
 | 
					class _PostGetInsightPopupState extends State<_PostGetInsightPopup> {
 | 
				
			||||||
  String? _response;
 | 
					  String? _response;
 | 
				
			||||||
  String? _thinkingProcess;
 | 
					  String? _thinkingProcess;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1314,8 +1447,14 @@ class _PostGetInsightSheetState extends State<_PostGetInsightSheet> {
 | 
				
			|||||||
            receiveTimeout: const Duration(minutes: 10),
 | 
					            receiveTimeout: const Duration(minutes: 10),
 | 
				
			||||||
          ));
 | 
					          ));
 | 
				
			||||||
      final out = resp.data['response'] as String;
 | 
					      final out = resp.data['response'] as String;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
        final document = XmlDocument.parse(out);
 | 
					        final document = XmlDocument.parse(out);
 | 
				
			||||||
        _thinkingProcess = document.getElement('think')?.innerText.trim();
 | 
					        _thinkingProcess = document.getElement('think')?.innerText.trim();
 | 
				
			||||||
 | 
					      } catch (_) {
 | 
				
			||||||
 | 
					        // ignore
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      RegExp cleanThinkingRegExp = RegExp(r'<think>[\s\S]*?</think>');
 | 
					      RegExp cleanThinkingRegExp = RegExp(r'<think>[\s\S]*?</think>');
 | 
				
			||||||
      setState(() => _response = out.replaceAll(cleanThinkingRegExp, '').trim());
 | 
					      setState(() => _response = out.replaceAll(cleanThinkingRegExp, '').trim());
 | 
				
			||||||
    } catch (err) {
 | 
					    } catch (err) {
 | 
				
			||||||
@@ -1384,3 +1523,29 @@ class _PostGetInsightSheetState extends State<_PostGetInsightSheet> {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _PostVideoPlayer extends StatelessWidget {
 | 
				
			||||||
 | 
					  final SnPost data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const _PostVideoPlayer({required this.data});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return Container(
 | 
				
			||||||
 | 
					      decoration: BoxDecoration(
 | 
				
			||||||
 | 
					        borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
 | 
					        border: Border.all(
 | 
				
			||||||
 | 
					          color: Theme.of(context).dividerColor,
 | 
				
			||||||
 | 
					          width: 1,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      child: AspectRatio(
 | 
				
			||||||
 | 
					        aspectRatio: 16 / 9,
 | 
				
			||||||
 | 
					        child: ClipRRect(
 | 
				
			||||||
 | 
					          borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
 | 
					          child: AttachmentItem(data: data.preload!.video!, heroTag: 'post-video-${data.id}'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -95,6 +95,7 @@ class PostMediaPendingList extends StatelessWidget {
 | 
				
			|||||||
      context: context,
 | 
					      context: context,
 | 
				
			||||||
      builder: (context) => AttachmentInputDialog(
 | 
					      builder: (context) => AttachmentInputDialog(
 | 
				
			||||||
        title: 'attachmentSetThumbnail'.tr(),
 | 
					        title: 'attachmentSetThumbnail'.tr(),
 | 
				
			||||||
 | 
					        pool: 'interactive',
 | 
				
			||||||
        analyzeNow: true,
 | 
					        analyzeNow: true,
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
@@ -292,7 +293,7 @@ class PostMediaPendingList extends StatelessWidget {
 | 
				
			|||||||
      constraints: const BoxConstraints(maxHeight: 120),
 | 
					      constraints: const BoxConstraints(maxHeight: 120),
 | 
				
			||||||
      child: Row(
 | 
					      child: Row(
 | 
				
			||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          const Gap(8),
 | 
					          const Gap(16),
 | 
				
			||||||
          if (thumbnail != null)
 | 
					          if (thumbnail != null)
 | 
				
			||||||
            ContextMenuArea(
 | 
					            ContextMenuArea(
 | 
				
			||||||
              contextMenu: _createContextMenu(context, -1, thumbnail!),
 | 
					              contextMenu: _createContextMenu(context, -1, thumbnail!),
 | 
				
			||||||
@@ -337,15 +338,10 @@ class _PostMediaPendingItem extends StatelessWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    final sn = context.read<SnNetworkProvider>();
 | 
					    final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Container(
 | 
					    return Material(
 | 
				
			||||||
      decoration: BoxDecoration(
 | 
					      elevation: 4,
 | 
				
			||||||
        border: Border.all(
 | 
					 | 
				
			||||||
          color: Theme.of(context).dividerColor,
 | 
					 | 
				
			||||||
          width: 1,
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        borderRadius: BorderRadius.circular(8),
 | 
					 | 
				
			||||||
      color: Theme.of(context).colorScheme.surfaceContainer,
 | 
					      color: Theme.of(context).colorScheme.surfaceContainer,
 | 
				
			||||||
      ),
 | 
					      borderRadius: BorderRadius.circular(8),
 | 
				
			||||||
      child: ClipRRect(
 | 
					      child: ClipRRect(
 | 
				
			||||||
        borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
					        borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
        child: Row(
 | 
					        child: Row(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,6 +19,7 @@ const Map<int, String> kPostVisibilityLevel = {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class PostMetaEditor extends StatelessWidget {
 | 
					class PostMetaEditor extends StatelessWidget {
 | 
				
			||||||
  final PostWriteController controller;
 | 
					  final PostWriteController controller;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const PostMetaEditor({super.key, required this.controller});
 | 
					  const PostMetaEditor({super.key, required this.controller});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<DateTime?> _selectDate(
 | 
					  Future<DateTime?> _selectDate(
 | 
				
			||||||
@@ -87,28 +88,6 @@ class PostMetaEditor extends StatelessWidget {
 | 
				
			|||||||
          padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 8),
 | 
					          padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 8),
 | 
				
			||||||
          child: Column(
 | 
					          child: Column(
 | 
				
			||||||
            children: [
 | 
					            children: [
 | 
				
			||||||
              TextField(
 | 
					 | 
				
			||||||
                controller: controller.titleController,
 | 
					 | 
				
			||||||
                decoration: InputDecoration(
 | 
					 | 
				
			||||||
                  labelText: 'fieldPostTitle'.tr(),
 | 
					 | 
				
			||||||
                  border: UnderlineInputBorder(),
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
                onTapOutside: (_) =>
 | 
					 | 
				
			||||||
                    FocusManager.instance.primaryFocus?.unfocus(),
 | 
					 | 
				
			||||||
              ).padding(horizontal: 24),
 | 
					 | 
				
			||||||
              if (controller.mode == 'articles') const Gap(4),
 | 
					 | 
				
			||||||
              if (controller.mode == 'articles')
 | 
					 | 
				
			||||||
                TextField(
 | 
					 | 
				
			||||||
                  controller: controller.descriptionController,
 | 
					 | 
				
			||||||
                  maxLines: null,
 | 
					 | 
				
			||||||
                  decoration: InputDecoration(
 | 
					 | 
				
			||||||
                    labelText: 'fieldPostDescription'.tr(),
 | 
					 | 
				
			||||||
                    border: UnderlineInputBorder(),
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                  onTapOutside: (_) =>
 | 
					 | 
				
			||||||
                      FocusManager.instance.primaryFocus?.unfocus(),
 | 
					 | 
				
			||||||
                ).padding(horizontal: 24),
 | 
					 | 
				
			||||||
              const Gap(4),
 | 
					 | 
				
			||||||
              PostTagsField(
 | 
					              PostTagsField(
 | 
				
			||||||
                initialTags: controller.tags,
 | 
					                initialTags: controller.tags,
 | 
				
			||||||
                labelText: 'fieldPostTags'.tr(),
 | 
					                labelText: 'fieldPostTags'.tr(),
 | 
				
			||||||
@@ -133,8 +112,7 @@ class PostMetaEditor extends StatelessWidget {
 | 
				
			|||||||
                  helperMaxLines: 2,
 | 
					                  helperMaxLines: 2,
 | 
				
			||||||
                  border: UnderlineInputBorder(),
 | 
					                  border: UnderlineInputBorder(),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                onTapOutside: (_) =>
 | 
					                onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
                    FocusManager.instance.primaryFocus?.unfocus(),
 | 
					 | 
				
			||||||
              ).padding(horizontal: 24),
 | 
					              ).padding(horizontal: 24),
 | 
				
			||||||
              const Gap(12),
 | 
					              const Gap(12),
 | 
				
			||||||
              ListTile(
 | 
					              ListTile(
 | 
				
			||||||
@@ -182,8 +160,7 @@ class PostMetaEditor extends StatelessWidget {
 | 
				
			|||||||
                  leading: Icon(Symbols.person),
 | 
					                  leading: Icon(Symbols.person),
 | 
				
			||||||
                  trailing: Icon(Symbols.chevron_right),
 | 
					                  trailing: Icon(Symbols.chevron_right),
 | 
				
			||||||
                  title: Text('postVisibleUsers').tr(),
 | 
					                  title: Text('postVisibleUsers').tr(),
 | 
				
			||||||
                  subtitle: Text('postSelectedUsers')
 | 
					                  subtitle: Text('postSelectedUsers').plural(controller.visibleUsers.length),
 | 
				
			||||||
                      .plural(controller.visibleUsers.length),
 | 
					 | 
				
			||||||
                  onTap: () {
 | 
					                  onTap: () {
 | 
				
			||||||
                    _selectVisibleUser(context);
 | 
					                    _selectVisibleUser(context);
 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
@@ -194,8 +171,7 @@ class PostMetaEditor extends StatelessWidget {
 | 
				
			|||||||
                  leading: Icon(Symbols.person),
 | 
					                  leading: Icon(Symbols.person),
 | 
				
			||||||
                  trailing: Icon(Symbols.chevron_right),
 | 
					                  trailing: Icon(Symbols.chevron_right),
 | 
				
			||||||
                  title: Text('postInvisibleUsers').tr(),
 | 
					                  title: Text('postInvisibleUsers').tr(),
 | 
				
			||||||
                  subtitle: Text('postSelectedUsers')
 | 
					                  subtitle: Text('postSelectedUsers').plural(controller.invisibleUsers.length),
 | 
				
			||||||
                      .plural(controller.invisibleUsers.length),
 | 
					 | 
				
			||||||
                  onTap: () {
 | 
					                  onTap: () {
 | 
				
			||||||
                    _selectInvisibleUser(context);
 | 
					                    _selectInvisibleUser(context);
 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
@@ -204,9 +180,7 @@ class PostMetaEditor extends StatelessWidget {
 | 
				
			|||||||
                leading: const Icon(Symbols.event_available),
 | 
					                leading: const Icon(Symbols.event_available),
 | 
				
			||||||
                title: Text('postPublishedAt').tr(),
 | 
					                title: Text('postPublishedAt').tr(),
 | 
				
			||||||
                subtitle: Text(
 | 
					                subtitle: Text(
 | 
				
			||||||
                  controller.publishedAt != null
 | 
					                  controller.publishedAt != null ? dateFormatter.format(controller.publishedAt!) : 'unset'.tr(),
 | 
				
			||||||
                      ? dateFormatter.format(controller.publishedAt!)
 | 
					 | 
				
			||||||
                      : 'unset'.tr(),
 | 
					 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                trailing: controller.publishedAt != null
 | 
					                trailing: controller.publishedAt != null
 | 
				
			||||||
                    ? IconButton(
 | 
					                    ? IconButton(
 | 
				
			||||||
@@ -230,9 +204,7 @@ class PostMetaEditor extends StatelessWidget {
 | 
				
			|||||||
                leading: const Icon(Symbols.event_busy),
 | 
					                leading: const Icon(Symbols.event_busy),
 | 
				
			||||||
                title: Text('postPublishedUntil').tr(),
 | 
					                title: Text('postPublishedUntil').tr(),
 | 
				
			||||||
                subtitle: Text(
 | 
					                subtitle: Text(
 | 
				
			||||||
                  controller.publishedUntil != null
 | 
					                  controller.publishedUntil != null ? dateFormatter.format(controller.publishedUntil!) : 'unset'.tr(),
 | 
				
			||||||
                      ? dateFormatter.format(controller.publishedUntil!)
 | 
					 | 
				
			||||||
                      : 'unset'.tr(),
 | 
					 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                trailing: controller.publishedUntil != null
 | 
					                trailing: controller.publishedUntil != null
 | 
				
			||||||
                    ? IconButton(
 | 
					                    ? IconButton(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,6 @@ PODS:
 | 
				
			|||||||
  - bitsdojo_window_macos (0.0.1):
 | 
					  - bitsdojo_window_macos (0.0.1):
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
  - connectivity_plus (0.0.1):
 | 
					  - connectivity_plus (0.0.1):
 | 
				
			||||||
    - Flutter
 | 
					 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
  - croppy (0.0.1):
 | 
					  - croppy (0.0.1):
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
@@ -143,7 +142,7 @@ PODS:
 | 
				
			|||||||
    - HotKey
 | 
					    - HotKey
 | 
				
			||||||
  - in_app_review (2.0.0):
 | 
					  - in_app_review (2.0.0):
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
  - livekit_client (2.3.5):
 | 
					  - livekit_client (2.3.6):
 | 
				
			||||||
    - flutter_webrtc
 | 
					    - flutter_webrtc
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
    - WebRTC-SDK (= 125.6422.06)
 | 
					    - WebRTC-SDK (= 125.6422.06)
 | 
				
			||||||
@@ -190,7 +189,7 @@ PODS:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
DEPENDENCIES:
 | 
					DEPENDENCIES:
 | 
				
			||||||
  - bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`)
 | 
					  - bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`)
 | 
				
			||||||
  - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`)
 | 
					  - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`)
 | 
				
			||||||
  - croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/macos`)
 | 
					  - croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/macos`)
 | 
				
			||||||
  - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
 | 
					  - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
 | 
				
			||||||
  - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
 | 
					  - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
 | 
				
			||||||
@@ -244,7 +243,7 @@ EXTERNAL SOURCES:
 | 
				
			|||||||
  bitsdojo_window_macos:
 | 
					  bitsdojo_window_macos:
 | 
				
			||||||
    :path: Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos
 | 
					    :path: Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos
 | 
				
			||||||
  connectivity_plus:
 | 
					  connectivity_plus:
 | 
				
			||||||
    :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin
 | 
					    :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos
 | 
				
			||||||
  croppy:
 | 
					  croppy:
 | 
				
			||||||
    :path: Flutter/ephemeral/.symlinks/plugins/croppy/macos
 | 
					    :path: Flutter/ephemeral/.symlinks/plugins/croppy/macos
 | 
				
			||||||
  device_info_plus:
 | 
					  device_info_plus:
 | 
				
			||||||
@@ -308,7 +307,7 @@ EXTERNAL SOURCES:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
SPEC CHECKSUMS:
 | 
					SPEC CHECKSUMS:
 | 
				
			||||||
  bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00
 | 
					  bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00
 | 
				
			||||||
  connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
 | 
					  connectivity_plus: 0a976dfd033b59192912fa3c6c7b54aab5093802
 | 
				
			||||||
  croppy: 25a638bd7d05411d8c697f481568f261037694fc
 | 
					  croppy: 25a638bd7d05411d8c697f481568f261037694fc
 | 
				
			||||||
  device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
 | 
					  device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
 | 
				
			||||||
  file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af
 | 
					  file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af
 | 
				
			||||||
@@ -334,7 +333,7 @@ SPEC CHECKSUMS:
 | 
				
			|||||||
  HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277
 | 
					  HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277
 | 
				
			||||||
  hotkey_manager_macos: 1e2edb0c7ae4fe67108af44a9d3445de41404160
 | 
					  hotkey_manager_macos: 1e2edb0c7ae4fe67108af44a9d3445de41404160
 | 
				
			||||||
  in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93
 | 
					  in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93
 | 
				
			||||||
  livekit_client: 91c68237edede55f8891a166a28c1daec8a1e4b1
 | 
					  livekit_client: 0ad107154753a5a76802d2222c040223ad049499
 | 
				
			||||||
  media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
 | 
					  media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
 | 
				
			||||||
  media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
 | 
					  media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
 | 
				
			||||||
  media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
 | 
					  media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										48
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										48
									
								
								pubspec.lock
									
									
									
									
									
								
							@@ -214,6 +214,14 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.4.3"
 | 
					    version: "0.4.3"
 | 
				
			||||||
 | 
					  chalkdart:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: chalkdart
 | 
				
			||||||
 | 
					      sha256: "08c910ee341fcdd1e46f20ddce59b13c1d85f5d97f2fd2f12014c46ede670e40"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "2.3.2"
 | 
				
			||||||
  characters:
 | 
					  characters:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -266,10 +274,10 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: connectivity_plus
 | 
					      name: connectivity_plus
 | 
				
			||||||
      sha256: "8a68739d3ee113e51ad35583fdf9ab82c55d09d693d3c39da1aebab87c938412"
 | 
					      sha256: "04bf81bb0b77de31557b58d052b24b3eee33f09a6e7a8c68a3e247c7df19ec27"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "6.1.2"
 | 
					    version: "6.1.3"
 | 
				
			||||||
  connectivity_plus_platform_interface:
 | 
					  connectivity_plus_platform_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -362,10 +370,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: device_info_plus
 | 
					      name: device_info_plus
 | 
				
			||||||
      sha256: e3fc9a65820fef83035af8ee8c09004a719d5d1d54e6de978fcb0d84bbeb241a
 | 
					      sha256: "72d146c6d7098689ff5c5f66bcf593ac11efc530095385356e131070333e64da"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "11.2.2"
 | 
					    version: "11.3.0"
 | 
				
			||||||
  device_info_plus_platform_interface:
 | 
					  device_info_plus_platform_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -490,10 +498,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: file_picker
 | 
					      name: file_picker
 | 
				
			||||||
      sha256: c9943dd7d702ab4199d199bc151a2d79c86db031a02ad84566dab58c494d2adc
 | 
					      sha256: cacfdc5abe93e64d418caa9256eef663499ad791bb688d9fd12c85a311968fba
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "8.3.1"
 | 
					    version: "8.3.2"
 | 
				
			||||||
  file_saver:
 | 
					  file_saver:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -830,10 +838,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: flutter_webrtc
 | 
					      name: flutter_webrtc
 | 
				
			||||||
      sha256: "4c76069f724f79dc6e8739c8f16c2ee00ca30a1f8e3aa75c0e830f8183278f03"
 | 
					      sha256: e917067abeef2400e6a7a03db53a6e1418551e54809f18ab80447ac323eb77e4
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.12.7"
 | 
					    version: "0.12.8"
 | 
				
			||||||
  freezed:
 | 
					  freezed:
 | 
				
			||||||
    dependency: "direct dev"
 | 
					    dependency: "direct dev"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -886,10 +894,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: go_router
 | 
					      name: go_router
 | 
				
			||||||
      sha256: "9b736a9fa879d8ad6df7932cbdcc58237c173ab004ef90d8377923d7ad731eaa"
 | 
					      sha256: "04539267a740931c6d4479a10d466717ca5901c6fdfd3fcda09391bbb8ebd651"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "14.7.2"
 | 
					    version: "14.8.0"
 | 
				
			||||||
  google_fonts:
 | 
					  google_fonts:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -934,10 +942,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: home_widget
 | 
					      name: home_widget
 | 
				
			||||||
      sha256: b313e3304c0429669fddf1286e1fbf61a64b873f38ba30b3eb890ef0d7560b12
 | 
					      sha256: "7430f7549d42cef2e729bd3c779de748b93f1eb78b1abfe6bca8fffd1cfce3e9"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.7.0"
 | 
					    version: "0.7.0+1"
 | 
				
			||||||
  hotkey_manager:
 | 
					  hotkey_manager:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1190,10 +1198,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: livekit_client
 | 
					      name: livekit_client
 | 
				
			||||||
      sha256: "02b4653d903852d0ae86b15fbe4324747606dae6410fe860d0c07a11c79988de"
 | 
					      sha256: "0cfb2f48eff7a93ea8927696dc6f218aebd2fcd1fcc1b1a7b2f53ff3597fdb52"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.3.5"
 | 
					    version: "2.3.6"
 | 
				
			||||||
  logging:
 | 
					  logging:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1246,10 +1254,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: material_symbols_icons
 | 
					      name: material_symbols_icons
 | 
				
			||||||
      sha256: "89aac72d25dd49303f71b3b1e70f8374791846729365b25bebc2a2531e5b86cd"
 | 
					      sha256: "1403944c2a68dbdf934fca2b2115a372e70a4a185c136ab71a9d16e2108d0b14"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "4.2801.1"
 | 
					    version: "4.2805.1"
 | 
				
			||||||
  media_kit:
 | 
					  media_kit:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1382,10 +1390,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: package_info_plus
 | 
					      name: package_info_plus
 | 
				
			||||||
      sha256: c447a3c3e7be4addf129b8f9ab6a4bd5d166b78918223e223b61fddf4d07e254
 | 
					      sha256: "67eae327b1b0faf761964a1d2e5d323c797f3799db0e85aa232db8d9e922bc35"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "8.2.0"
 | 
					    version: "8.2.1"
 | 
				
			||||||
  package_info_plus_platform_interface:
 | 
					  package_info_plus_platform_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -2251,10 +2259,10 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: webrtc_interface
 | 
					      name: webrtc_interface
 | 
				
			||||||
      sha256: abec3ab7956bd5ac539cf34a42fa0c82ea26675847c0966bb85160400eea9388
 | 
					      sha256: "10fc6dc0ac16f909f5e434c18902415211d759313c87261f1e4ec5b4f6a04c26"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.2.0"
 | 
					    version: "1.2.1"
 | 
				
			||||||
  win32:
 | 
					  win32:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
 | 
				
			|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
					# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
				
			||||||
# In Windows, build-name is used as the major, minor, and patch parts
 | 
					# In Windows, build-name is used as the major, minor, and patch parts
 | 
				
			||||||
# of the product and file versions while build-number is used as the build suffix.
 | 
					# of the product and file versions while build-number is used as the build suffix.
 | 
				
			||||||
version: 2.3.2+63
 | 
					version: 2.3.2+65
 | 
				
			||||||
 | 
					
 | 
				
			||||||
environment:
 | 
					environment:
 | 
				
			||||||
  sdk: ^3.5.4
 | 
					  sdk: ^3.5.4
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user