Compare commits
	
		
			22 Commits
		
	
	
		
			2.4.2+76
			...
			e88dea8858
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| e88dea8858 | |||
| 813679b161 | |||
| 9d4ce6ca8c | |||
| 88396647f3 | |||
| 335318ae3f | |||
| da25fb9c29 | |||
| c1aef89b84 | |||
| 0241c5f804 | |||
| f6939d7c23 | |||
| d654c162e3 | |||
| 25550ba197 | |||
| 3defd3a593 | |||
| d62ed4c375 | |||
| 857f3cc832 | |||
| e16bc80eea | |||
| a4f6e8af56 | |||
| 060a97f5ec | |||
| 92f7e92018 | |||
| 5c483bd3b8 | |||
| 1c510d63fe | |||
| 115cb4adc1 | |||
| 54c098c274 | 
@@ -15,12 +15,10 @@ body:json {
 | 
			
		||||
    "client_id": "{{third_client_id}}",
 | 
			
		||||
    "client_secret":"{{third_client_tk}}",
 | 
			
		||||
    "type": "general",
 | 
			
		||||
    "subject": "新年快乐!",
 | 
			
		||||
    "subtitle": "一条来自 Solar Network 团队的信息",
 | 
			
		||||
    "content": "今天是农历正月初一,小羊祝您新年快乐 🎉",
 | 
			
		||||
    "metadata": {
 | 
			
		||||
      "image": "D2EDbcrsTugs3xk5"
 | 
			
		||||
    },
 | 
			
		||||
    "subject": "关于迁移服务器完成的提示",
 | 
			
		||||
    "subtitle": "一条来自 Solar Network 团队的运营信息",
 | 
			
		||||
    "content": "我们已经将所有用户数据迁移到新版服务器,刚刚发布新的 DNS,因为部分 DNS 缓存的影响。可能更改不会生效,可以使用 nslookup / ping 检查解析地址是否未 8. 开头,您可以主动刷新 DNS。谢谢!",
 | 
			
		||||
    "metadata": {},
 | 
			
		||||
    "priority": 10
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								assets/fonts/Nunito-Bold.ttf
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/fonts/Nunito-Bold.ttf
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/fonts/Nunito-Italic.ttf
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/fonts/Nunito-Italic.ttf
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/fonts/Nunito-Regular.ttf
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/fonts/Nunito-Regular.ttf
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							@@ -153,6 +153,11 @@
 | 
			
		||||
  "publisherRunBy": "Run by {}",
 | 
			
		||||
  "fieldPublisherBelongToRealm": "Belongs to",
 | 
			
		||||
  "fieldPublisherBelongToRealmUnset": "Unset Publisher Belongs to Realm",
 | 
			
		||||
  "writePost": "Compose",
 | 
			
		||||
  "postTypeStory": "Story",
 | 
			
		||||
  "postTypeArticle": "Article",
 | 
			
		||||
  "postTypeQuestion": "Question",
 | 
			
		||||
  "postTypeVideo": "Video",
 | 
			
		||||
  "writePostTypeStory": "Post a story",
 | 
			
		||||
  "writePostTypeArticle": "Write an article",
 | 
			
		||||
  "writePostTypeQuestion": "Ask a question",
 | 
			
		||||
@@ -763,5 +768,28 @@
 | 
			
		||||
  "decrypting": "Decrypting……",
 | 
			
		||||
  "decryptingKeyNotFound": "Key not found or exchange failed, the other party may not be online",
 | 
			
		||||
  "messageUnablePreview": "Unable preview",
 | 
			
		||||
  "messageUnablePreviewEncrypted": "Unable preview encrypted message"
 | 
			
		||||
  "messageUnablePreviewEncrypted": "Unable preview encrypted message",
 | 
			
		||||
  "postViewInGlobalDescription": "Do not view the post in the specific realm.",
 | 
			
		||||
  "postDraftSaved": "The draft has been saved.",
 | 
			
		||||
  "postDraftBox": "Draft Box",
 | 
			
		||||
  "postShuffle": "Read Randomly",
 | 
			
		||||
  "checkInStreak": {
 | 
			
		||||
    "zero": "No streak",
 | 
			
		||||
    "one": "{} day streak",
 | 
			
		||||
    "other": "{} days streak"
 | 
			
		||||
  },
 | 
			
		||||
  "accountChangeStatus": "Change Status",
 | 
			
		||||
  "accountStatusSilent": "Do not Disturb",
 | 
			
		||||
  "accountStatusSilentDesc": "The notification will stop popping up",
 | 
			
		||||
  "accountStatusInvisible": "Invisible",
 | 
			
		||||
  "accountStatusInvisibleDesc": "Will show as offline, but all features still remain normal",
 | 
			
		||||
  "accountCustomStatus": "Custom Status",
 | 
			
		||||
  "accountCustomStatusDescription": "Customize your status.",
 | 
			
		||||
  "accountClearStatus": "Clear Status",
 | 
			
		||||
  "accountClearStatusDescription": "Clear your status, and let server decide which status you are for you.",
 | 
			
		||||
  "fieldAccountStatusLabel": "Status Text",
 | 
			
		||||
  "fieldAccountStatusClearAt": "Clear At",
 | 
			
		||||
  "accountStatusNegative": "Negative",
 | 
			
		||||
  "accountStatusNeutral": "Neutral",
 | 
			
		||||
  "accountStatusPositive": "Positive"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -137,6 +137,11 @@
 | 
			
		||||
  "publisherRunBy": "由 {} 管理",
 | 
			
		||||
  "fieldPublisherBelongToRealm": "所属领域",
 | 
			
		||||
  "fieldPublisherBelongToRealmUnset": "未设置发布者所属领域",
 | 
			
		||||
  "writePost": "撰写",
 | 
			
		||||
  "postTypeStory": "动态",
 | 
			
		||||
  "postTypeArticle": "文章",
 | 
			
		||||
  "postTypeQuestion": "问题",
 | 
			
		||||
  "postTypeVideo": "视频",
 | 
			
		||||
  "writePostTypeStory": "发动态",
 | 
			
		||||
  "writePostTypeArticle": "写文章",
 | 
			
		||||
  "writePostTypeQuestion": "提问题",
 | 
			
		||||
@@ -761,5 +766,28 @@
 | 
			
		||||
  "decrypting": "解密中……",
 | 
			
		||||
  "decryptingKeyNotFound": "未找到密钥对或交换失败,对方可能不在线",
 | 
			
		||||
  "messageUnablePreview": "无法预览消息",
 | 
			
		||||
  "messageUnablePreviewEncrypted": "无法预览加密消息"
 | 
			
		||||
  "messageUnablePreviewEncrypted": "无法预览加密消息",
 | 
			
		||||
  "postViewInGlobalDescription": "不查看特定领域的帖子。",
 | 
			
		||||
  "postDraftSaved": "已保存为草稿。",
 | 
			
		||||
  "postDraftBox": "草稿箱",
 | 
			
		||||
  "postShuffle": "随便看看",
 | 
			
		||||
  "checkInStreak": {
 | 
			
		||||
    "zero": "无连击",
 | 
			
		||||
    "one": "连续签到 {} 天",
 | 
			
		||||
    "other": "连续签到 {} 天"
 | 
			
		||||
  },
 | 
			
		||||
  "accountChangeStatus": "修改状态",
 | 
			
		||||
  "accountStatusSilent": "请勿打扰",
 | 
			
		||||
  "accountStatusSilentDesc": "将会暂停所有通知推送",
 | 
			
		||||
  "accountStatusInvisible": "隐身",
 | 
			
		||||
  "accountStatusInvisibleDesc": "将会在他人界面显示离线,但不影响功能使用",
 | 
			
		||||
  "accountCustomStatus": "自定义状态",
 | 
			
		||||
  "accountCustomStatusDescription": "客制化你的状态。",
 | 
			
		||||
  "accountClearStatus": "清除状态",
 | 
			
		||||
  "accountClearStatusDescription": "清除你的状态,并让服务器决定你的状态。",
 | 
			
		||||
  "fieldAccountStatusLabel": "状态文字",
 | 
			
		||||
  "fieldAccountStatusClearAt": "清除时间",
 | 
			
		||||
  "accountStatusNegative": "负面",
 | 
			
		||||
  "accountStatusNeutral": "中性",
 | 
			
		||||
  "accountStatusPositive": "正面"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -137,6 +137,11 @@
 | 
			
		||||
  "publisherRunBy": "由 {} 管理",
 | 
			
		||||
  "fieldPublisherBelongToRealm": "所屬領域",
 | 
			
		||||
  "fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
 | 
			
		||||
  "writePost": "撰寫",
 | 
			
		||||
  "postTypeStory": "動態",
 | 
			
		||||
  "postTypeArticle": "文章",
 | 
			
		||||
  "postTypeQuestion": "問題",
 | 
			
		||||
  "postTypeVideo": "視頻",
 | 
			
		||||
  "writePostTypeStory": "發動態",
 | 
			
		||||
  "writePostTypeArticle": "寫文章",
 | 
			
		||||
  "writePostTypeQuestion": "提問題",
 | 
			
		||||
@@ -761,5 +766,28 @@
 | 
			
		||||
  "decrypting": "解密中……",
 | 
			
		||||
  "decryptingKeyNotFound": "未找到密鑰對或交換失敗,對方可能不在線",
 | 
			
		||||
  "messageUnablePreview": "無法預覽消息",
 | 
			
		||||
  "messageUnablePreviewEncrypted": "無法預覽加密消息"
 | 
			
		||||
  "messageUnablePreviewEncrypted": "無法預覽加密消息",
 | 
			
		||||
  "postViewInGlobalDescription": "不查看特定領域的帖子。",
 | 
			
		||||
  "postDraftSaved": "已保存為草稿。",
 | 
			
		||||
  "postDraftBox": "草稿箱",
 | 
			
		||||
  "postShuffle": "隨便看看",
 | 
			
		||||
  "checkInStreak": {
 | 
			
		||||
    "zero": "無連擊",
 | 
			
		||||
    "one": "連續簽到 {} 天",
 | 
			
		||||
    "other": "連續簽到 {} 天"
 | 
			
		||||
  },
 | 
			
		||||
  "accountChangeStatus": "修改狀態",
 | 
			
		||||
  "accountStatusSilent": "請勿打擾",
 | 
			
		||||
  "accountStatusSilentDesc": "將會暫停所有通知推送",
 | 
			
		||||
  "accountStatusInvisible": "隱身",
 | 
			
		||||
  "accountStatusInvisibleDesc": "將會在他人界面顯示離線,但不影響功能使用",
 | 
			
		||||
  "accountCustomStatus": "自定義狀態",
 | 
			
		||||
  "accountCustomStatusDescription": "客製化你的狀態。",
 | 
			
		||||
  "accountClearStatus": "清除狀態",
 | 
			
		||||
  "accountClearStatusDescription": "清除你的狀態,並讓服務器決定你的狀態。",
 | 
			
		||||
  "fieldAccountStatusLabel": "狀態文字",
 | 
			
		||||
  "fieldAccountStatusClearAt": "清除時間",
 | 
			
		||||
  "accountStatusNegative": "負面",
 | 
			
		||||
  "accountStatusNeutral": "中性",
 | 
			
		||||
  "accountStatusPositive": "正面"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -137,6 +137,11 @@
 | 
			
		||||
  "publisherRunBy": "由 {} 管理",
 | 
			
		||||
  "fieldPublisherBelongToRealm": "所屬領域",
 | 
			
		||||
  "fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
 | 
			
		||||
  "writePost": "撰寫",
 | 
			
		||||
  "postTypeStory": "動態",
 | 
			
		||||
  "postTypeArticle": "文章",
 | 
			
		||||
  "postTypeQuestion": "問題",
 | 
			
		||||
  "postTypeVideo": "視頻",
 | 
			
		||||
  "writePostTypeStory": "發動態",
 | 
			
		||||
  "writePostTypeArticle": "寫文章",
 | 
			
		||||
  "writePostTypeQuestion": "提問題",
 | 
			
		||||
@@ -761,5 +766,28 @@
 | 
			
		||||
  "decrypting": "解密中……",
 | 
			
		||||
  "decryptingKeyNotFound": "未找到密鑰對或交換失敗,對方可能不在線",
 | 
			
		||||
  "messageUnablePreview": "無法預覽消息",
 | 
			
		||||
  "messageUnablePreviewEncrypted": "無法預覽加密消息"
 | 
			
		||||
  "messageUnablePreviewEncrypted": "無法預覽加密消息",
 | 
			
		||||
  "postViewInGlobalDescription": "不查看特定領域的帖子。",
 | 
			
		||||
  "postDraftSaved": "已保存為草稿。",
 | 
			
		||||
  "postDraftBox": "草稿箱",
 | 
			
		||||
  "postShuffle": "隨便看看",
 | 
			
		||||
  "checkInStreak": {
 | 
			
		||||
    "zero": "無連擊",
 | 
			
		||||
    "one": "連續簽到 {} 天",
 | 
			
		||||
    "other": "連續簽到 {} 天"
 | 
			
		||||
  },
 | 
			
		||||
  "accountChangeStatus": "修改狀態",
 | 
			
		||||
  "accountStatusSilent": "請勿打擾",
 | 
			
		||||
  "accountStatusSilentDesc": "將會暫停所有通知推送",
 | 
			
		||||
  "accountStatusInvisible": "隱身",
 | 
			
		||||
  "accountStatusInvisibleDesc": "將會在他人界面顯示離線,但不影響功能使用",
 | 
			
		||||
  "accountCustomStatus": "自定義狀態",
 | 
			
		||||
  "accountCustomStatusDescription": "客製化你的狀態。",
 | 
			
		||||
  "accountClearStatus": "清除狀態",
 | 
			
		||||
  "accountClearStatusDescription": "清除你的狀態,並讓服務器決定你的狀態。",
 | 
			
		||||
  "fieldAccountStatusLabel": "狀態文字",
 | 
			
		||||
  "fieldAccountStatusClearAt": "清除時間",
 | 
			
		||||
  "accountStatusNegative": "負面",
 | 
			
		||||
  "accountStatusNeutral": "中性",
 | 
			
		||||
  "accountStatusPositive": "正面"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -71,7 +71,8 @@ class PostWriteMedia {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  PostWriteMedia.fromBytes(this.raw, this.name, this.type, {this.attachment, this.file});
 | 
			
		||||
  PostWriteMedia.fromBytes(this.raw, this.name, this.type,
 | 
			
		||||
      {this.attachment, this.file});
 | 
			
		||||
 | 
			
		||||
  bool get isEmpty => attachment == null && file == null && raw == null;
 | 
			
		||||
 | 
			
		||||
@@ -105,7 +106,8 @@ class PostWriteMedia {
 | 
			
		||||
  }) {
 | 
			
		||||
    if (attachment != null) {
 | 
			
		||||
      final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
      final ImageProvider provider = UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
 | 
			
		||||
      final ImageProvider provider =
 | 
			
		||||
          UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
 | 
			
		||||
      if (width != null && height != null && !kIsWeb) {
 | 
			
		||||
        return ResizeImage(
 | 
			
		||||
          provider,
 | 
			
		||||
@@ -116,7 +118,8 @@ class PostWriteMedia {
 | 
			
		||||
      }
 | 
			
		||||
      return provider;
 | 
			
		||||
    } else if (file != null) {
 | 
			
		||||
      final ImageProvider provider = kIsWeb ? NetworkImage(file!.path) : FileImage(File(file!.path));
 | 
			
		||||
      final ImageProvider provider =
 | 
			
		||||
          kIsWeb ? NetworkImage(file!.path) : FileImage(File(file!.path));
 | 
			
		||||
      if (width != null && height != null) {
 | 
			
		||||
        return ResizeImage(
 | 
			
		||||
          provider,
 | 
			
		||||
@@ -159,11 +162,14 @@ class PostWriteController extends ChangeNotifier {
 | 
			
		||||
  final TextEditingController aliasController = TextEditingController();
 | 
			
		||||
  final TextEditingController rewardController = TextEditingController();
 | 
			
		||||
 | 
			
		||||
  ContentInsertionConfiguration get contentInsertionConfiguration => ContentInsertionConfiguration(
 | 
			
		||||
  ContentInsertionConfiguration get contentInsertionConfiguration =>
 | 
			
		||||
      ContentInsertionConfiguration(
 | 
			
		||||
        onContentInserted: (KeyboardInsertedContent content) {
 | 
			
		||||
          if (content.hasData) {
 | 
			
		||||
            addAttachments(
 | 
			
		||||
                [PostWriteMedia.fromBytes(content.data!, 'attachmentInsertedImage'.tr(), SnMediaType.image)]);
 | 
			
		||||
            addAttachments([
 | 
			
		||||
              PostWriteMedia.fromBytes(content.data!,
 | 
			
		||||
                  'attachmentInsertedImage'.tr(), SnMediaType.image)
 | 
			
		||||
            ]);
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
@@ -193,7 +199,8 @@ class PostWriteController extends ChangeNotifier {
 | 
			
		||||
 | 
			
		||||
  String get description => descriptionController.text;
 | 
			
		||||
 | 
			
		||||
  bool get isRelatedNull => ![editingPost, repostingPost, replyingPost].any((ele) => ele != null);
 | 
			
		||||
  bool get isRelatedNull =>
 | 
			
		||||
      ![editingPost, repostingPost, replyingPost].any((ele) => ele != null);
 | 
			
		||||
 | 
			
		||||
  bool isLoading = false, isBusy = false;
 | 
			
		||||
  double? progress;
 | 
			
		||||
@@ -201,6 +208,7 @@ class PostWriteController extends ChangeNotifier {
 | 
			
		||||
  SnRealm? realm;
 | 
			
		||||
  SnPublisher? publisher;
 | 
			
		||||
  SnPost? editingPost, repostingPost, replyingPost;
 | 
			
		||||
  bool editingDraft = false;
 | 
			
		||||
 | 
			
		||||
  int visibility = 0;
 | 
			
		||||
  List<int> visibleUsers = List.empty();
 | 
			
		||||
@@ -237,14 +245,20 @@ class PostWriteController extends ChangeNotifier {
 | 
			
		||||
        publishedAt = post.publishedAt;
 | 
			
		||||
        publishedUntil = post.publishedUntil;
 | 
			
		||||
        visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
 | 
			
		||||
        invisibleUsers = List.from(post.invisibleUsersList ?? [], growable: true);
 | 
			
		||||
        invisibleUsers =
 | 
			
		||||
            List.from(post.invisibleUsersList ?? [], growable: true);
 | 
			
		||||
        visibility = post.visibility;
 | 
			
		||||
        tags = List.from(post.tags.map((ele) => ele.alias), growable: true);
 | 
			
		||||
        categories = List.from(post.categories.map((ele) => ele.alias), growable: true);
 | 
			
		||||
        attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
 | 
			
		||||
        categories =
 | 
			
		||||
            List.from(post.categories.map((ele) => ele.alias), growable: true);
 | 
			
		||||
        attachments.addAll(
 | 
			
		||||
            post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
 | 
			
		||||
        poll = post.preload?.poll;
 | 
			
		||||
 | 
			
		||||
        if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
 | 
			
		||||
        editingDraft = post.isDraft;
 | 
			
		||||
 | 
			
		||||
        if (post.preload?.thumbnail != null &&
 | 
			
		||||
            (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
 | 
			
		||||
          thumbnail = PostWriteMedia(post.preload!.thumbnail);
 | 
			
		||||
        }
 | 
			
		||||
        if (post.preload?.realm != null) {
 | 
			
		||||
@@ -272,7 +286,8 @@ class PostWriteController extends ChangeNotifier {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<SnAttachment> _uploadAttachment(BuildContext context, PostWriteMedia media,
 | 
			
		||||
  Future<SnAttachment> _uploadAttachment(
 | 
			
		||||
      BuildContext context, PostWriteMedia media,
 | 
			
		||||
      {bool isCompressed = false}) async {
 | 
			
		||||
    final attach = context.read<SnAttachmentProvider>();
 | 
			
		||||
 | 
			
		||||
@@ -281,7 +296,9 @@ class PostWriteController extends ChangeNotifier {
 | 
			
		||||
      media.name,
 | 
			
		||||
      'interactive',
 | 
			
		||||
      null,
 | 
			
		||||
      mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
 | 
			
		||||
      mimetype: media.raw != null && media.type == SnMediaType.image
 | 
			
		||||
          ? 'image/png'
 | 
			
		||||
          : null,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    var item = await attach.chunkedUploadParts(
 | 
			
		||||
@@ -297,9 +314,11 @@ class PostWriteController extends ChangeNotifier {
 | 
			
		||||
 | 
			
		||||
    if (media.type == SnMediaType.video && !isCompressed && context.mounted) {
 | 
			
		||||
      try {
 | 
			
		||||
        final compressedAttachment = await _tryCompressVideoCopy(context, media);
 | 
			
		||||
        final compressedAttachment =
 | 
			
		||||
            await _tryCompressVideoCopy(context, media);
 | 
			
		||||
        if (compressedAttachment != null) {
 | 
			
		||||
          item = await attach.updateOne(item, compressedId: compressedAttachment.id);
 | 
			
		||||
          item = await attach.updateOne(item,
 | 
			
		||||
              compressedId: compressedAttachment.id);
 | 
			
		||||
        }
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
        if (context.mounted) context.showErrorDialog(err);
 | 
			
		||||
@@ -309,8 +328,10 @@ class PostWriteController extends ChangeNotifier {
 | 
			
		||||
    return item;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<SnAttachment?> _tryCompressVideoCopy(BuildContext context, PostWriteMedia media) async {
 | 
			
		||||
    if (kIsWeb || !(Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) return null;
 | 
			
		||||
  Future<SnAttachment?> _tryCompressVideoCopy(
 | 
			
		||||
      BuildContext context, PostWriteMedia media) async {
 | 
			
		||||
    if (kIsWeb || !(Platform.isAndroid || Platform.isIOS || Platform.isMacOS))
 | 
			
		||||
      return null;
 | 
			
		||||
    if (media.type != SnMediaType.video) return null;
 | 
			
		||||
    if (media.file == null) return null;
 | 
			
		||||
    if (VideoCompress.isCompressing) return null;
 | 
			
		||||
@@ -334,7 +355,8 @@ class PostWriteController extends ChangeNotifier {
 | 
			
		||||
    if (!context.mounted) return null;
 | 
			
		||||
 | 
			
		||||
    final compressedMedia = PostWriteMedia.fromFile(XFile(mediaInfo.path!));
 | 
			
		||||
    final compressedAttachment = await _uploadAttachment(context, compressedMedia, isCompressed: true);
 | 
			
		||||
    final compressedAttachment =
 | 
			
		||||
        await _uploadAttachment(context, compressedMedia, isCompressed: true);
 | 
			
		||||
 | 
			
		||||
    return compressedAttachment;
 | 
			
		||||
  }
 | 
			
		||||
@@ -370,18 +392,25 @@ class PostWriteController extends ChangeNotifier {
 | 
			
		||||
          'content': contentController.text,
 | 
			
		||||
          if (aliasController.text.isNotEmpty) 'alias': aliasController.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(),
 | 
			
		||||
          'attachments':
 | 
			
		||||
              attachments.where((e) => e.attachment != null).map((e) => e.attachment!.toJson()).toList(growable: true),
 | 
			
		||||
          if (thumbnail != null && thumbnail!.attachment != null)
 | 
			
		||||
            'thumbnail': thumbnail!.attachment!.toJson(),
 | 
			
		||||
          'attachments': attachments
 | 
			
		||||
              .where((e) => e.attachment != null)
 | 
			
		||||
              .map((e) => e.attachment!.toJson())
 | 
			
		||||
              .toList(growable: true),
 | 
			
		||||
          'tags': tags.map((ele) => {'alias': ele}).toList(growable: true),
 | 
			
		||||
          'categories': categories.map((ele) => {'alias': ele}).toList(growable: true),
 | 
			
		||||
          'categories':
 | 
			
		||||
              categories.map((ele) => {'alias': ele}).toList(growable: true),
 | 
			
		||||
          'visibility': visibility,
 | 
			
		||||
          'visible_users_list': visibleUsers,
 | 
			
		||||
          'invisible_users_list': invisibleUsers,
 | 
			
		||||
          if (publishedAt != null) 'published_at': publishedAt!.toUtc().toIso8601String(),
 | 
			
		||||
          if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
 | 
			
		||||
          if (publishedAt != null)
 | 
			
		||||
            'published_at': publishedAt!.toUtc().toIso8601String(),
 | 
			
		||||
          if (publishedUntil != null)
 | 
			
		||||
            'published_until': publishedAt!.toUtc().toIso8601String(),
 | 
			
		||||
          if (replyingPost != null) 'reply_to': replyingPost!.toJson(),
 | 
			
		||||
          if (repostingPost != null) 'repost_to': repostingPost!.toJson(),
 | 
			
		||||
          if (poll != null) 'poll': poll!.toJson(),
 | 
			
		||||
@@ -391,6 +420,12 @@ class PostWriteController extends ChangeNotifier {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool get isNotEmpty =>
 | 
			
		||||
      title.isNotEmpty ||
 | 
			
		||||
      description.isNotEmpty ||
 | 
			
		||||
      contentController.text.isNotEmpty ||
 | 
			
		||||
      attachments.isNotEmpty;
 | 
			
		||||
 | 
			
		||||
  bool temporaryRestored = false;
 | 
			
		||||
 | 
			
		||||
  void _temporaryLoad() {
 | 
			
		||||
@@ -403,18 +438,24 @@ class PostWriteController extends ChangeNotifier {
 | 
			
		||||
      titleController.text = data['title'] ?? '';
 | 
			
		||||
      descriptionController.text = data['description'] ?? '';
 | 
			
		||||
      rewardController.text = data['reward']?.toString() ?? '';
 | 
			
		||||
      if (data['thumbnail'] != null) thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
 | 
			
		||||
      attachments
 | 
			
		||||
          .addAll(data['attachments'].map((ele) => PostWriteMedia(SnAttachment.fromJson(ele))).cast<PostWriteMedia>());
 | 
			
		||||
      if (data['thumbnail'] != null)
 | 
			
		||||
        thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
 | 
			
		||||
      attachments.addAll(data['attachments']
 | 
			
		||||
          .map((ele) => PostWriteMedia(SnAttachment.fromJson(ele)))
 | 
			
		||||
          .cast<PostWriteMedia>());
 | 
			
		||||
      tags = List.from(data['tags'].map((ele) => ele['alias']));
 | 
			
		||||
      categories = List.from(data['categories'].map((ele) => ele['alias']));
 | 
			
		||||
      visibility = data['visibility'];
 | 
			
		||||
      visibleUsers = List.from(data['visible_users_list'] ?? []);
 | 
			
		||||
      invisibleUsers = List.from(data['invisible_users_list'] ?? []);
 | 
			
		||||
      if (data['published_at'] != null) publishedAt = DateTime.tryParse(data['published_at'])?.toLocal();
 | 
			
		||||
      if (data['published_until'] != null) publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal();
 | 
			
		||||
      replyingPost = data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null;
 | 
			
		||||
      repostingPost = data['repost_to'] != null ? SnPost.fromJson(data['repost_to']) : null;
 | 
			
		||||
      if (data['published_at'] != null)
 | 
			
		||||
        publishedAt = DateTime.tryParse(data['published_at'])?.toLocal();
 | 
			
		||||
      if (data['published_until'] != null)
 | 
			
		||||
        publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal();
 | 
			
		||||
      replyingPost =
 | 
			
		||||
          data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null;
 | 
			
		||||
      repostingPost =
 | 
			
		||||
          data['repost_to'] != null ? SnPost.fromJson(data['repost_to']) : null;
 | 
			
		||||
      poll = data['poll'] != null ? SnPoll.fromJson(data['poll']) : null;
 | 
			
		||||
      realm = data['realm'] != null ? SnRealm.fromJson(data['realm']) : null;
 | 
			
		||||
      temporaryRestored = true;
 | 
			
		||||
@@ -436,7 +477,10 @@ class PostWriteController extends ChangeNotifier {
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> sendPost(BuildContext context) async {
 | 
			
		||||
  Future<void> sendPost(
 | 
			
		||||
    BuildContext context, {
 | 
			
		||||
    bool saveAsDraft = false,
 | 
			
		||||
  }) async {
 | 
			
		||||
    if (isBusy || publisher == null) return;
 | 
			
		||||
 | 
			
		||||
    final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
@@ -463,7 +507,9 @@ class PostWriteController extends ChangeNotifier {
 | 
			
		||||
          media.name,
 | 
			
		||||
          'interactive',
 | 
			
		||||
          null,
 | 
			
		||||
          mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
 | 
			
		||||
          mimetype: media.raw != null && media.type == SnMediaType.image
 | 
			
		||||
              ? 'image/png'
 | 
			
		||||
              : null,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        var item = await attach.chunkedUploadParts(
 | 
			
		||||
@@ -472,16 +518,20 @@ class PostWriteController extends ChangeNotifier {
 | 
			
		||||
          place.$2,
 | 
			
		||||
          onProgress: (value) {
 | 
			
		||||
            // Calculate overall progress for attachments
 | 
			
		||||
            progress = math.max(((i + value) / attachments.length) * kAttachmentProgressWeight, value);
 | 
			
		||||
            progress = math.max(
 | 
			
		||||
                ((i + value) / attachments.length) * kAttachmentProgressWeight,
 | 
			
		||||
                value);
 | 
			
		||||
            notifyListeners();
 | 
			
		||||
          },
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
          if (context.mounted) {
 | 
			
		||||
            final compressedAttachment = await _tryCompressVideoCopy(context, media);
 | 
			
		||||
            final compressedAttachment =
 | 
			
		||||
                await _tryCompressVideoCopy(context, media);
 | 
			
		||||
            if (compressedAttachment != null) {
 | 
			
		||||
              item = await attach.updateOne(item, compressedId: compressedAttachment.id);
 | 
			
		||||
              item = await attach.updateOne(item,
 | 
			
		||||
                  compressedId: compressedAttachment.id);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        } catch (err) {
 | 
			
		||||
@@ -508,7 +558,7 @@ class PostWriteController extends ChangeNotifier {
 | 
			
		||||
    // Posting the content
 | 
			
		||||
    try {
 | 
			
		||||
      final baseProgressVal = progress!;
 | 
			
		||||
      await sn.client.request(
 | 
			
		||||
      final resp = await sn.client.request(
 | 
			
		||||
        [
 | 
			
		||||
          '/cgi/co/$mode',
 | 
			
		||||
          if (editingPost != null) '${editingPost!.id}',
 | 
			
		||||
@@ -518,36 +568,56 @@ class PostWriteController extends ChangeNotifier {
 | 
			
		||||
          'content': contentController.text,
 | 
			
		||||
          if (aliasController.text.isNotEmpty) 'alias': aliasController.text,
 | 
			
		||||
          if (titleController.text.isNotEmpty) 'title': titleController.text,
 | 
			
		||||
          if (descriptionController.text.isNotEmpty) 'description': descriptionController.text,
 | 
			
		||||
          if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.rid,
 | 
			
		||||
          'attachments': attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(),
 | 
			
		||||
          if (descriptionController.text.isNotEmpty)
 | 
			
		||||
            'description': descriptionController.text,
 | 
			
		||||
          if (thumbnail != null && thumbnail!.attachment != null)
 | 
			
		||||
            'thumbnail': thumbnail!.attachment!.rid,
 | 
			
		||||
          'attachments': attachments
 | 
			
		||||
              .where((e) => e.attachment != null)
 | 
			
		||||
              .map((e) => e.attachment!.rid)
 | 
			
		||||
              .toList(),
 | 
			
		||||
          'tags': tags.map((ele) => {'alias': ele}).toList(),
 | 
			
		||||
          'categories': categories.map((ele) => {'alias': ele}).toList(),
 | 
			
		||||
          'visibility': visibility,
 | 
			
		||||
          'visible_users_list': visibleUsers,
 | 
			
		||||
          'invisible_users_list': invisibleUsers,
 | 
			
		||||
          if (publishedAt != null) 'published_at': publishedAt!.toUtc().toIso8601String(),
 | 
			
		||||
          if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
 | 
			
		||||
          if (publishedAt != null)
 | 
			
		||||
            'published_at': publishedAt!.toUtc().toIso8601String(),
 | 
			
		||||
          if (publishedUntil != null)
 | 
			
		||||
            'published_until': publishedAt!.toUtc().toIso8601String(),
 | 
			
		||||
          if (replyingPost != null) 'reply_to': replyingPost!.id,
 | 
			
		||||
          if (repostingPost != null) 'repost_to': repostingPost!.id,
 | 
			
		||||
          if (reward != null) 'reward': reward,
 | 
			
		||||
          if (videoAttachment != null) 'video': videoAttachment!.rid,
 | 
			
		||||
          if (poll != null) 'poll': poll!.id,
 | 
			
		||||
          if (realm != null) 'realm': realm!.id,
 | 
			
		||||
          'is_draft': saveAsDraft,
 | 
			
		||||
        },
 | 
			
		||||
        onSendProgress: (count, total) {
 | 
			
		||||
          progress = baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
 | 
			
		||||
          progress =
 | 
			
		||||
              baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
 | 
			
		||||
          notifyListeners();
 | 
			
		||||
        },
 | 
			
		||||
        onReceiveProgress: (count, total) {
 | 
			
		||||
          progress = baseProgressVal + (kPostingProgressWeight / 2) + (count / total) * (kPostingProgressWeight / 2);
 | 
			
		||||
          progress = baseProgressVal +
 | 
			
		||||
              (kPostingProgressWeight / 2) +
 | 
			
		||||
              (count / total) * (kPostingProgressWeight / 2);
 | 
			
		||||
          notifyListeners();
 | 
			
		||||
        },
 | 
			
		||||
        options: Options(
 | 
			
		||||
          method: editingPost != null ? 'PUT' : 'POST',
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
      reset();
 | 
			
		||||
      if (saveAsDraft) {
 | 
			
		||||
        if (!context.mounted) return;
 | 
			
		||||
        editingDraft = true;
 | 
			
		||||
        final out = SnPost.fromJson(resp.data);
 | 
			
		||||
        final pt = context.read<SnPostContentProvider>();
 | 
			
		||||
        editingPost = await pt.completePostData(out);
 | 
			
		||||
        notifyListeners();
 | 
			
		||||
      } else {
 | 
			
		||||
        reset();
 | 
			
		||||
      }
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!context.mounted) return;
 | 
			
		||||
      context.showErrorDialog(err);
 | 
			
		||||
@@ -683,7 +753,8 @@ class PostWriteController extends ChangeNotifier {
 | 
			
		||||
    repostingPost = null;
 | 
			
		||||
    mode = kTitleMap.keys.first;
 | 
			
		||||
    temporaryRestored = false;
 | 
			
		||||
    SharedPreferences.getInstance().then((prefs) => prefs.remove(kTemporaryStorageKey));
 | 
			
		||||
    SharedPreferences.getInstance()
 | 
			
		||||
        .then((prefs) => prefs.remove(kTemporaryStorageKey));
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -23,8 +23,6 @@ class $SnLocalChatChannelTable extends SnLocalChatChannel
 | 
			
		||||
  late final GeneratedColumn<String> alias = GeneratedColumn<String>(
 | 
			
		||||
      'alias', aliasedName, false,
 | 
			
		||||
      type: DriftSqlType.string, requiredDuringInsert: true);
 | 
			
		||||
  static const VerificationMeta _contentMeta =
 | 
			
		||||
      const VerificationMeta('content');
 | 
			
		||||
  @override
 | 
			
		||||
  late final GeneratedColumnWithTypeConverter<SnChannel, String> content =
 | 
			
		||||
      GeneratedColumn<String>('content', aliasedName, false,
 | 
			
		||||
@@ -60,7 +58,6 @@ class $SnLocalChatChannelTable extends SnLocalChatChannel
 | 
			
		||||
    } else if (isInserting) {
 | 
			
		||||
      context.missing(_aliasMeta);
 | 
			
		||||
    }
 | 
			
		||||
    context.handle(_contentMeta, const VerificationResult.success());
 | 
			
		||||
    if (data.containsKey('created_at')) {
 | 
			
		||||
      context.handle(_createdAtMeta,
 | 
			
		||||
          createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
 | 
			
		||||
@@ -295,8 +292,6 @@ class $SnLocalChatMessageTable extends SnLocalChatMessage
 | 
			
		||||
  late final GeneratedColumn<int> senderId = GeneratedColumn<int>(
 | 
			
		||||
      'sender_id', aliasedName, true,
 | 
			
		||||
      type: DriftSqlType.int, requiredDuringInsert: false);
 | 
			
		||||
  static const VerificationMeta _contentMeta =
 | 
			
		||||
      const VerificationMeta('content');
 | 
			
		||||
  @override
 | 
			
		||||
  late final GeneratedColumnWithTypeConverter<SnChatMessage, String> content =
 | 
			
		||||
      GeneratedColumn<String>('content', aliasedName, false,
 | 
			
		||||
@@ -338,7 +333,6 @@ class $SnLocalChatMessageTable extends SnLocalChatMessage
 | 
			
		||||
      context.handle(_senderIdMeta,
 | 
			
		||||
          senderId.isAcceptableOrUnknown(data['sender_id']!, _senderIdMeta));
 | 
			
		||||
    }
 | 
			
		||||
    context.handle(_contentMeta, const VerificationResult.success());
 | 
			
		||||
    if (data.containsKey('created_at')) {
 | 
			
		||||
      context.handle(_createdAtMeta,
 | 
			
		||||
          createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
 | 
			
		||||
@@ -604,8 +598,6 @@ class $SnLocalChannelMemberTable extends SnLocalChannelMember
 | 
			
		||||
  late final GeneratedColumn<int> accountId = GeneratedColumn<int>(
 | 
			
		||||
      'account_id', aliasedName, false,
 | 
			
		||||
      type: DriftSqlType.int, requiredDuringInsert: true);
 | 
			
		||||
  static const VerificationMeta _contentMeta =
 | 
			
		||||
      const VerificationMeta('content');
 | 
			
		||||
  @override
 | 
			
		||||
  late final GeneratedColumnWithTypeConverter<SnChannelMember, String> content =
 | 
			
		||||
      GeneratedColumn<String>('content', aliasedName, false,
 | 
			
		||||
@@ -655,7 +647,6 @@ class $SnLocalChannelMemberTable extends SnLocalChannelMember
 | 
			
		||||
    } else if (isInserting) {
 | 
			
		||||
      context.missing(_accountIdMeta);
 | 
			
		||||
    }
 | 
			
		||||
    context.handle(_contentMeta, const VerificationResult.success());
 | 
			
		||||
    if (data.containsKey('created_at')) {
 | 
			
		||||
      context.handle(_createdAtMeta,
 | 
			
		||||
          createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
 | 
			
		||||
@@ -1265,8 +1256,6 @@ class $SnLocalAccountTable extends SnLocalAccount
 | 
			
		||||
  late final GeneratedColumn<String> name = GeneratedColumn<String>(
 | 
			
		||||
      'name', aliasedName, false,
 | 
			
		||||
      type: DriftSqlType.string, requiredDuringInsert: true);
 | 
			
		||||
  static const VerificationMeta _contentMeta =
 | 
			
		||||
      const VerificationMeta('content');
 | 
			
		||||
  @override
 | 
			
		||||
  late final GeneratedColumnWithTypeConverter<SnAccount, String> content =
 | 
			
		||||
      GeneratedColumn<String>('content', aliasedName, false,
 | 
			
		||||
@@ -1308,7 +1297,6 @@ class $SnLocalAccountTable extends SnLocalAccount
 | 
			
		||||
    } else if (isInserting) {
 | 
			
		||||
      context.missing(_nameMeta);
 | 
			
		||||
    }
 | 
			
		||||
    context.handle(_contentMeta, const VerificationResult.success());
 | 
			
		||||
    if (data.containsKey('created_at')) {
 | 
			
		||||
      context.handle(_createdAtMeta,
 | 
			
		||||
          createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
 | 
			
		||||
@@ -1582,8 +1570,6 @@ class $SnLocalAttachmentTable extends SnLocalAttachment
 | 
			
		||||
      type: DriftSqlType.string,
 | 
			
		||||
      requiredDuringInsert: true,
 | 
			
		||||
      defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE'));
 | 
			
		||||
  static const VerificationMeta _contentMeta =
 | 
			
		||||
      const VerificationMeta('content');
 | 
			
		||||
  @override
 | 
			
		||||
  late final GeneratedColumnWithTypeConverter<SnAttachment, String> content =
 | 
			
		||||
      GeneratedColumn<String>('content', aliasedName, false,
 | 
			
		||||
@@ -1639,7 +1625,6 @@ class $SnLocalAttachmentTable extends SnLocalAttachment
 | 
			
		||||
    } else if (isInserting) {
 | 
			
		||||
      context.missing(_uuidMeta);
 | 
			
		||||
    }
 | 
			
		||||
    context.handle(_contentMeta, const VerificationResult.success());
 | 
			
		||||
    if (data.containsKey('account_id')) {
 | 
			
		||||
      context.handle(_accountIdMeta,
 | 
			
		||||
          accountId.isAcceptableOrUnknown(data['account_id']!, _accountIdMeta));
 | 
			
		||||
@@ -1968,8 +1953,6 @@ class $SnLocalStickerTable extends SnLocalSticker
 | 
			
		||||
  late final GeneratedColumn<String> fullAlias = GeneratedColumn<String>(
 | 
			
		||||
      'full_alias', aliasedName, false,
 | 
			
		||||
      type: DriftSqlType.string, requiredDuringInsert: true);
 | 
			
		||||
  static const VerificationMeta _contentMeta =
 | 
			
		||||
      const VerificationMeta('content');
 | 
			
		||||
  @override
 | 
			
		||||
  late final GeneratedColumnWithTypeConverter<SnSticker, String> content =
 | 
			
		||||
      GeneratedColumn<String>('content', aliasedName, false,
 | 
			
		||||
@@ -2011,7 +1994,6 @@ class $SnLocalStickerTable extends SnLocalSticker
 | 
			
		||||
    } else if (isInserting) {
 | 
			
		||||
      context.missing(_fullAliasMeta);
 | 
			
		||||
    }
 | 
			
		||||
    context.handle(_contentMeta, const VerificationResult.success());
 | 
			
		||||
    if (data.containsKey('created_at')) {
 | 
			
		||||
      context.handle(_createdAtMeta,
 | 
			
		||||
          createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
 | 
			
		||||
@@ -2261,8 +2243,6 @@ class $SnLocalStickerPackTable extends SnLocalStickerPack
 | 
			
		||||
      requiredDuringInsert: false,
 | 
			
		||||
      defaultConstraints:
 | 
			
		||||
          GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
 | 
			
		||||
  static const VerificationMeta _contentMeta =
 | 
			
		||||
      const VerificationMeta('content');
 | 
			
		||||
  @override
 | 
			
		||||
  late final GeneratedColumnWithTypeConverter<SnStickerPack, String> content =
 | 
			
		||||
      GeneratedColumn<String>('content', aliasedName, false,
 | 
			
		||||
@@ -2293,7 +2273,6 @@ class $SnLocalStickerPackTable extends SnLocalStickerPack
 | 
			
		||||
    if (data.containsKey('id')) {
 | 
			
		||||
      context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
 | 
			
		||||
    }
 | 
			
		||||
    context.handle(_contentMeta, const VerificationResult.success());
 | 
			
		||||
    if (data.containsKey('created_at')) {
 | 
			
		||||
      context.handle(_createdAtMeta,
 | 
			
		||||
          createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
 | 
			
		||||
 
 | 
			
		||||
@@ -44,6 +44,7 @@ import 'package:surface/providers/widget.dart';
 | 
			
		||||
import 'package:surface/router.dart';
 | 
			
		||||
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/menu_bar.dart';
 | 
			
		||||
import 'package:tray_manager/tray_manager.dart';
 | 
			
		||||
import 'package:version/version.dart';
 | 
			
		||||
import 'package:workmanager/workmanager.dart';
 | 
			
		||||
@@ -331,18 +332,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
 | 
			
		||||
 | 
			
		||||
  Future<void> _hotkeyInitialization() async {
 | 
			
		||||
    if (kIsWeb) return;
 | 
			
		||||
 | 
			
		||||
    if (Platform.isMacOS) {
 | 
			
		||||
      HotKey quitHotKey = HotKey(
 | 
			
		||||
        key: PhysicalKeyboardKey.keyQ,
 | 
			
		||||
        modifiers: [HotKeyModifier.meta],
 | 
			
		||||
        scope: HotKeyScope.inapp,
 | 
			
		||||
      );
 | 
			
		||||
      await hotKeyManager.register(quitHotKey, keyUpHandler: (_) {
 | 
			
		||||
        _appLifecycleListener?.dispose();
 | 
			
		||||
        SystemChannels.platform.invokeMethod('SystemNavigator.pop');
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    // The quit key has been removed, and the logic of the quit key is moved to system menu bar activator.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  final Menu _appTrayMenu = Menu(
 | 
			
		||||
@@ -426,6 +416,15 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
 | 
			
		||||
    return AppExitResponse.cancel;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _quitApp() {
 | 
			
		||||
    _appLifecycleListener?.dispose();
 | 
			
		||||
    if (Platform.isWindows) {
 | 
			
		||||
      appWindow.close();
 | 
			
		||||
    } else {
 | 
			
		||||
      SystemChannels.platform.invokeMethod('SystemNavigator.pop');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void onTrayIconMouseDown() {
 | 
			
		||||
    if (Platform.isWindows) {
 | 
			
		||||
@@ -460,12 +459,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
 | 
			
		||||
        Timer(const Duration(milliseconds: 100), () => appWindow.show());
 | 
			
		||||
        break;
 | 
			
		||||
      case 'exit':
 | 
			
		||||
        _appLifecycleListener?.dispose();
 | 
			
		||||
        if (Platform.isWindows) {
 | 
			
		||||
          appWindow.close();
 | 
			
		||||
        } else {
 | 
			
		||||
          SystemChannels.platform.invokeMethod('SystemNavigator.pop');
 | 
			
		||||
        }
 | 
			
		||||
        _quitApp();
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -482,28 +476,31 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final cfg = context.read<ConfigProvider>();
 | 
			
		||||
    return NotificationListener<SizeChangedLayoutNotification>(
 | 
			
		||||
      onNotification: (notification) {
 | 
			
		||||
        WidgetsBinding.instance.addPostFrameCallback((_) {
 | 
			
		||||
          cfg.calcDrawerSize(context);
 | 
			
		||||
        });
 | 
			
		||||
        return false;
 | 
			
		||||
      },
 | 
			
		||||
      child: OrientationBuilder(
 | 
			
		||||
        builder: (context, orientation) {
 | 
			
		||||
          final cfg = context.read<ConfigProvider>();
 | 
			
		||||
    return AppSystemMenuBar(
 | 
			
		||||
      onQuit: _quitApp,
 | 
			
		||||
      child: NotificationListener<SizeChangedLayoutNotification>(
 | 
			
		||||
        onNotification: (notification) {
 | 
			
		||||
          WidgetsBinding.instance.addPostFrameCallback((_) {
 | 
			
		||||
            cfg.calcDrawerSize(context);
 | 
			
		||||
          });
 | 
			
		||||
          Future.delayed(const Duration(milliseconds: 300), () {
 | 
			
		||||
            if (context.mounted) {
 | 
			
		||||
              cfg.calcDrawerSize(context);
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
          return SizeChangedLayoutNotifier(
 | 
			
		||||
            child: widget.child,
 | 
			
		||||
          );
 | 
			
		||||
          return false;
 | 
			
		||||
        },
 | 
			
		||||
        child: OrientationBuilder(
 | 
			
		||||
          builder: (context, orientation) {
 | 
			
		||||
            final cfg = context.read<ConfigProvider>();
 | 
			
		||||
            WidgetsBinding.instance.addPostFrameCallback((_) {
 | 
			
		||||
              cfg.calcDrawerSize(context);
 | 
			
		||||
            });
 | 
			
		||||
            Future.delayed(const Duration(milliseconds: 300), () {
 | 
			
		||||
              if (context.mounted) {
 | 
			
		||||
                cfg.calcDrawerSize(context);
 | 
			
		||||
              }
 | 
			
		||||
            });
 | 
			
		||||
            return SizeChangedLayoutNotifier(
 | 
			
		||||
              child: widget.child,
 | 
			
		||||
            );
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -60,16 +60,24 @@ class SnPostContentProvider {
 | 
			
		||||
 | 
			
		||||
      out[i] = out[i].copyWith(
 | 
			
		||||
        preload: SnPostPreload(
 | 
			
		||||
          thumbnail: attachments.where((ele) => ele?.rid == out[i].body['thumbnail']).firstOrNull,
 | 
			
		||||
          attachments: attachments.where((ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false).toList(),
 | 
			
		||||
          video: attachments.where((ele) => ele?.rid == out[i].body['video']).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(),
 | 
			
		||||
          video: attachments
 | 
			
		||||
              .where((ele) => ele?.rid == out[i].body['video'])
 | 
			
		||||
              .firstOrNull,
 | 
			
		||||
          poll: poll,
 | 
			
		||||
          realm: realm,
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    uids.addAll(attachments.where((ele) => ele != null).map((ele) => ele!.accountId));
 | 
			
		||||
    uids.addAll(
 | 
			
		||||
        attachments.where((ele) => ele != null).map((ele) => ele!.accountId));
 | 
			
		||||
    await _ud.listAccount(uids);
 | 
			
		||||
 | 
			
		||||
    return out;
 | 
			
		||||
@@ -107,15 +115,23 @@ class SnPostContentProvider {
 | 
			
		||||
 | 
			
		||||
    out = out.copyWith(
 | 
			
		||||
      preload: SnPostPreload(
 | 
			
		||||
        thumbnail: attachments.where((ele) => ele?.rid == out.body['thumbnail']).firstOrNull,
 | 
			
		||||
        attachments: attachments.where((ele) => out.body['attachments']?.contains(ele?.rid) ?? false).toList(),
 | 
			
		||||
        video: attachments.where((ele) => ele?.rid == out.body['video']).firstOrNull,
 | 
			
		||||
        thumbnail: attachments
 | 
			
		||||
            .where((ele) => ele?.rid == out.body['thumbnail'])
 | 
			
		||||
            .firstOrNull,
 | 
			
		||||
        attachments: attachments
 | 
			
		||||
            .where(
 | 
			
		||||
                (ele) => out.body['attachments']?.contains(ele?.rid) ?? false)
 | 
			
		||||
            .toList(),
 | 
			
		||||
        video: attachments
 | 
			
		||||
            .where((ele) => ele?.rid == out.body['video'])
 | 
			
		||||
            .firstOrNull,
 | 
			
		||||
        poll: poll,
 | 
			
		||||
        realm: realm,
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    uids.addAll(attachments.where((ele) => ele != null).map((ele) => ele!.accountId));
 | 
			
		||||
    uids.addAll(
 | 
			
		||||
        attachments.where((ele) => ele != null).map((ele) => ele!.accountId));
 | 
			
		||||
    await _ud.listAccount(uids);
 | 
			
		||||
 | 
			
		||||
    return out;
 | 
			
		||||
@@ -138,17 +154,25 @@ class SnPostContentProvider {
 | 
			
		||||
    Iterable<String>? tags,
 | 
			
		||||
    String? realm,
 | 
			
		||||
    String? channel,
 | 
			
		||||
    bool isDraft = false,
 | 
			
		||||
    bool isShuffle = false,
 | 
			
		||||
  }) async {
 | 
			
		||||
    final resp = await _sn.client.get('/cgi/co/posts', queryParameters: {
 | 
			
		||||
      'take': take,
 | 
			
		||||
      'offset': offset,
 | 
			
		||||
      if (type != null) 'type': type,
 | 
			
		||||
      if (author != null) 'author': author,
 | 
			
		||||
      if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','),
 | 
			
		||||
      if (categories?.isNotEmpty ?? false) 'categories': categories!.join(','),
 | 
			
		||||
      if (realm != null) 'realm': realm,
 | 
			
		||||
      if (channel != null) 'channel': channel,
 | 
			
		||||
    });
 | 
			
		||||
    final resp = await _sn.client.get(
 | 
			
		||||
      isShuffle
 | 
			
		||||
          ? '/cgi/co/recommendations/shuffle'
 | 
			
		||||
          : '/cgi/co/posts${isDraft ? '/drafts' : ''}',
 | 
			
		||||
      queryParameters: {
 | 
			
		||||
        'take': take,
 | 
			
		||||
        'offset': offset,
 | 
			
		||||
        if (type != null) 'type': type,
 | 
			
		||||
        if (author != null) 'author': author,
 | 
			
		||||
        if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','),
 | 
			
		||||
        if (categories?.isNotEmpty ?? false)
 | 
			
		||||
          'categories': categories!.join(','),
 | 
			
		||||
        if (realm != null) 'realm': realm,
 | 
			
		||||
        if (channel != null) 'channel': channel,
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
    final List<SnPost> out = await _preloadRelatedDataInBatch(
 | 
			
		||||
      List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []),
 | 
			
		||||
    );
 | 
			
		||||
@@ -161,7 +185,8 @@ class SnPostContentProvider {
 | 
			
		||||
    int take = 10,
 | 
			
		||||
    int offset = 0,
 | 
			
		||||
  }) async {
 | 
			
		||||
    final resp = await _sn.client.get('/cgi/co/posts/$parentId/replies', queryParameters: {
 | 
			
		||||
    final resp = await _sn.client
 | 
			
		||||
        .get('/cgi/co/posts/$parentId/replies', queryParameters: {
 | 
			
		||||
      'take': take,
 | 
			
		||||
      'offset': offset,
 | 
			
		||||
    });
 | 
			
		||||
@@ -200,4 +225,9 @@ class SnPostContentProvider {
 | 
			
		||||
    );
 | 
			
		||||
    return out;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<SnPost> completePostData(SnPost post) async {
 | 
			
		||||
    final out = await _preloadRelatedDataSingle(post);
 | 
			
		||||
    return out;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,7 @@ class UserDirectoryProvider {
 | 
			
		||||
 | 
			
		||||
  final Map<String, int> _idCache = {};
 | 
			
		||||
  final Map<int, SnAccount> _cache = {};
 | 
			
		||||
  DateTime? _cacheExpiredAt;
 | 
			
		||||
 | 
			
		||||
  Future<int> loadAccountCache({int max = 100}) async {
 | 
			
		||||
    final out = await (_dt.db.snLocalAccount.select()..limit(max)).get();
 | 
			
		||||
@@ -26,11 +27,18 @@ class UserDirectoryProvider {
 | 
			
		||||
      _cache[ele.id] = ele.content;
 | 
			
		||||
      _idCache[ele.name] = ele.id;
 | 
			
		||||
    }
 | 
			
		||||
    _cacheExpiredAt = DateTime.now().add(const Duration(hours: 1));
 | 
			
		||||
    return out.length;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<List<SnAccount?>> listAccount(Iterable<dynamic> id) async {
 | 
			
		||||
    // In-memory cache
 | 
			
		||||
    if (_cacheExpiredAt != null && _cacheExpiredAt!.isBefore(DateTime.now())) {
 | 
			
		||||
      _cache.clear();
 | 
			
		||||
      _cacheExpiredAt = DateTime.now().add(const Duration(hours: 1));
 | 
			
		||||
    } else {
 | 
			
		||||
      _cacheExpiredAt ??= DateTime.now().add(const Duration(hours: 1));
 | 
			
		||||
    }
 | 
			
		||||
    final out = List<SnAccount?>.generate(id.length, (e) => null);
 | 
			
		||||
    final plannedQuery = <int>{};
 | 
			
		||||
    for (var idx = 0; idx < out.length; idx++) {
 | 
			
		||||
@@ -62,6 +70,7 @@ class UserDirectoryProvider {
 | 
			
		||||
      plannedQuery.remove(dbResp[idx].id);
 | 
			
		||||
    }
 | 
			
		||||
    // Remote server
 | 
			
		||||
    _saveToLocal(out.where((ele) => ele != null).cast());
 | 
			
		||||
    if (plannedQuery.isEmpty) return out;
 | 
			
		||||
    final resp = await _sn.client
 | 
			
		||||
        .get('/cgi/id/users', queryParameters: {'id': plannedQuery.join(',')});
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,9 @@ import 'package:surface/screens/news/news_detail.dart';
 | 
			
		||||
import 'package:surface/screens/news/news_list.dart';
 | 
			
		||||
import 'package:surface/screens/notification.dart';
 | 
			
		||||
import 'package:surface/screens/post/post_detail.dart';
 | 
			
		||||
import 'package:surface/screens/post/post_draft.dart';
 | 
			
		||||
import 'package:surface/screens/post/post_editor.dart';
 | 
			
		||||
import 'package:surface/screens/post/post_shuffle.dart';
 | 
			
		||||
import 'package:surface/screens/post/publisher_page.dart';
 | 
			
		||||
import 'package:surface/screens/post/post_search.dart';
 | 
			
		||||
import 'package:surface/screens/realm.dart';
 | 
			
		||||
@@ -66,10 +68,15 @@ final _appRoutes = [
 | 
			
		||||
    builder: (context, state) => const ExploreScreen(),
 | 
			
		||||
    routes: [
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/write/:mode',
 | 
			
		||||
        path: '/draft',
 | 
			
		||||
        name: 'postDraftBox',
 | 
			
		||||
        builder: (context, state) => const PostDraftBox(),
 | 
			
		||||
      ),
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/write',
 | 
			
		||||
        name: 'postEditor',
 | 
			
		||||
        builder: (context, state) => PostEditorScreen(
 | 
			
		||||
          mode: state.pathParameters['mode']!,
 | 
			
		||||
          mode: state.uri.queryParameters['mode'],
 | 
			
		||||
          postEditId: int.tryParse(
 | 
			
		||||
            state.uri.queryParameters['editing'] ?? '',
 | 
			
		||||
          ),
 | 
			
		||||
@@ -82,6 +89,11 @@ final _appRoutes = [
 | 
			
		||||
          extraProps: state.extra as PostEditorExtra?,
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/shuffle',
 | 
			
		||||
        name: 'postShuffle',
 | 
			
		||||
        builder: (context, state) => const PostShuffleScreen(),
 | 
			
		||||
      ),
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/search',
 | 
			
		||||
        name: 'postSearch',
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,9 @@ import 'package:surface/providers/database.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/providers/userinfo.dart';
 | 
			
		||||
import 'package:surface/providers/websocket.dart';
 | 
			
		||||
import 'package:surface/types/account.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_status.dart';
 | 
			
		||||
import 'package:surface/widgets/app_bar_leading.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
@@ -112,7 +114,14 @@ class _AuthorizedAccountScreen extends StatelessWidget {
 | 
			
		||||
              child: Column(
 | 
			
		||||
                crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                children: [
 | 
			
		||||
                  AccountImage(content: ua.user!.avatar, radius: 28),
 | 
			
		||||
                  Row(
 | 
			
		||||
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
			
		||||
                    crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                    children: [
 | 
			
		||||
                      AccountImage(content: ua.user!.avatar, radius: 28),
 | 
			
		||||
                      _AccountStatusWidget(account: ua.user!),
 | 
			
		||||
                    ],
 | 
			
		||||
                  ),
 | 
			
		||||
                  const Gap(8),
 | 
			
		||||
                  Row(
 | 
			
		||||
                    crossAxisAlignment: CrossAxisAlignment.baseline,
 | 
			
		||||
@@ -290,3 +299,81 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _AccountStatusWidget extends StatefulWidget {
 | 
			
		||||
  final SnAccount account;
 | 
			
		||||
  const _AccountStatusWidget({required this.account});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<_AccountStatusWidget> createState() => _AccountStatusWidgetState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _AccountStatusWidgetState extends State<_AccountStatusWidget> {
 | 
			
		||||
  SnAccountStatusInfo? _status;
 | 
			
		||||
 | 
			
		||||
  Future<void> _fetchStatus() async {
 | 
			
		||||
    try {
 | 
			
		||||
      final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
      final resp =
 | 
			
		||||
          await sn.client.get('/cgi/id/users/${widget.account.name}/status');
 | 
			
		||||
      setState(() {
 | 
			
		||||
        _status = SnAccountStatusInfo.fromJson(resp.data);
 | 
			
		||||
      });
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showErrorDialog(err);
 | 
			
		||||
    } finally {
 | 
			
		||||
      setState(() {});
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    _fetchStatus();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return InkWell(
 | 
			
		||||
      child: Row(
 | 
			
		||||
        children: [
 | 
			
		||||
          Text(
 | 
			
		||||
            _status != null
 | 
			
		||||
                ? (_status!.status?.label.isNotEmpty ?? false)
 | 
			
		||||
                    ? _status!.status!.label
 | 
			
		||||
                    : _status!.isOnline
 | 
			
		||||
                        ? 'accountStatusOnline'.tr()
 | 
			
		||||
                        : 'accountStatusOffline'.tr()
 | 
			
		||||
                : 'loading'.tr(),
 | 
			
		||||
          ),
 | 
			
		||||
          const Gap(4),
 | 
			
		||||
          Icon(
 | 
			
		||||
            (_status?.isDisturbable ?? true)
 | 
			
		||||
                ? Symbols.circle
 | 
			
		||||
                : Symbols.do_not_disturb_on,
 | 
			
		||||
            fill: (_status?.isOnline ?? false) ? 1 : 0,
 | 
			
		||||
            size: 16,
 | 
			
		||||
            color: (_status?.isOnline ?? false)
 | 
			
		||||
                ? (_status?.isDisturbable ?? true)
 | 
			
		||||
                    ? Colors.green
 | 
			
		||||
                    : Colors.red
 | 
			
		||||
                : Colors.grey,
 | 
			
		||||
          ).padding(all: 4),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
      onTap: () {
 | 
			
		||||
        showModalBottomSheet(
 | 
			
		||||
          context: context,
 | 
			
		||||
          builder: (context) => AccountStatusActionPopup(
 | 
			
		||||
            currentStatus: _status,
 | 
			
		||||
          ),
 | 
			
		||||
        ).then((value) {
 | 
			
		||||
          if (value == true && mounted) {
 | 
			
		||||
            _fetchStatus();
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ import 'package:surface/types/account.dart';
 | 
			
		||||
import 'package:surface/types/check_in.dart';
 | 
			
		||||
import 'package:surface/types/post.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
import 'package:surface/widgets/account/badge.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/universal_image.dart';
 | 
			
		||||
import 'package:surface/theme.dart';
 | 
			
		||||
@@ -450,19 +451,25 @@ class _UserScreenState extends State<UserScreen>
 | 
			
		||||
                    child: Row(
 | 
			
		||||
                      children: [
 | 
			
		||||
                        Icon(
 | 
			
		||||
                          Symbols.circle,
 | 
			
		||||
                          fill: 1,
 | 
			
		||||
                          (_status?.isDisturbable ?? true)
 | 
			
		||||
                              ? Symbols.circle
 | 
			
		||||
                              : Symbols.do_not_disturb_on,
 | 
			
		||||
                          fill: (_status?.isOnline ?? false) ? 1 : 0,
 | 
			
		||||
                          size: 16,
 | 
			
		||||
                          color: (_status?.isOnline ?? false)
 | 
			
		||||
                              ? Colors.green
 | 
			
		||||
                              ? (_status?.isDisturbable ?? true)
 | 
			
		||||
                                  ? Colors.green
 | 
			
		||||
                                  : Colors.red
 | 
			
		||||
                              : Colors.grey,
 | 
			
		||||
                        ).padding(all: 4),
 | 
			
		||||
                        const Gap(8),
 | 
			
		||||
                        Text(
 | 
			
		||||
                          _status != null
 | 
			
		||||
                              ? _status!.isOnline
 | 
			
		||||
                                  ? 'accountStatusOnline'.tr()
 | 
			
		||||
                                  : 'accountStatusOffline'.tr()
 | 
			
		||||
                              ? (_status!.status?.label.isNotEmpty ?? false)
 | 
			
		||||
                                  ? _status!.status!.label
 | 
			
		||||
                                  : _status!.isOnline
 | 
			
		||||
                                      ? 'accountStatusOnline'.tr()
 | 
			
		||||
                                      : 'accountStatusOffline'.tr()
 | 
			
		||||
                              : 'loading'.tr(),
 | 
			
		||||
                        ),
 | 
			
		||||
                        if (_status != null &&
 | 
			
		||||
@@ -484,34 +491,7 @@ class _UserScreenState extends State<UserScreen>
 | 
			
		||||
                  Wrap(
 | 
			
		||||
                    children: _account!.badges
 | 
			
		||||
                        .map(
 | 
			
		||||
                          (ele) => Tooltip(
 | 
			
		||||
                            richMessage: TextSpan(
 | 
			
		||||
                              children: [
 | 
			
		||||
                                TextSpan(
 | 
			
		||||
                                  text: kBadgesMeta[ele.type]?.$1.tr() ??
 | 
			
		||||
                                      'unknown'.tr(),
 | 
			
		||||
                                ),
 | 
			
		||||
                                if (ele.metadata['title'] != null)
 | 
			
		||||
                                  TextSpan(
 | 
			
		||||
                                    text: '\n${ele.metadata['title']}',
 | 
			
		||||
                                    style: const TextStyle(
 | 
			
		||||
                                        fontWeight: FontWeight.bold),
 | 
			
		||||
                                  ),
 | 
			
		||||
                                TextSpan(text: '\n'),
 | 
			
		||||
                                TextSpan(
 | 
			
		||||
                                  text: DateFormat.yMEd().format(ele.createdAt),
 | 
			
		||||
                                ),
 | 
			
		||||
                              ],
 | 
			
		||||
                            ),
 | 
			
		||||
                            child: Icon(
 | 
			
		||||
                              kBadgesMeta[ele.type]?.$2 ??
 | 
			
		||||
                                  Symbols.question_mark,
 | 
			
		||||
                              color: ele.metadata['color'] != null
 | 
			
		||||
                                  ? HexColor.fromHex(ele.metadata['color']!)
 | 
			
		||||
                                  : kBadgesMeta[ele.type]?.$3,
 | 
			
		||||
                              fill: 1,
 | 
			
		||||
                            ),
 | 
			
		||||
                          ),
 | 
			
		||||
                          (ele) => AccountBadge(badge: ele),
 | 
			
		||||
                        )
 | 
			
		||||
                        .toList(),
 | 
			
		||||
                  ).padding(horizontal: 8),
 | 
			
		||||
 
 | 
			
		||||
@@ -204,7 +204,6 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
			
		||||
              Theme.of(context).floatingActionButtonTheme.foregroundColor,
 | 
			
		||||
          backgroundColor:
 | 
			
		||||
              Theme.of(context).floatingActionButtonTheme.backgroundColor,
 | 
			
		||||
          shape: const CircleBorder(),
 | 
			
		||||
        ),
 | 
			
		||||
        closeButtonBuilder: DefaultFloatingActionButtonBuilder(
 | 
			
		||||
          child: const Icon(Symbols.close, size: 28),
 | 
			
		||||
@@ -213,7 +212,6 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
			
		||||
              Theme.of(context).floatingActionButtonTheme.foregroundColor,
 | 
			
		||||
          backgroundColor:
 | 
			
		||||
              Theme.of(context).floatingActionButtonTheme.backgroundColor,
 | 
			
		||||
          shape: const CircleBorder(),
 | 
			
		||||
        ),
 | 
			
		||||
        children: [
 | 
			
		||||
          Row(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,3 @@
 | 
			
		||||
import 'package:dropdown_button2/dropdown_button2.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
 | 
			
		||||
@@ -6,10 +5,12 @@ import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:go_router/go_router.dart';
 | 
			
		||||
import 'package:material_symbols_icons/symbols.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:responsive_framework/responsive_framework.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/post.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/providers/sn_realm.dart';
 | 
			
		||||
import 'package:surface/providers/userinfo.dart';
 | 
			
		||||
import 'package:surface/types/post.dart';
 | 
			
		||||
import 'package:surface/types/realm.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
@@ -19,6 +20,9 @@ import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:surface/widgets/post/post_item.dart';
 | 
			
		||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
 | 
			
		||||
 | 
			
		||||
const kPostChannels = ['Global', 'Friends', 'Following'];
 | 
			
		||||
const kPostChannelIcons = [Symbols.globe, Symbols.group, Symbols.subscriptions];
 | 
			
		||||
 | 
			
		||||
const Map<String, IconData> kCategoryIcons = {
 | 
			
		||||
  'technology': Symbols.tools_wrench,
 | 
			
		||||
  'gaming': Symbols.gamepad,
 | 
			
		||||
@@ -39,17 +43,17 @@ class ExploreScreen extends StatefulWidget {
 | 
			
		||||
  State<ExploreScreen> createState() => _ExploreScreenState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// You know what? I'm not going to make this a global variable.
 | 
			
		||||
// Cuz the global key make the selected category not update to child widget when the category is changed.
 | 
			
		||||
SnPostCategory? _selectedCategory;
 | 
			
		||||
 | 
			
		||||
class _ExploreScreenState extends State<ExploreScreen>
 | 
			
		||||
    with SingleTickerProviderStateMixin {
 | 
			
		||||
  late final TabController _tabController =
 | 
			
		||||
      TabController(length: 4, vsync: this);
 | 
			
		||||
    with TickerProviderStateMixin {
 | 
			
		||||
  late TabController _tabController = TabController(
 | 
			
		||||
    length: kPostChannels.length,
 | 
			
		||||
    vsync: this,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  final _fabKey = GlobalKey<ExpandableFabState>();
 | 
			
		||||
  final _listKeys = List.generate(4, (_) => GlobalKey<_PostListWidgetState>());
 | 
			
		||||
  final _listKey = GlobalKey<_PostListWidgetState>();
 | 
			
		||||
 | 
			
		||||
  bool _showCategories = false;
 | 
			
		||||
 | 
			
		||||
  final List<SnPostCategory> _categories = List.empty(growable: true);
 | 
			
		||||
 | 
			
		||||
@@ -69,14 +73,70 @@ class _ExploreScreenState extends State<ExploreScreen>
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _clearFilter() {
 | 
			
		||||
    _selectedCategory = null;
 | 
			
		||||
  final List<SnRealm> _realms = List.empty(growable: true);
 | 
			
		||||
 | 
			
		||||
  Future<void> _fetchRealms() async {
 | 
			
		||||
    try {
 | 
			
		||||
      final ua = context.read<UserProvider>();
 | 
			
		||||
      if (!ua.isAuthorized) return;
 | 
			
		||||
      final rels = context.read<SnRealmProvider>();
 | 
			
		||||
      final out = await rels.listAvailableRealms();
 | 
			
		||||
      setState(() {
 | 
			
		||||
        _realms.addAll(out);
 | 
			
		||||
      });
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showErrorDialog(err);
 | 
			
		||||
      rethrow;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _toggleShowCategories() {
 | 
			
		||||
    _showCategories = !_showCategories;
 | 
			
		||||
    if (_showCategories) {
 | 
			
		||||
      _tabController = TabController(length: _categories.length, vsync: this);
 | 
			
		||||
      _listKey.currentState?.setCategory(_categories[_tabController.index]);
 | 
			
		||||
      _listKey.currentState?.refreshPosts();
 | 
			
		||||
    } else {
 | 
			
		||||
      _tabController = TabController(length: kPostChannels.length, vsync: this);
 | 
			
		||||
      _listKey.currentState?.setCategory(null);
 | 
			
		||||
      _listKey.currentState?.refreshPosts();
 | 
			
		||||
    }
 | 
			
		||||
    _tabListen();
 | 
			
		||||
    setState(() {});
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _tabListen() {
 | 
			
		||||
    _tabController.addListener(() {
 | 
			
		||||
      if (_tabController.indexIsChanging) {
 | 
			
		||||
        if (_showCategories) {
 | 
			
		||||
          _listKey.currentState?.setCategory(_categories[_tabController.index]);
 | 
			
		||||
          _listKey.currentState?.refreshPosts();
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        switch (_tabController.index) {
 | 
			
		||||
          case 0:
 | 
			
		||||
          case 3:
 | 
			
		||||
            _listKey.currentState?.setChannel(null);
 | 
			
		||||
            break;
 | 
			
		||||
          case 1:
 | 
			
		||||
            _listKey.currentState?.setChannel('friends');
 | 
			
		||||
            break;
 | 
			
		||||
          case 2:
 | 
			
		||||
            _listKey.currentState?.setChannel('following');
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        _listKey.currentState?.refreshPosts();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    _fetchCategories();
 | 
			
		||||
    super.initState();
 | 
			
		||||
    _tabListen();
 | 
			
		||||
    _fetchCategories();
 | 
			
		||||
    _fetchRealms();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@@ -86,7 +146,7 @@ class _ExploreScreenState extends State<ExploreScreen>
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> refreshPosts() async {
 | 
			
		||||
    await _listKeys[_tabController.index].currentState?.refreshPosts();
 | 
			
		||||
    await _listKey.currentState?.refreshPosts();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@@ -111,7 +171,6 @@ class _ExploreScreenState extends State<ExploreScreen>
 | 
			
		||||
              Theme.of(context).floatingActionButtonTheme.foregroundColor,
 | 
			
		||||
          backgroundColor:
 | 
			
		||||
              Theme.of(context).floatingActionButtonTheme.backgroundColor,
 | 
			
		||||
          shape: const CircleBorder(),
 | 
			
		||||
        ),
 | 
			
		||||
        closeButtonBuilder: DefaultFloatingActionButtonBuilder(
 | 
			
		||||
          child: const Icon(Symbols.close, size: 28),
 | 
			
		||||
@@ -120,90 +179,39 @@ class _ExploreScreenState extends State<ExploreScreen>
 | 
			
		||||
              Theme.of(context).floatingActionButtonTheme.foregroundColor,
 | 
			
		||||
          backgroundColor:
 | 
			
		||||
              Theme.of(context).floatingActionButtonTheme.backgroundColor,
 | 
			
		||||
          shape: const CircleBorder(),
 | 
			
		||||
        ),
 | 
			
		||||
        children: [
 | 
			
		||||
          Row(
 | 
			
		||||
            children: [
 | 
			
		||||
              Text('writePostTypeStory').tr(),
 | 
			
		||||
              Text('writePost').tr(),
 | 
			
		||||
              const Gap(20),
 | 
			
		||||
              FloatingActionButton(
 | 
			
		||||
                heroTag: null,
 | 
			
		||||
                tooltip: 'writePostTypeStory'.tr(),
 | 
			
		||||
                tooltip: 'writePost'.tr(),
 | 
			
		||||
                onPressed: () {
 | 
			
		||||
                  GoRouter.of(context).pushNamed('postEditor', pathParameters: {
 | 
			
		||||
                    'mode': 'stories',
 | 
			
		||||
                  }).then((value) {
 | 
			
		||||
                  GoRouter.of(context).pushNamed('postEditor').then((value) {
 | 
			
		||||
                    if (value == true) {
 | 
			
		||||
                      refreshPosts();
 | 
			
		||||
                    }
 | 
			
		||||
                  });
 | 
			
		||||
                  _fabKey.currentState!.toggle();
 | 
			
		||||
                },
 | 
			
		||||
                child: const Icon(Symbols.post_rounded),
 | 
			
		||||
                child: const Icon(Symbols.edit),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
          Row(
 | 
			
		||||
            children: [
 | 
			
		||||
              Text('writePostTypeArticle').tr(),
 | 
			
		||||
              Text('postDraftBox').tr(),
 | 
			
		||||
              const Gap(20),
 | 
			
		||||
              FloatingActionButton(
 | 
			
		||||
                heroTag: null,
 | 
			
		||||
                tooltip: 'writePostTypeArticle'.tr(),
 | 
			
		||||
                tooltip: 'postDraftBox'.tr(),
 | 
			
		||||
                onPressed: () {
 | 
			
		||||
                  GoRouter.of(context).pushNamed('postEditor', pathParameters: {
 | 
			
		||||
                    'mode': 'articles',
 | 
			
		||||
                  }).then((value) {
 | 
			
		||||
                    if (value == true) {
 | 
			
		||||
                      refreshPosts();
 | 
			
		||||
                    }
 | 
			
		||||
                  });
 | 
			
		||||
                  GoRouter.of(context).pushNamed('postDraftBox');
 | 
			
		||||
                  _fabKey.currentState!.toggle();
 | 
			
		||||
                },
 | 
			
		||||
                child: const Icon(Symbols.news),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
          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),
 | 
			
		||||
                child: const Icon(Symbols.box_edit),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
@@ -215,26 +223,80 @@ class _ExploreScreenState extends State<ExploreScreen>
 | 
			
		||||
            SliverOverlapAbsorber(
 | 
			
		||||
              handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
 | 
			
		||||
              sliver: SliverAppBar(
 | 
			
		||||
                leading: AutoAppBarLeading(),
 | 
			
		||||
                title: Text('screenExplore').tr(),
 | 
			
		||||
                leading:
 | 
			
		||||
                    ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)
 | 
			
		||||
                        ? AutoAppBarLeading()
 | 
			
		||||
                        : null,
 | 
			
		||||
                titleSpacing: 0,
 | 
			
		||||
                title: Row(
 | 
			
		||||
                  children: [
 | 
			
		||||
                    if (ResponsiveBreakpoints.of(context).largerThan(MOBILE))
 | 
			
		||||
                      const Gap(8),
 | 
			
		||||
                    IconButton(
 | 
			
		||||
                      icon: const Icon(Symbols.shuffle),
 | 
			
		||||
                      onPressed: () {
 | 
			
		||||
                        GoRouter.of(context).pushNamed('postShuffle');
 | 
			
		||||
                      },
 | 
			
		||||
                    ),
 | 
			
		||||
                    Expanded(
 | 
			
		||||
                      child: Center(
 | 
			
		||||
                        child: IconButton(
 | 
			
		||||
                          padding: EdgeInsets.zero,
 | 
			
		||||
                          constraints: const BoxConstraints(),
 | 
			
		||||
                          visualDensity: VisualDensity.compact,
 | 
			
		||||
                          icon: _listKey.currentState?.realm != null
 | 
			
		||||
                              ? AccountImage(
 | 
			
		||||
                                  content: _listKey.currentState!.realm!.avatar,
 | 
			
		||||
                                  radius: 14,
 | 
			
		||||
                                )
 | 
			
		||||
                              : Image.asset(
 | 
			
		||||
                                  'assets/icon/icon-dark.png',
 | 
			
		||||
                                  width: 32,
 | 
			
		||||
                                  height: 32,
 | 
			
		||||
                                  color: Theme.of(context)
 | 
			
		||||
                                      .appBarTheme
 | 
			
		||||
                                      .foregroundColor,
 | 
			
		||||
                                ),
 | 
			
		||||
                          onPressed: () {
 | 
			
		||||
                            showModalBottomSheet(
 | 
			
		||||
                              context: context,
 | 
			
		||||
                              builder: (context) => _PostListRealmPopup(
 | 
			
		||||
                                realms: _realms,
 | 
			
		||||
                                onUpdate: (realm) {
 | 
			
		||||
                                  _listKey.currentState?.setRealm(realm);
 | 
			
		||||
                                  _listKey.currentState?.refreshPosts();
 | 
			
		||||
                                  Future.delayed(
 | 
			
		||||
                                      const Duration(milliseconds: 100), () {
 | 
			
		||||
                                    if (mounted) {
 | 
			
		||||
                                      setState(() {});
 | 
			
		||||
                                    }
 | 
			
		||||
                                  });
 | 
			
		||||
                                },
 | 
			
		||||
                              ),
 | 
			
		||||
                            );
 | 
			
		||||
                          },
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ],
 | 
			
		||||
                ),
 | 
			
		||||
                floating: true,
 | 
			
		||||
                snap: true,
 | 
			
		||||
                actions: [
 | 
			
		||||
                  IconButton(
 | 
			
		||||
                    icon: const Icon(Symbols.category),
 | 
			
		||||
                    style: _showCategories
 | 
			
		||||
                        ? ButtonStyle(
 | 
			
		||||
                            foregroundColor: WidgetStateProperty.all(
 | 
			
		||||
                              Theme.of(context).colorScheme.primary,
 | 
			
		||||
                            ),
 | 
			
		||||
                            backgroundColor: MaterialStateProperty.all(
 | 
			
		||||
                              Theme.of(context).colorScheme.secondaryContainer,
 | 
			
		||||
                            ),
 | 
			
		||||
                          )
 | 
			
		||||
                        : null,
 | 
			
		||||
                    onPressed: () {
 | 
			
		||||
                      showModalBottomSheet(
 | 
			
		||||
                        context: context,
 | 
			
		||||
                        builder: (context) => _PostCategoryPickerPopup(
 | 
			
		||||
                          categories: _categories,
 | 
			
		||||
                          selected: _selectedCategory,
 | 
			
		||||
                        ),
 | 
			
		||||
                      ).then((value) {
 | 
			
		||||
                        if (value != null && context.mounted) {
 | 
			
		||||
                          _selectedCategory = value == false ? null : value;
 | 
			
		||||
                          refreshPosts();
 | 
			
		||||
                        }
 | 
			
		||||
                      });
 | 
			
		||||
                      _toggleShowCategories();
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
                  IconButton(
 | 
			
		||||
@@ -246,122 +308,79 @@ class _ExploreScreenState extends State<ExploreScreen>
 | 
			
		||||
                  const Gap(8),
 | 
			
		||||
                ],
 | 
			
		||||
                bottom: TabBar(
 | 
			
		||||
                  isScrollable: _showCategories,
 | 
			
		||||
                  controller: _tabController,
 | 
			
		||||
                  tabs: [
 | 
			
		||||
                    Tab(
 | 
			
		||||
                      child: Row(
 | 
			
		||||
                        mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                        crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
                        children: [
 | 
			
		||||
                          Icon(Symbols.globe,
 | 
			
		||||
                              size: 20,
 | 
			
		||||
                              color: Theme.of(context)
 | 
			
		||||
                                  .appBarTheme
 | 
			
		||||
                                  .foregroundColor),
 | 
			
		||||
                          const Gap(8),
 | 
			
		||||
                          Flexible(
 | 
			
		||||
                            child: Text(
 | 
			
		||||
                              'postChannelGlobal',
 | 
			
		||||
                              maxLines: 1,
 | 
			
		||||
                            ).tr().textColor(
 | 
			
		||||
                                Theme.of(context).appBarTheme.foregroundColor),
 | 
			
		||||
                          ),
 | 
			
		||||
                  tabs: _showCategories
 | 
			
		||||
                      ? [
 | 
			
		||||
                          for (final category in _categories)
 | 
			
		||||
                            Tab(
 | 
			
		||||
                              child: Row(
 | 
			
		||||
                                mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                                crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
                                children: [
 | 
			
		||||
                                  Icon(
 | 
			
		||||
                                    kCategoryIcons[category.alias] ??
 | 
			
		||||
                                        Symbols.question_mark,
 | 
			
		||||
                                    color: Theme.of(context)
 | 
			
		||||
                                        .appBarTheme
 | 
			
		||||
                                        .foregroundColor!,
 | 
			
		||||
                                  ),
 | 
			
		||||
                                  const Gap(8),
 | 
			
		||||
                                  Flexible(
 | 
			
		||||
                                    child: Text(
 | 
			
		||||
                                      'postCategory${category.alias.capitalize()}'
 | 
			
		||||
                                              .trExists()
 | 
			
		||||
                                          ? 'postCategory${category.alias.capitalize()}'
 | 
			
		||||
                                              .tr()
 | 
			
		||||
                                          : category.name,
 | 
			
		||||
                                      maxLines: 1,
 | 
			
		||||
                                    ).textColor(
 | 
			
		||||
                                      Theme.of(context)
 | 
			
		||||
                                          .appBarTheme
 | 
			
		||||
                                          .foregroundColor!,
 | 
			
		||||
                                    ),
 | 
			
		||||
                                  ),
 | 
			
		||||
                                ],
 | 
			
		||||
                              ),
 | 
			
		||||
                            ),
 | 
			
		||||
                        ]
 | 
			
		||||
                      : [
 | 
			
		||||
                          for (final channel in kPostChannels)
 | 
			
		||||
                            Tab(
 | 
			
		||||
                              child: Row(
 | 
			
		||||
                                mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                                crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
                                children: [
 | 
			
		||||
                                  Icon(
 | 
			
		||||
                                    kPostChannelIcons[
 | 
			
		||||
                                        kPostChannels.indexOf(channel)],
 | 
			
		||||
                                    size: 20,
 | 
			
		||||
                                    color: Theme.of(context)
 | 
			
		||||
                                        .appBarTheme
 | 
			
		||||
                                        .foregroundColor,
 | 
			
		||||
                                  ),
 | 
			
		||||
                                  const Gap(8),
 | 
			
		||||
                                  Flexible(
 | 
			
		||||
                                    child: Text(
 | 
			
		||||
                                      'postChannel$channel',
 | 
			
		||||
                                      maxLines: 1,
 | 
			
		||||
                                    ).tr().textColor(
 | 
			
		||||
                                          Theme.of(context)
 | 
			
		||||
                                              .appBarTheme
 | 
			
		||||
                                              .foregroundColor,
 | 
			
		||||
                                        ),
 | 
			
		||||
                                  ),
 | 
			
		||||
                                ],
 | 
			
		||||
                              ),
 | 
			
		||||
                            ),
 | 
			
		||||
                        ],
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                    Tab(
 | 
			
		||||
                      child: Row(
 | 
			
		||||
                        mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                        crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
                        children: [
 | 
			
		||||
                          Icon(Symbols.group,
 | 
			
		||||
                              size: 20,
 | 
			
		||||
                              color: Theme.of(context)
 | 
			
		||||
                                  .appBarTheme
 | 
			
		||||
                                  .foregroundColor),
 | 
			
		||||
                          const Gap(8),
 | 
			
		||||
                          Flexible(
 | 
			
		||||
                            child: Text(
 | 
			
		||||
                              'postChannelFriends',
 | 
			
		||||
                              maxLines: 1,
 | 
			
		||||
                              textAlign: TextAlign.center,
 | 
			
		||||
                            ).tr().textColor(
 | 
			
		||||
                                Theme.of(context).appBarTheme.foregroundColor),
 | 
			
		||||
                          ),
 | 
			
		||||
                        ],
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                    Tab(
 | 
			
		||||
                      child: Row(
 | 
			
		||||
                        mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                        crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
                        children: [
 | 
			
		||||
                          Icon(Symbols.subscriptions,
 | 
			
		||||
                              size: 20,
 | 
			
		||||
                              color: Theme.of(context)
 | 
			
		||||
                                  .appBarTheme
 | 
			
		||||
                                  .foregroundColor),
 | 
			
		||||
                          const Gap(8),
 | 
			
		||||
                          Flexible(
 | 
			
		||||
                            child: Text(
 | 
			
		||||
                              'postChannelFollowing',
 | 
			
		||||
                              maxLines: 1,
 | 
			
		||||
                            ).tr().textColor(
 | 
			
		||||
                                Theme.of(context).appBarTheme.foregroundColor),
 | 
			
		||||
                          ),
 | 
			
		||||
                        ],
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                    Tab(
 | 
			
		||||
                      child: Row(
 | 
			
		||||
                        mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                        crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
                        children: [
 | 
			
		||||
                          Icon(Symbols.workspaces,
 | 
			
		||||
                              size: 20,
 | 
			
		||||
                              color: Theme.of(context)
 | 
			
		||||
                                  .appBarTheme
 | 
			
		||||
                                  .foregroundColor),
 | 
			
		||||
                          const Gap(8),
 | 
			
		||||
                          Flexible(
 | 
			
		||||
                            child: Text(
 | 
			
		||||
                              'postChannelRealm',
 | 
			
		||||
                              maxLines: 1,
 | 
			
		||||
                            ).tr().textColor(
 | 
			
		||||
                                Theme.of(context).appBarTheme.foregroundColor),
 | 
			
		||||
                          ),
 | 
			
		||||
                        ],
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ],
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ];
 | 
			
		||||
        },
 | 
			
		||||
        body: TabBarView(
 | 
			
		||||
          controller: _tabController,
 | 
			
		||||
          children: [
 | 
			
		||||
            _PostListWidget(
 | 
			
		||||
              key: _listKeys[0],
 | 
			
		||||
              onClearFilter: _clearFilter,
 | 
			
		||||
            ),
 | 
			
		||||
            _PostListWidget(
 | 
			
		||||
              key: _listKeys[1],
 | 
			
		||||
              channel: 'friends',
 | 
			
		||||
              onClearFilter: _clearFilter,
 | 
			
		||||
            ),
 | 
			
		||||
            _PostListWidget(
 | 
			
		||||
              key: _listKeys[2],
 | 
			
		||||
              channel: 'following',
 | 
			
		||||
              onClearFilter: _clearFilter,
 | 
			
		||||
            ),
 | 
			
		||||
            _PostListWidget(
 | 
			
		||||
              key: _listKeys[3],
 | 
			
		||||
              withRealm: true,
 | 
			
		||||
              onClearFilter: _clearFilter,
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        body: _PostListWidget(
 | 
			
		||||
          key: _listKey,
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
@@ -369,15 +388,7 @@ class _ExploreScreenState extends State<ExploreScreen>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _PostListWidget extends StatefulWidget {
 | 
			
		||||
  final String? channel;
 | 
			
		||||
  final bool withRealm;
 | 
			
		||||
  final Function onClearFilter;
 | 
			
		||||
 | 
			
		||||
  const _PostListWidget(
 | 
			
		||||
      {super.key,
 | 
			
		||||
      this.channel,
 | 
			
		||||
      this.withRealm = false,
 | 
			
		||||
      required this.onClearFilter});
 | 
			
		||||
  const _PostListWidget({super.key});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<_PostListWidget> createState() => _PostListWidgetState();
 | 
			
		||||
@@ -386,25 +397,13 @@ class _PostListWidget extends StatefulWidget {
 | 
			
		||||
class _PostListWidgetState extends State<_PostListWidget> {
 | 
			
		||||
  bool _isBusy = false;
 | 
			
		||||
 | 
			
		||||
  final List<SnPost> _posts = List.empty(growable: true);
 | 
			
		||||
  final List<SnRealm> _realms = List.empty(growable: true);
 | 
			
		||||
  SnRealm? _selectedRealm;
 | 
			
		||||
  int? _postCount;
 | 
			
		||||
  SnRealm? get realm => _selectedRealm;
 | 
			
		||||
 | 
			
		||||
  Future<void> _fetchRealms() async {
 | 
			
		||||
    try {
 | 
			
		||||
      final rels = context.read<SnRealmProvider>();
 | 
			
		||||
      final out = await rels.listAvailableRealms();
 | 
			
		||||
      setState(() {
 | 
			
		||||
        _realms.addAll(out);
 | 
			
		||||
        _selectedRealm = out.firstOrNull;
 | 
			
		||||
      });
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showErrorDialog(err);
 | 
			
		||||
      rethrow;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  final List<SnPost> _posts = List.empty(growable: true);
 | 
			
		||||
  SnRealm? _selectedRealm;
 | 
			
		||||
  String? _selectedChannel;
 | 
			
		||||
  SnPostCategory? _selectedCategory;
 | 
			
		||||
  int? _postCount;
 | 
			
		||||
 | 
			
		||||
  Future<void> _fetchPosts() async {
 | 
			
		||||
    if (_postCount != null && _posts.length >= _postCount!) return;
 | 
			
		||||
@@ -416,7 +415,7 @@ class _PostListWidgetState extends State<_PostListWidget> {
 | 
			
		||||
      take: 10,
 | 
			
		||||
      offset: _posts.length,
 | 
			
		||||
      categories: _selectedCategory != null ? [_selectedCategory!.alias] : null,
 | 
			
		||||
      channel: widget.channel,
 | 
			
		||||
      channel: _selectedChannel,
 | 
			
		||||
      realm: _selectedRealm?.alias,
 | 
			
		||||
    );
 | 
			
		||||
    final out = result.$1;
 | 
			
		||||
@@ -429,6 +428,21 @@ class _PostListWidgetState extends State<_PostListWidget> {
 | 
			
		||||
    if (mounted) setState(() => _isBusy = false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void setChannel(String? channel) {
 | 
			
		||||
    _selectedChannel = channel;
 | 
			
		||||
    setState(() {});
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void setRealm(SnRealm? realm) {
 | 
			
		||||
    _selectedRealm = realm;
 | 
			
		||||
    setState(() {});
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void setCategory(SnPostCategory? category) {
 | 
			
		||||
    _selectedCategory = category;
 | 
			
		||||
    setState(() {});
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> refreshPosts() {
 | 
			
		||||
    _postCount = null;
 | 
			
		||||
    _posts.clear();
 | 
			
		||||
@@ -438,13 +452,7 @@ class _PostListWidgetState extends State<_PostListWidget> {
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    if (widget.withRealm) {
 | 
			
		||||
      _fetchRealms().then((_) {
 | 
			
		||||
        _fetchPosts();
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      _fetchPosts();
 | 
			
		||||
    }
 | 
			
		||||
    _fetchPosts();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@@ -467,52 +475,13 @@ class _PostListWidgetState extends State<_PostListWidget> {
 | 
			
		||||
              IconButton(
 | 
			
		||||
                icon: const Icon(Symbols.clear),
 | 
			
		||||
                onPressed: () {
 | 
			
		||||
                  widget.onClearFilter.call();
 | 
			
		||||
                  setState(() => _selectedCategory = null);
 | 
			
		||||
                  refreshPosts();
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
            padding: const EdgeInsets.only(left: 20, right: 4),
 | 
			
		||||
          ),
 | 
			
		||||
        if (widget.withRealm)
 | 
			
		||||
          DropdownButtonHideUnderline(
 | 
			
		||||
            child: DropdownButton2<SnRealm>(
 | 
			
		||||
              isExpanded: true,
 | 
			
		||||
              items: _realms
 | 
			
		||||
                  .map(
 | 
			
		||||
                    (ele) => DropdownMenuItem<SnRealm>(
 | 
			
		||||
                      value: ele,
 | 
			
		||||
                      child: Row(
 | 
			
		||||
                        children: [
 | 
			
		||||
                          AccountImage(
 | 
			
		||||
                            content: ele.avatar,
 | 
			
		||||
                            fallbackWidget: const Icon(Symbols.group, size: 16),
 | 
			
		||||
                            radius: 14,
 | 
			
		||||
                          ),
 | 
			
		||||
                          const Gap(8),
 | 
			
		||||
                          Text(
 | 
			
		||||
                            ele.name,
 | 
			
		||||
                            style: Theme.of(context).textTheme.bodyMedium,
 | 
			
		||||
                          ),
 | 
			
		||||
                        ],
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  )
 | 
			
		||||
                  .toList(),
 | 
			
		||||
              value: _selectedRealm,
 | 
			
		||||
              onChanged: (SnRealm? value) {
 | 
			
		||||
                setState(() => _selectedRealm = value);
 | 
			
		||||
                refreshPosts();
 | 
			
		||||
              },
 | 
			
		||||
              buttonStyleData: const ButtonStyleData(
 | 
			
		||||
                padding: EdgeInsets.only(left: 4, right: 12),
 | 
			
		||||
              ),
 | 
			
		||||
              menuItemStyleData: const MenuItemStyleData(
 | 
			
		||||
                height: 48,
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        if (widget.withRealm) const Divider(height: 1),
 | 
			
		||||
        Expanded(
 | 
			
		||||
          child: MediaQuery.removePadding(
 | 
			
		||||
            context: context,
 | 
			
		||||
@@ -521,6 +490,7 @@ class _PostListWidgetState extends State<_PostListWidget> {
 | 
			
		||||
              displacement: 40 + MediaQuery.of(context).padding.top,
 | 
			
		||||
              onRefresh: () => refreshPosts(),
 | 
			
		||||
              child: InfiniteList(
 | 
			
		||||
                padding: EdgeInsets.only(top: 8),
 | 
			
		||||
                itemCount: _posts.length,
 | 
			
		||||
                isLoading: _isBusy,
 | 
			
		||||
                centerLoading: true,
 | 
			
		||||
@@ -542,18 +512,21 @@ class _PostListWidgetState extends State<_PostListWidget> {
 | 
			
		||||
                separatorBuilder: (_, __) => const Gap(8),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ).padding(top: 8),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _PostCategoryPickerPopup extends StatelessWidget {
 | 
			
		||||
  final List<SnPostCategory> categories;
 | 
			
		||||
  final SnPostCategory? selected;
 | 
			
		||||
class _PostListRealmPopup extends StatelessWidget {
 | 
			
		||||
  final List<SnRealm>? realms;
 | 
			
		||||
  final Function(SnRealm?) onUpdate;
 | 
			
		||||
 | 
			
		||||
  const _PostCategoryPickerPopup({required this.categories, this.selected});
 | 
			
		||||
  const _PostListRealmPopup({
 | 
			
		||||
    required this.realms,
 | 
			
		||||
    required this.onUpdate,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
@@ -563,62 +536,38 @@ class _PostCategoryPickerPopup extends StatelessWidget {
 | 
			
		||||
        Row(
 | 
			
		||||
          crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
          children: [
 | 
			
		||||
            const Icon(Symbols.category, size: 24),
 | 
			
		||||
            const Icon(Symbols.face, size: 24),
 | 
			
		||||
            const Gap(16),
 | 
			
		||||
            Text('postCategory')
 | 
			
		||||
                .tr()
 | 
			
		||||
                .textStyle(Theme.of(context).textTheme.titleLarge!),
 | 
			
		||||
            Text('accountRealms', style: Theme.of(context).textTheme.titleLarge)
 | 
			
		||||
                .tr(),
 | 
			
		||||
          ],
 | 
			
		||||
        ).padding(horizontal: 20, top: 16, bottom: 12),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          leading: const Icon(Symbols.clear),
 | 
			
		||||
          title: Text('postFilterReset').tr(),
 | 
			
		||||
          subtitle: Text('postFilterResetDescription').tr(),
 | 
			
		||||
          contentPadding: const EdgeInsets.symmetric(horizontal: 20),
 | 
			
		||||
          leading: const Icon(Symbols.close),
 | 
			
		||||
          title: Text('postInGlobal').tr(),
 | 
			
		||||
          subtitle: Text('postViewInGlobalDescription').tr(),
 | 
			
		||||
          contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
          onTap: () {
 | 
			
		||||
            Navigator.pop(context, false);
 | 
			
		||||
            onUpdate.call(null);
 | 
			
		||||
            Navigator.pop(context);
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        const Divider(height: 1),
 | 
			
		||||
        Expanded(
 | 
			
		||||
          child: GridView.count(
 | 
			
		||||
            crossAxisCount: 4,
 | 
			
		||||
            shrinkWrap: true,
 | 
			
		||||
            physics: const NeverScrollableScrollPhysics(),
 | 
			
		||||
            childAspectRatio: 1,
 | 
			
		||||
            children: categories
 | 
			
		||||
                .map(
 | 
			
		||||
                  (ele) => InkWell(
 | 
			
		||||
                    onTap: () {
 | 
			
		||||
                      _selectedCategory = ele;
 | 
			
		||||
                      Navigator.pop(context, ele);
 | 
			
		||||
                    },
 | 
			
		||||
                    child: Column(
 | 
			
		||||
                      crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
                      mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
                      mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                      children: [
 | 
			
		||||
                        Icon(
 | 
			
		||||
                          kCategoryIcons[ele.alias] ?? Symbols.question_mark,
 | 
			
		||||
                          color: selected == ele
 | 
			
		||||
                              ? Theme.of(context).colorScheme.primary
 | 
			
		||||
                              : null,
 | 
			
		||||
                        ),
 | 
			
		||||
                        const Gap(4),
 | 
			
		||||
                        Text(
 | 
			
		||||
                          'postCategory${ele.alias.capitalize()}'.trExists()
 | 
			
		||||
                              ? 'postCategory${ele.alias.capitalize()}'.tr()
 | 
			
		||||
                              : ele.name,
 | 
			
		||||
                        )
 | 
			
		||||
                            .textStyle(Theme.of(context).textTheme.titleMedium!)
 | 
			
		||||
                            .textColor(selected == ele
 | 
			
		||||
                                ? Theme.of(context).colorScheme.primary
 | 
			
		||||
                                : null),
 | 
			
		||||
                      ],
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                )
 | 
			
		||||
                .toList(),
 | 
			
		||||
          child: ListView.builder(
 | 
			
		||||
            itemCount: realms?.length ?? 0,
 | 
			
		||||
            itemBuilder: (context, idx) {
 | 
			
		||||
              final realm = realms![idx];
 | 
			
		||||
              return ListTile(
 | 
			
		||||
                title: Text(realm.name),
 | 
			
		||||
                subtitle: Text('@${realm.alias}'),
 | 
			
		||||
                leading: AccountImage(content: realm.avatar, radius: 18),
 | 
			
		||||
                onTap: () {
 | 
			
		||||
                  onUpdate.call(realm);
 | 
			
		||||
                  Navigator.pop(context);
 | 
			
		||||
                },
 | 
			
		||||
              );
 | 
			
		||||
            },
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
 
 | 
			
		||||
@@ -546,11 +546,26 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
 | 
			
		||||
                          '+${_todayRecord!.resultExperience} EXP',
 | 
			
		||||
                          style: Theme.of(context).textTheme.bodyLarge,
 | 
			
		||||
                        ),
 | 
			
		||||
                        if (_todayRecord!.resultCoin >= 0)
 | 
			
		||||
                        if (_todayRecord!.resultCoin > 0)
 | 
			
		||||
                          Text(
 | 
			
		||||
                            '+${_todayRecord!.resultCoin} ${'walletCurrencyShort'.tr()}',
 | 
			
		||||
                            style: Theme.of(context).textTheme.bodyLarge,
 | 
			
		||||
                          )
 | 
			
		||||
                          ),
 | 
			
		||||
                        if (_todayRecord!.currentStreak > 0)
 | 
			
		||||
                          Row(
 | 
			
		||||
                            children: [
 | 
			
		||||
                              const Icon(
 | 
			
		||||
                                Symbols.local_fire_department,
 | 
			
		||||
                                size: 14,
 | 
			
		||||
                              ).padding(bottom: 2),
 | 
			
		||||
                              const Gap(4),
 | 
			
		||||
                              Text(
 | 
			
		||||
                                'checkInStreak'
 | 
			
		||||
                                    .plural(_todayRecord!.currentStreak),
 | 
			
		||||
                                style: Theme.of(context).textTheme.bodySmall,
 | 
			
		||||
                              ),
 | 
			
		||||
                            ],
 | 
			
		||||
                          ).padding(top: 4),
 | 
			
		||||
                      ],
 | 
			
		||||
                    ),
 | 
			
		||||
            ),
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										88
									
								
								lib/screens/post/post_draft.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								lib/screens/post/post_draft.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,88 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:surface/providers/post.dart';
 | 
			
		||||
import 'package:surface/types/post.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:surface/widgets/post/post_item.dart';
 | 
			
		||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
 | 
			
		||||
 | 
			
		||||
class PostDraftBox extends StatefulWidget {
 | 
			
		||||
  const PostDraftBox({super.key});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<PostDraftBox> createState() => _PostDraftBoxState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _PostDraftBoxState extends State<PostDraftBox> {
 | 
			
		||||
  bool _isBusy = false;
 | 
			
		||||
  final List<SnPost> _posts = List.empty(growable: true);
 | 
			
		||||
  int? _totalCount;
 | 
			
		||||
 | 
			
		||||
  Future<void> _fetchPosts() async {
 | 
			
		||||
    setState(() => _isBusy = true);
 | 
			
		||||
    try {
 | 
			
		||||
      final pt = context.read<SnPostContentProvider>();
 | 
			
		||||
      final resp = await pt.listPosts(
 | 
			
		||||
        take: 10,
 | 
			
		||||
        offset: _posts.length,
 | 
			
		||||
        isDraft: true,
 | 
			
		||||
      );
 | 
			
		||||
      final out = resp.$1;
 | 
			
		||||
      _totalCount = resp.$2;
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      _posts.addAll(out);
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showErrorDialog(err);
 | 
			
		||||
    } finally {
 | 
			
		||||
      setState(() => _isBusy = false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: Text('postDraftBox').tr(),
 | 
			
		||||
      ),
 | 
			
		||||
      body: Column(
 | 
			
		||||
        children: [
 | 
			
		||||
          LoadingIndicator(isActive: _isBusy),
 | 
			
		||||
          Expanded(
 | 
			
		||||
            child: RefreshIndicator(
 | 
			
		||||
              onRefresh: () {
 | 
			
		||||
                _posts.clear();
 | 
			
		||||
                return _fetchPosts();
 | 
			
		||||
              },
 | 
			
		||||
              child: InfiniteList(
 | 
			
		||||
                padding: EdgeInsets.only(top: 8),
 | 
			
		||||
                hasReachedMax:
 | 
			
		||||
                    _totalCount != null && _posts.length >= _totalCount!,
 | 
			
		||||
                itemCount: _posts.length,
 | 
			
		||||
                onFetchData: () => _fetchPosts(),
 | 
			
		||||
                itemBuilder: (context, idx) {
 | 
			
		||||
                  final ele = _posts[idx];
 | 
			
		||||
                  return OpenablePostItem(
 | 
			
		||||
                    data: ele,
 | 
			
		||||
                    onChanged: (data) {
 | 
			
		||||
                      _posts[idx] = data;
 | 
			
		||||
                    },
 | 
			
		||||
                    onDeleted: () {
 | 
			
		||||
                      _posts.clear();
 | 
			
		||||
                      _fetchPosts();
 | 
			
		||||
                    },
 | 
			
		||||
                  );
 | 
			
		||||
                },
 | 
			
		||||
                separatorBuilder: (_, __) => const Gap(8),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -18,6 +18,7 @@ import 'package:surface/controllers/post_write_controller.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_realm.dart';
 | 
			
		||||
import 'package:surface/types/attachment.dart';
 | 
			
		||||
import 'package:surface/types/post.dart';
 | 
			
		||||
import 'package:surface/types/realm.dart';
 | 
			
		||||
@@ -36,7 +37,8 @@ import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:surface/widgets/post/post_poll_editor.dart';
 | 
			
		||||
import 'package:uuid/uuid.dart';
 | 
			
		||||
 | 
			
		||||
import '../../providers/sn_realm.dart';
 | 
			
		||||
const kPostTypes = ['Story', 'Article', 'Question', 'Video'];
 | 
			
		||||
const kPostTypeAliases = ['stories', 'articles', 'questions', 'videos'];
 | 
			
		||||
 | 
			
		||||
class PostEditorExtra {
 | 
			
		||||
  final String? text;
 | 
			
		||||
@@ -53,7 +55,7 @@ class PostEditorExtra {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class PostEditorScreen extends StatefulWidget {
 | 
			
		||||
  final String mode;
 | 
			
		||||
  final String? mode;
 | 
			
		||||
  final int? postEditId;
 | 
			
		||||
  final int? postReplyId;
 | 
			
		||||
  final int? postRepostId;
 | 
			
		||||
@@ -72,7 +74,10 @@ class PostEditorScreen extends StatefulWidget {
 | 
			
		||||
  State<PostEditorScreen> createState() => _PostEditorScreenState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _PostEditorScreenState extends State<PostEditorScreen> {
 | 
			
		||||
class _PostEditorScreenState extends State<PostEditorScreen>
 | 
			
		||||
    with SingleTickerProviderStateMixin {
 | 
			
		||||
  late final TabController _tabController =
 | 
			
		||||
      TabController(length: 4, vsync: this);
 | 
			
		||||
  late final PostWriteController _writeController = PostWriteController(
 | 
			
		||||
    doLoadFromTemporary: widget.postEditId == null,
 | 
			
		||||
  );
 | 
			
		||||
@@ -133,6 +138,15 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
 | 
			
		||||
    ],
 | 
			
		||||
    scope: HotKeyScope.inapp,
 | 
			
		||||
  );
 | 
			
		||||
  final HotKey _saveDraftHotKey = HotKey(
 | 
			
		||||
    key: PhysicalKeyboardKey.keyS,
 | 
			
		||||
    modifiers: [
 | 
			
		||||
      (!kIsWeb && Platform.isMacOS)
 | 
			
		||||
          ? HotKeyModifier.meta
 | 
			
		||||
          : HotKeyModifier.control
 | 
			
		||||
    ],
 | 
			
		||||
    scope: HotKeyScope.inapp,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  void _registerHotKey() {
 | 
			
		||||
    if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
 | 
			
		||||
@@ -148,6 +162,11 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
 | 
			
		||||
      ]);
 | 
			
		||||
      setState(() {});
 | 
			
		||||
    });
 | 
			
		||||
    hotKeyManager.register(_saveDraftHotKey, keyDownHandler: (_) async {
 | 
			
		||||
      if (mounted) {
 | 
			
		||||
        _writeController.sendPost(context, saveAsDraft: true);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _showPublisherPopup() {
 | 
			
		||||
@@ -209,9 +228,11 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void dispose() {
 | 
			
		||||
    _tabController.dispose();
 | 
			
		||||
    _writeController.dispose();
 | 
			
		||||
    if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) {
 | 
			
		||||
      hotKeyManager.unregister(_pasteHotKey);
 | 
			
		||||
      hotKeyManager.unregister(_saveDraftHotKey);
 | 
			
		||||
    }
 | 
			
		||||
    super.dispose();
 | 
			
		||||
  }
 | 
			
		||||
@@ -220,14 +241,16 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    _registerHotKey();
 | 
			
		||||
    if (!PostWriteController.kTitleMap.keys.contains(widget.mode)) {
 | 
			
		||||
      context.showErrorDialog('Unknown post type');
 | 
			
		||||
      Navigator.pop(context);
 | 
			
		||||
    } else {
 | 
			
		||||
      _writeController.setMode(widget.mode);
 | 
			
		||||
    }
 | 
			
		||||
    _fetchRealms();
 | 
			
		||||
    _fetchPublishers();
 | 
			
		||||
    if (widget.mode != null) {
 | 
			
		||||
      _writeController.setMode(widget.mode!);
 | 
			
		||||
    }
 | 
			
		||||
    _tabController.addListener(() {
 | 
			
		||||
      if (_tabController.indexIsChanging) {
 | 
			
		||||
        _writeController.setMode(kPostTypeAliases[_tabController.index]);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    _writeController.fetchRelatedPost(
 | 
			
		||||
      context,
 | 
			
		||||
      editing: widget.postEditId,
 | 
			
		||||
@@ -255,38 +278,55 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
 | 
			
		||||
                Navigator.pop(context);
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
            title: RichText(
 | 
			
		||||
              textAlign: TextAlign.center,
 | 
			
		||||
              text: TextSpan(children: [
 | 
			
		||||
                TextSpan(
 | 
			
		||||
                  text: _writeController.title.isNotEmpty
 | 
			
		||||
                      ? _writeController.title
 | 
			
		||||
                      : 'untitled'.tr(),
 | 
			
		||||
                  style: Theme.of(context).textTheme.titleLarge!.copyWith(
 | 
			
		||||
                        color: Theme.of(context).appBarTheme.foregroundColor!,
 | 
			
		||||
                      ),
 | 
			
		||||
                ),
 | 
			
		||||
                const TextSpan(text: '\n'),
 | 
			
		||||
                TextSpan(
 | 
			
		||||
                  text: PostWriteController.kTitleMap[widget.mode]!.tr(),
 | 
			
		||||
                  style: Theme.of(context).textTheme.bodySmall!.copyWith(
 | 
			
		||||
                        color: Theme.of(context).appBarTheme.foregroundColor!,
 | 
			
		||||
                      ),
 | 
			
		||||
                ),
 | 
			
		||||
              ]),
 | 
			
		||||
              maxLines: 2,
 | 
			
		||||
            title: Text(
 | 
			
		||||
              _writeController.title.isNotEmpty
 | 
			
		||||
                  ? _writeController.title
 | 
			
		||||
                  : 'untitled'.tr(),
 | 
			
		||||
            ),
 | 
			
		||||
            actions: [
 | 
			
		||||
              IconButton(
 | 
			
		||||
                icon: _writeController.editingDraft
 | 
			
		||||
                    ? const Icon(Icons.save)
 | 
			
		||||
                    : const Icon(Symbols.save_as),
 | 
			
		||||
                onPressed: () {
 | 
			
		||||
                  _writeController.sendPost(context, saveAsDraft: true).then(
 | 
			
		||||
                    (_) {
 | 
			
		||||
                      if (!context.mounted) return;
 | 
			
		||||
                      context.showSnackbar('postDraftSaved'.tr());
 | 
			
		||||
                      HapticFeedback.mediumImpact();
 | 
			
		||||
                    },
 | 
			
		||||
                  );
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
              IconButton(
 | 
			
		||||
                icon: const Icon(Symbols.tune),
 | 
			
		||||
                onPressed: _writeController.isBusy ? null : _updateMeta,
 | 
			
		||||
              ),
 | 
			
		||||
              const Gap(8),
 | 
			
		||||
            ],
 | 
			
		||||
            bottom: _writeController.isNotEmpty || widget.mode != null
 | 
			
		||||
                ? null
 | 
			
		||||
                : TabBar(
 | 
			
		||||
                    controller: _tabController,
 | 
			
		||||
                    tabs: [
 | 
			
		||||
                      for (final type in kPostTypes)
 | 
			
		||||
                        Tab(
 | 
			
		||||
                          child: Text(
 | 
			
		||||
                            'postType$type'.tr(),
 | 
			
		||||
                            style: TextStyle(
 | 
			
		||||
                              color: Theme.of(context)
 | 
			
		||||
                                  .appBarTheme
 | 
			
		||||
                                  .foregroundColor!,
 | 
			
		||||
                            ),
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                    ],
 | 
			
		||||
                  ),
 | 
			
		||||
          ),
 | 
			
		||||
          body: Column(
 | 
			
		||||
            children: [
 | 
			
		||||
              if (_writeController.editingPost != null)
 | 
			
		||||
              if (_writeController.editingPost != null &&
 | 
			
		||||
                  !_writeController.editingDraft)
 | 
			
		||||
                Container(
 | 
			
		||||
                  padding: const EdgeInsets.only(
 | 
			
		||||
                      top: 4, bottom: 4, left: 20, right: 20),
 | 
			
		||||
@@ -374,7 +414,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
 | 
			
		||||
                  children: [
 | 
			
		||||
                    SingleChildScrollView(
 | 
			
		||||
                      padding: EdgeInsets.only(bottom: 160),
 | 
			
		||||
                      child: StyledWidget(switch (_writeController.mode) {
 | 
			
		||||
                      child: switch (_writeController.mode) {
 | 
			
		||||
                        'stories' => _PostStoryEditor(
 | 
			
		||||
                            controller: _writeController,
 | 
			
		||||
                            onTapPublisher: _showPublisherPopup,
 | 
			
		||||
@@ -396,8 +436,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
 | 
			
		||||
                            onTapRealm: _showRealmPopup,
 | 
			
		||||
                          ),
 | 
			
		||||
                        _ => const Placeholder(),
 | 
			
		||||
                      })
 | 
			
		||||
                          .padding(top: 8),
 | 
			
		||||
                      },
 | 
			
		||||
                    ),
 | 
			
		||||
                    if (_writeController.attachments.isNotEmpty ||
 | 
			
		||||
                        _writeController.thumbnail != null)
 | 
			
		||||
@@ -720,7 +759,7 @@ class _PostStoryEditor extends StatelessWidget {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Container(
 | 
			
		||||
      padding: const EdgeInsets.symmetric(horizontal: 12),
 | 
			
		||||
      padding: const EdgeInsets.only(left: 12, right: 12, top: 8),
 | 
			
		||||
      constraints: const BoxConstraints(maxWidth: 640),
 | 
			
		||||
      child: Row(
 | 
			
		||||
        crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
@@ -969,7 +1008,7 @@ class _PostQuestionEditor extends StatelessWidget {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Container(
 | 
			
		||||
      padding: const EdgeInsets.symmetric(horizontal: 12),
 | 
			
		||||
      padding: const EdgeInsets.only(left: 12, right: 12, top: 8),
 | 
			
		||||
      constraints: const BoxConstraints(maxWidth: 640),
 | 
			
		||||
      child: Row(
 | 
			
		||||
        crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
@@ -1053,7 +1092,7 @@ class _PostQuestionEditor extends StatelessWidget {
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ).padding(top: 8),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1154,7 +1193,7 @@ class _PostVideoEditor extends StatelessWidget {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Container(
 | 
			
		||||
      padding: const EdgeInsets.symmetric(horizontal: 12),
 | 
			
		||||
      padding: const EdgeInsets.only(left: 12, right: 12, top: 8),
 | 
			
		||||
      constraints: const BoxConstraints(maxWidth: 640),
 | 
			
		||||
      child: Row(
 | 
			
		||||
        crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										132
									
								
								lib/screens/post/post_shuffle.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								lib/screens/post/post_shuffle.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,132 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_card_swiper/flutter_card_swiper.dart';
 | 
			
		||||
import 'package:material_symbols_icons/symbols.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/post.dart';
 | 
			
		||||
import 'package:surface/types/post.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:surface/widgets/post/post_item.dart';
 | 
			
		||||
 | 
			
		||||
class PostShuffleScreen extends StatefulWidget {
 | 
			
		||||
  const PostShuffleScreen({super.key});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<PostShuffleScreen> createState() => _PostShuffleScreenState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _PostShuffleScreenState extends State<PostShuffleScreen> {
 | 
			
		||||
  late final CardSwiperController _cardController = CardSwiperController();
 | 
			
		||||
 | 
			
		||||
  bool _isBusy = false;
 | 
			
		||||
  final List<SnPost> _posts = List.empty(growable: true);
 | 
			
		||||
 | 
			
		||||
  Future<void> _fetchPosts() async {
 | 
			
		||||
    _posts.clear();
 | 
			
		||||
    setState(() => _isBusy = true);
 | 
			
		||||
    try {
 | 
			
		||||
      final pt = context.read<SnPostContentProvider>();
 | 
			
		||||
      final result = await pt.listPosts(
 | 
			
		||||
        take: 10,
 | 
			
		||||
        offset: _posts.length,
 | 
			
		||||
        isShuffle: true,
 | 
			
		||||
      );
 | 
			
		||||
      _posts.addAll(result.$1);
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showErrorDialog(err);
 | 
			
		||||
    } finally {
 | 
			
		||||
      setState(() => _isBusy = false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    _fetchPosts();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void dispose() {
 | 
			
		||||
    super.dispose();
 | 
			
		||||
    _cardController.dispose();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: Text('postShuffle').tr(),
 | 
			
		||||
      ),
 | 
			
		||||
      body: Stack(
 | 
			
		||||
        children: [
 | 
			
		||||
          Column(
 | 
			
		||||
            children: [
 | 
			
		||||
              if (_isBusy || _posts.isEmpty)
 | 
			
		||||
                const Expanded(
 | 
			
		||||
                  child: Center(
 | 
			
		||||
                    child: CircularProgressIndicator(),
 | 
			
		||||
                  ),
 | 
			
		||||
                )
 | 
			
		||||
              else
 | 
			
		||||
                Expanded(
 | 
			
		||||
                  child: CardSwiper(
 | 
			
		||||
                    controller: _cardController,
 | 
			
		||||
                    isLoop: false,
 | 
			
		||||
                    padding: EdgeInsets.zero,
 | 
			
		||||
                    cardsCount: _posts.length,
 | 
			
		||||
                    cardBuilder: (context, idx, _, __) {
 | 
			
		||||
                      final ele = _posts[idx];
 | 
			
		||||
                      return SingleChildScrollView(
 | 
			
		||||
                        child: Center(
 | 
			
		||||
                          child: OpenablePostItem(
 | 
			
		||||
                            key: ValueKey(ele),
 | 
			
		||||
                            data: ele,
 | 
			
		||||
                            maxWidth: 640,
 | 
			
		||||
                            onChanged: (ele) {
 | 
			
		||||
                              _posts[idx] = ele;
 | 
			
		||||
                              setState(() {});
 | 
			
		||||
                            },
 | 
			
		||||
                            onDeleted: () {
 | 
			
		||||
                              _fetchPosts();
 | 
			
		||||
                            },
 | 
			
		||||
                          ).padding(
 | 
			
		||||
                            all: 24,
 | 
			
		||||
                            bottom:
 | 
			
		||||
                                MediaQuery.of(context).padding.bottom + 16 + 50,
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                      );
 | 
			
		||||
                    },
 | 
			
		||||
                    onEnd: () {
 | 
			
		||||
                      _fetchPosts();
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
          if (!_isBusy && _posts.isNotEmpty)
 | 
			
		||||
            Positioned(
 | 
			
		||||
              bottom: MediaQuery.of(context).padding.bottom + 16,
 | 
			
		||||
              left: 16,
 | 
			
		||||
              right: 16,
 | 
			
		||||
              child: Row(
 | 
			
		||||
                mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
                children: [
 | 
			
		||||
                  IconButton.filled(
 | 
			
		||||
                    icon: const Icon(Symbols.next_plan),
 | 
			
		||||
                    color: Theme.of(context).colorScheme.onPrimary,
 | 
			
		||||
                    onPressed: () {
 | 
			
		||||
                      _cardController.swipe(CardSwiperDirection.right);
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -61,7 +61,7 @@ class _AppSharingListenerState extends State<AppSharingListener> {
 | 
			
		||||
                      onTap: () {
 | 
			
		||||
                        GoRouter.of(context).pushNamed(
 | 
			
		||||
                          'postEditor',
 | 
			
		||||
                          pathParameters: {
 | 
			
		||||
                          queryParameters: {
 | 
			
		||||
                            'mode': 'stories',
 | 
			
		||||
                          },
 | 
			
		||||
                          extra: PostEditorExtra(
 | 
			
		||||
 
 | 
			
		||||
@@ -50,16 +50,17 @@ Future<ThemeData> createAppTheme(
 | 
			
		||||
      useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true);
 | 
			
		||||
 | 
			
		||||
  final inUseFonts = (customFonts ?? prefs.getString(kAppCustomFonts))
 | 
			
		||||
      ?.split(',')
 | 
			
		||||
      .map((ele) => ele.trim())
 | 
			
		||||
      .toList();
 | 
			
		||||
          ?.split(',')
 | 
			
		||||
          .map((ele) => ele.trim())
 | 
			
		||||
          .toList() ??
 | 
			
		||||
      ['Nunito'];
 | 
			
		||||
 | 
			
		||||
  return ThemeData(
 | 
			
		||||
    useMaterial3: useM3,
 | 
			
		||||
    colorScheme: colorScheme,
 | 
			
		||||
    brightness: brightness,
 | 
			
		||||
    fontFamily: inUseFonts?.firstOrNull,
 | 
			
		||||
    fontFamilyFallback: inUseFonts?.sublist(1),
 | 
			
		||||
    fontFamily: inUseFonts.firstOrNull,
 | 
			
		||||
    fontFamilyFallback: inUseFonts.sublist(1),
 | 
			
		||||
    iconTheme: IconThemeData(
 | 
			
		||||
      fill: 0,
 | 
			
		||||
      weight: 400,
 | 
			
		||||
 
 | 
			
		||||
@@ -119,13 +119,33 @@ abstract class SnAccountStatusInfo with _$SnAccountStatusInfo {
 | 
			
		||||
    required bool isDisturbable,
 | 
			
		||||
    required bool isOnline,
 | 
			
		||||
    required DateTime? lastSeenAt,
 | 
			
		||||
    required dynamic status,
 | 
			
		||||
    required SnAccountStatus? status,
 | 
			
		||||
  }) = _SnAccountStatusInfo;
 | 
			
		||||
 | 
			
		||||
  factory SnAccountStatusInfo.fromJson(Map<String, Object?> json) =>
 | 
			
		||||
      _$SnAccountStatusInfoFromJson(json);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@freezed
 | 
			
		||||
abstract class SnAccountStatus with _$SnAccountStatus {
 | 
			
		||||
  const factory SnAccountStatus({
 | 
			
		||||
    required int id,
 | 
			
		||||
    required DateTime createdAt,
 | 
			
		||||
    required DateTime updatedAt,
 | 
			
		||||
    required DateTime? deletedAt,
 | 
			
		||||
    required String type,
 | 
			
		||||
    required String label,
 | 
			
		||||
    required int attitude,
 | 
			
		||||
    required bool isNoDisturb,
 | 
			
		||||
    required bool isInvisible,
 | 
			
		||||
    required DateTime? clearAt,
 | 
			
		||||
    required int accountId,
 | 
			
		||||
  }) = _SnAccountStatus;
 | 
			
		||||
 | 
			
		||||
  factory SnAccountStatus.fromJson(Map<String, Object?> json) =>
 | 
			
		||||
      _$SnAccountStatusFromJson(json);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@freezed
 | 
			
		||||
abstract class SnAbuseReport with _$SnAbuseReport {
 | 
			
		||||
  const factory SnAbuseReport({
 | 
			
		||||
 
 | 
			
		||||
@@ -2139,7 +2139,7 @@ mixin _$SnAccountStatusInfo {
 | 
			
		||||
  bool get isDisturbable;
 | 
			
		||||
  bool get isOnline;
 | 
			
		||||
  DateTime? get lastSeenAt;
 | 
			
		||||
  dynamic get status;
 | 
			
		||||
  SnAccountStatus? get status;
 | 
			
		||||
 | 
			
		||||
  /// Create a copy of SnAccountStatusInfo
 | 
			
		||||
  /// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
@@ -2163,13 +2163,13 @@ mixin _$SnAccountStatusInfo {
 | 
			
		||||
                other.isOnline == isOnline) &&
 | 
			
		||||
            (identical(other.lastSeenAt, lastSeenAt) ||
 | 
			
		||||
                other.lastSeenAt == lastSeenAt) &&
 | 
			
		||||
            const DeepCollectionEquality().equals(other.status, status));
 | 
			
		||||
            (identical(other.status, status) || other.status == status));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
  @override
 | 
			
		||||
  int get hashCode => Object.hash(runtimeType, isDisturbable, isOnline,
 | 
			
		||||
      lastSeenAt, const DeepCollectionEquality().hash(status));
 | 
			
		||||
  int get hashCode =>
 | 
			
		||||
      Object.hash(runtimeType, isDisturbable, isOnline, lastSeenAt, status);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() {
 | 
			
		||||
@@ -2187,7 +2187,9 @@ abstract mixin class $SnAccountStatusInfoCopyWith<$Res> {
 | 
			
		||||
      {bool isDisturbable,
 | 
			
		||||
      bool isOnline,
 | 
			
		||||
      DateTime? lastSeenAt,
 | 
			
		||||
      dynamic status});
 | 
			
		||||
      SnAccountStatus? status});
 | 
			
		||||
 | 
			
		||||
  $SnAccountStatusCopyWith<$Res>? get status;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
@@ -2224,9 +2226,23 @@ class _$SnAccountStatusInfoCopyWithImpl<$Res>
 | 
			
		||||
      status: freezed == status
 | 
			
		||||
          ? _self.status
 | 
			
		||||
          : status // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as dynamic,
 | 
			
		||||
              as SnAccountStatus?,
 | 
			
		||||
    ));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Create a copy of SnAccountStatusInfo
 | 
			
		||||
  /// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
  @override
 | 
			
		||||
  @pragma('vm:prefer-inline')
 | 
			
		||||
  $SnAccountStatusCopyWith<$Res>? get status {
 | 
			
		||||
    if (_self.status == null) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return $SnAccountStatusCopyWith<$Res>(_self.status!, (value) {
 | 
			
		||||
      return _then(_self.copyWith(status: value));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
@@ -2247,7 +2263,7 @@ class _SnAccountStatusInfo implements SnAccountStatusInfo {
 | 
			
		||||
  @override
 | 
			
		||||
  final DateTime? lastSeenAt;
 | 
			
		||||
  @override
 | 
			
		||||
  final dynamic status;
 | 
			
		||||
  final SnAccountStatus? status;
 | 
			
		||||
 | 
			
		||||
  /// Create a copy of SnAccountStatusInfo
 | 
			
		||||
  /// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
@@ -2276,13 +2292,13 @@ class _SnAccountStatusInfo implements SnAccountStatusInfo {
 | 
			
		||||
                other.isOnline == isOnline) &&
 | 
			
		||||
            (identical(other.lastSeenAt, lastSeenAt) ||
 | 
			
		||||
                other.lastSeenAt == lastSeenAt) &&
 | 
			
		||||
            const DeepCollectionEquality().equals(other.status, status));
 | 
			
		||||
            (identical(other.status, status) || other.status == status));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
  @override
 | 
			
		||||
  int get hashCode => Object.hash(runtimeType, isDisturbable, isOnline,
 | 
			
		||||
      lastSeenAt, const DeepCollectionEquality().hash(status));
 | 
			
		||||
  int get hashCode =>
 | 
			
		||||
      Object.hash(runtimeType, isDisturbable, isOnline, lastSeenAt, status);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() {
 | 
			
		||||
@@ -2302,7 +2318,10 @@ abstract mixin class _$SnAccountStatusInfoCopyWith<$Res>
 | 
			
		||||
      {bool isDisturbable,
 | 
			
		||||
      bool isOnline,
 | 
			
		||||
      DateTime? lastSeenAt,
 | 
			
		||||
      dynamic status});
 | 
			
		||||
      SnAccountStatus? status});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  $SnAccountStatusCopyWith<$Res>? get status;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
@@ -2339,7 +2358,386 @@ class __$SnAccountStatusInfoCopyWithImpl<$Res>
 | 
			
		||||
      status: freezed == status
 | 
			
		||||
          ? _self.status
 | 
			
		||||
          : status // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as dynamic,
 | 
			
		||||
              as SnAccountStatus?,
 | 
			
		||||
    ));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Create a copy of SnAccountStatusInfo
 | 
			
		||||
  /// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
  @override
 | 
			
		||||
  @pragma('vm:prefer-inline')
 | 
			
		||||
  $SnAccountStatusCopyWith<$Res>? get status {
 | 
			
		||||
    if (_self.status == null) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return $SnAccountStatusCopyWith<$Res>(_self.status!, (value) {
 | 
			
		||||
      return _then(_self.copyWith(status: value));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
mixin _$SnAccountStatus {
 | 
			
		||||
  int get id;
 | 
			
		||||
  DateTime get createdAt;
 | 
			
		||||
  DateTime get updatedAt;
 | 
			
		||||
  DateTime? get deletedAt;
 | 
			
		||||
  String get type;
 | 
			
		||||
  String get label;
 | 
			
		||||
  int get attitude;
 | 
			
		||||
  bool get isNoDisturb;
 | 
			
		||||
  bool get isInvisible;
 | 
			
		||||
  DateTime? get clearAt;
 | 
			
		||||
  int get accountId;
 | 
			
		||||
 | 
			
		||||
  /// Create a copy of SnAccountStatus
 | 
			
		||||
  /// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
  @JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
  @pragma('vm:prefer-inline')
 | 
			
		||||
  $SnAccountStatusCopyWith<SnAccountStatus> get copyWith =>
 | 
			
		||||
      _$SnAccountStatusCopyWithImpl<SnAccountStatus>(
 | 
			
		||||
          this as SnAccountStatus, _$identity);
 | 
			
		||||
 | 
			
		||||
  /// Serializes this SnAccountStatus to a JSON map.
 | 
			
		||||
  Map<String, dynamic> toJson();
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  bool operator ==(Object other) {
 | 
			
		||||
    return identical(this, other) ||
 | 
			
		||||
        (other.runtimeType == runtimeType &&
 | 
			
		||||
            other is SnAccountStatus &&
 | 
			
		||||
            (identical(other.id, id) || other.id == id) &&
 | 
			
		||||
            (identical(other.createdAt, createdAt) ||
 | 
			
		||||
                other.createdAt == createdAt) &&
 | 
			
		||||
            (identical(other.updatedAt, updatedAt) ||
 | 
			
		||||
                other.updatedAt == updatedAt) &&
 | 
			
		||||
            (identical(other.deletedAt, deletedAt) ||
 | 
			
		||||
                other.deletedAt == deletedAt) &&
 | 
			
		||||
            (identical(other.type, type) || other.type == type) &&
 | 
			
		||||
            (identical(other.label, label) || other.label == label) &&
 | 
			
		||||
            (identical(other.attitude, attitude) ||
 | 
			
		||||
                other.attitude == attitude) &&
 | 
			
		||||
            (identical(other.isNoDisturb, isNoDisturb) ||
 | 
			
		||||
                other.isNoDisturb == isNoDisturb) &&
 | 
			
		||||
            (identical(other.isInvisible, isInvisible) ||
 | 
			
		||||
                other.isInvisible == isInvisible) &&
 | 
			
		||||
            (identical(other.clearAt, clearAt) || other.clearAt == clearAt) &&
 | 
			
		||||
            (identical(other.accountId, accountId) ||
 | 
			
		||||
                other.accountId == accountId));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
  @override
 | 
			
		||||
  int get hashCode => Object.hash(
 | 
			
		||||
      runtimeType,
 | 
			
		||||
      id,
 | 
			
		||||
      createdAt,
 | 
			
		||||
      updatedAt,
 | 
			
		||||
      deletedAt,
 | 
			
		||||
      type,
 | 
			
		||||
      label,
 | 
			
		||||
      attitude,
 | 
			
		||||
      isNoDisturb,
 | 
			
		||||
      isInvisible,
 | 
			
		||||
      clearAt,
 | 
			
		||||
      accountId);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() {
 | 
			
		||||
    return 'SnAccountStatus(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, label: $label, attitude: $attitude, isNoDisturb: $isNoDisturb, isInvisible: $isInvisible, clearAt: $clearAt, accountId: $accountId)';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
abstract mixin class $SnAccountStatusCopyWith<$Res> {
 | 
			
		||||
  factory $SnAccountStatusCopyWith(
 | 
			
		||||
          SnAccountStatus value, $Res Function(SnAccountStatus) _then) =
 | 
			
		||||
      _$SnAccountStatusCopyWithImpl;
 | 
			
		||||
  @useResult
 | 
			
		||||
  $Res call(
 | 
			
		||||
      {int id,
 | 
			
		||||
      DateTime createdAt,
 | 
			
		||||
      DateTime updatedAt,
 | 
			
		||||
      DateTime? deletedAt,
 | 
			
		||||
      String type,
 | 
			
		||||
      String label,
 | 
			
		||||
      int attitude,
 | 
			
		||||
      bool isNoDisturb,
 | 
			
		||||
      bool isInvisible,
 | 
			
		||||
      DateTime? clearAt,
 | 
			
		||||
      int accountId});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
class _$SnAccountStatusCopyWithImpl<$Res>
 | 
			
		||||
    implements $SnAccountStatusCopyWith<$Res> {
 | 
			
		||||
  _$SnAccountStatusCopyWithImpl(this._self, this._then);
 | 
			
		||||
 | 
			
		||||
  final SnAccountStatus _self;
 | 
			
		||||
  final $Res Function(SnAccountStatus) _then;
 | 
			
		||||
 | 
			
		||||
  /// Create a copy of SnAccountStatus
 | 
			
		||||
  /// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
  @pragma('vm:prefer-inline')
 | 
			
		||||
  @override
 | 
			
		||||
  $Res call({
 | 
			
		||||
    Object? id = null,
 | 
			
		||||
    Object? createdAt = null,
 | 
			
		||||
    Object? updatedAt = null,
 | 
			
		||||
    Object? deletedAt = freezed,
 | 
			
		||||
    Object? type = null,
 | 
			
		||||
    Object? label = null,
 | 
			
		||||
    Object? attitude = null,
 | 
			
		||||
    Object? isNoDisturb = null,
 | 
			
		||||
    Object? isInvisible = null,
 | 
			
		||||
    Object? clearAt = freezed,
 | 
			
		||||
    Object? accountId = null,
 | 
			
		||||
  }) {
 | 
			
		||||
    return _then(_self.copyWith(
 | 
			
		||||
      id: null == id
 | 
			
		||||
          ? _self.id
 | 
			
		||||
          : id // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as int,
 | 
			
		||||
      createdAt: null == createdAt
 | 
			
		||||
          ? _self.createdAt
 | 
			
		||||
          : createdAt // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as DateTime,
 | 
			
		||||
      updatedAt: null == updatedAt
 | 
			
		||||
          ? _self.updatedAt
 | 
			
		||||
          : updatedAt // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as DateTime,
 | 
			
		||||
      deletedAt: freezed == deletedAt
 | 
			
		||||
          ? _self.deletedAt
 | 
			
		||||
          : deletedAt // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as DateTime?,
 | 
			
		||||
      type: null == type
 | 
			
		||||
          ? _self.type
 | 
			
		||||
          : type // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as String,
 | 
			
		||||
      label: null == label
 | 
			
		||||
          ? _self.label
 | 
			
		||||
          : label // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as String,
 | 
			
		||||
      attitude: null == attitude
 | 
			
		||||
          ? _self.attitude
 | 
			
		||||
          : attitude // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as int,
 | 
			
		||||
      isNoDisturb: null == isNoDisturb
 | 
			
		||||
          ? _self.isNoDisturb
 | 
			
		||||
          : isNoDisturb // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as bool,
 | 
			
		||||
      isInvisible: null == isInvisible
 | 
			
		||||
          ? _self.isInvisible
 | 
			
		||||
          : isInvisible // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as bool,
 | 
			
		||||
      clearAt: freezed == clearAt
 | 
			
		||||
          ? _self.clearAt
 | 
			
		||||
          : clearAt // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as DateTime?,
 | 
			
		||||
      accountId: null == accountId
 | 
			
		||||
          ? _self.accountId
 | 
			
		||||
          : accountId // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as int,
 | 
			
		||||
    ));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
@JsonSerializable()
 | 
			
		||||
class _SnAccountStatus implements SnAccountStatus {
 | 
			
		||||
  const _SnAccountStatus(
 | 
			
		||||
      {required this.id,
 | 
			
		||||
      required this.createdAt,
 | 
			
		||||
      required this.updatedAt,
 | 
			
		||||
      required this.deletedAt,
 | 
			
		||||
      required this.type,
 | 
			
		||||
      required this.label,
 | 
			
		||||
      required this.attitude,
 | 
			
		||||
      required this.isNoDisturb,
 | 
			
		||||
      required this.isInvisible,
 | 
			
		||||
      required this.clearAt,
 | 
			
		||||
      required this.accountId});
 | 
			
		||||
  factory _SnAccountStatus.fromJson(Map<String, dynamic> json) =>
 | 
			
		||||
      _$SnAccountStatusFromJson(json);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  final int id;
 | 
			
		||||
  @override
 | 
			
		||||
  final DateTime createdAt;
 | 
			
		||||
  @override
 | 
			
		||||
  final DateTime updatedAt;
 | 
			
		||||
  @override
 | 
			
		||||
  final DateTime? deletedAt;
 | 
			
		||||
  @override
 | 
			
		||||
  final String type;
 | 
			
		||||
  @override
 | 
			
		||||
  final String label;
 | 
			
		||||
  @override
 | 
			
		||||
  final int attitude;
 | 
			
		||||
  @override
 | 
			
		||||
  final bool isNoDisturb;
 | 
			
		||||
  @override
 | 
			
		||||
  final bool isInvisible;
 | 
			
		||||
  @override
 | 
			
		||||
  final DateTime? clearAt;
 | 
			
		||||
  @override
 | 
			
		||||
  final int accountId;
 | 
			
		||||
 | 
			
		||||
  /// Create a copy of SnAccountStatus
 | 
			
		||||
  /// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
  @override
 | 
			
		||||
  @JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
  @pragma('vm:prefer-inline')
 | 
			
		||||
  _$SnAccountStatusCopyWith<_SnAccountStatus> get copyWith =>
 | 
			
		||||
      __$SnAccountStatusCopyWithImpl<_SnAccountStatus>(this, _$identity);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Map<String, dynamic> toJson() {
 | 
			
		||||
    return _$SnAccountStatusToJson(
 | 
			
		||||
      this,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  bool operator ==(Object other) {
 | 
			
		||||
    return identical(this, other) ||
 | 
			
		||||
        (other.runtimeType == runtimeType &&
 | 
			
		||||
            other is _SnAccountStatus &&
 | 
			
		||||
            (identical(other.id, id) || other.id == id) &&
 | 
			
		||||
            (identical(other.createdAt, createdAt) ||
 | 
			
		||||
                other.createdAt == createdAt) &&
 | 
			
		||||
            (identical(other.updatedAt, updatedAt) ||
 | 
			
		||||
                other.updatedAt == updatedAt) &&
 | 
			
		||||
            (identical(other.deletedAt, deletedAt) ||
 | 
			
		||||
                other.deletedAt == deletedAt) &&
 | 
			
		||||
            (identical(other.type, type) || other.type == type) &&
 | 
			
		||||
            (identical(other.label, label) || other.label == label) &&
 | 
			
		||||
            (identical(other.attitude, attitude) ||
 | 
			
		||||
                other.attitude == attitude) &&
 | 
			
		||||
            (identical(other.isNoDisturb, isNoDisturb) ||
 | 
			
		||||
                other.isNoDisturb == isNoDisturb) &&
 | 
			
		||||
            (identical(other.isInvisible, isInvisible) ||
 | 
			
		||||
                other.isInvisible == isInvisible) &&
 | 
			
		||||
            (identical(other.clearAt, clearAt) || other.clearAt == clearAt) &&
 | 
			
		||||
            (identical(other.accountId, accountId) ||
 | 
			
		||||
                other.accountId == accountId));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
  @override
 | 
			
		||||
  int get hashCode => Object.hash(
 | 
			
		||||
      runtimeType,
 | 
			
		||||
      id,
 | 
			
		||||
      createdAt,
 | 
			
		||||
      updatedAt,
 | 
			
		||||
      deletedAt,
 | 
			
		||||
      type,
 | 
			
		||||
      label,
 | 
			
		||||
      attitude,
 | 
			
		||||
      isNoDisturb,
 | 
			
		||||
      isInvisible,
 | 
			
		||||
      clearAt,
 | 
			
		||||
      accountId);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() {
 | 
			
		||||
    return 'SnAccountStatus(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, label: $label, attitude: $attitude, isNoDisturb: $isNoDisturb, isInvisible: $isInvisible, clearAt: $clearAt, accountId: $accountId)';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
abstract mixin class _$SnAccountStatusCopyWith<$Res>
 | 
			
		||||
    implements $SnAccountStatusCopyWith<$Res> {
 | 
			
		||||
  factory _$SnAccountStatusCopyWith(
 | 
			
		||||
          _SnAccountStatus value, $Res Function(_SnAccountStatus) _then) =
 | 
			
		||||
      __$SnAccountStatusCopyWithImpl;
 | 
			
		||||
  @override
 | 
			
		||||
  @useResult
 | 
			
		||||
  $Res call(
 | 
			
		||||
      {int id,
 | 
			
		||||
      DateTime createdAt,
 | 
			
		||||
      DateTime updatedAt,
 | 
			
		||||
      DateTime? deletedAt,
 | 
			
		||||
      String type,
 | 
			
		||||
      String label,
 | 
			
		||||
      int attitude,
 | 
			
		||||
      bool isNoDisturb,
 | 
			
		||||
      bool isInvisible,
 | 
			
		||||
      DateTime? clearAt,
 | 
			
		||||
      int accountId});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
class __$SnAccountStatusCopyWithImpl<$Res>
 | 
			
		||||
    implements _$SnAccountStatusCopyWith<$Res> {
 | 
			
		||||
  __$SnAccountStatusCopyWithImpl(this._self, this._then);
 | 
			
		||||
 | 
			
		||||
  final _SnAccountStatus _self;
 | 
			
		||||
  final $Res Function(_SnAccountStatus) _then;
 | 
			
		||||
 | 
			
		||||
  /// Create a copy of SnAccountStatus
 | 
			
		||||
  /// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
  @override
 | 
			
		||||
  @pragma('vm:prefer-inline')
 | 
			
		||||
  $Res call({
 | 
			
		||||
    Object? id = null,
 | 
			
		||||
    Object? createdAt = null,
 | 
			
		||||
    Object? updatedAt = null,
 | 
			
		||||
    Object? deletedAt = freezed,
 | 
			
		||||
    Object? type = null,
 | 
			
		||||
    Object? label = null,
 | 
			
		||||
    Object? attitude = null,
 | 
			
		||||
    Object? isNoDisturb = null,
 | 
			
		||||
    Object? isInvisible = null,
 | 
			
		||||
    Object? clearAt = freezed,
 | 
			
		||||
    Object? accountId = null,
 | 
			
		||||
  }) {
 | 
			
		||||
    return _then(_SnAccountStatus(
 | 
			
		||||
      id: null == id
 | 
			
		||||
          ? _self.id
 | 
			
		||||
          : id // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as int,
 | 
			
		||||
      createdAt: null == createdAt
 | 
			
		||||
          ? _self.createdAt
 | 
			
		||||
          : createdAt // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as DateTime,
 | 
			
		||||
      updatedAt: null == updatedAt
 | 
			
		||||
          ? _self.updatedAt
 | 
			
		||||
          : updatedAt // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as DateTime,
 | 
			
		||||
      deletedAt: freezed == deletedAt
 | 
			
		||||
          ? _self.deletedAt
 | 
			
		||||
          : deletedAt // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as DateTime?,
 | 
			
		||||
      type: null == type
 | 
			
		||||
          ? _self.type
 | 
			
		||||
          : type // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as String,
 | 
			
		||||
      label: null == label
 | 
			
		||||
          ? _self.label
 | 
			
		||||
          : label // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as String,
 | 
			
		||||
      attitude: null == attitude
 | 
			
		||||
          ? _self.attitude
 | 
			
		||||
          : attitude // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as int,
 | 
			
		||||
      isNoDisturb: null == isNoDisturb
 | 
			
		||||
          ? _self.isNoDisturb
 | 
			
		||||
          : isNoDisturb // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as bool,
 | 
			
		||||
      isInvisible: null == isInvisible
 | 
			
		||||
          ? _self.isInvisible
 | 
			
		||||
          : isInvisible // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as bool,
 | 
			
		||||
      clearAt: freezed == clearAt
 | 
			
		||||
          ? _self.clearAt
 | 
			
		||||
          : clearAt // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as DateTime?,
 | 
			
		||||
      accountId: null == accountId
 | 
			
		||||
          ? _self.accountId
 | 
			
		||||
          : accountId // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as int,
 | 
			
		||||
    ));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -210,7 +210,9 @@ _SnAccountStatusInfo _$SnAccountStatusInfoFromJson(Map<String, dynamic> json) =>
 | 
			
		||||
      lastSeenAt: json['last_seen_at'] == null
 | 
			
		||||
          ? null
 | 
			
		||||
          : DateTime.parse(json['last_seen_at'] as String),
 | 
			
		||||
      status: json['status'],
 | 
			
		||||
      status: json['status'] == null
 | 
			
		||||
          ? null
 | 
			
		||||
          : SnAccountStatus.fromJson(json['status'] as Map<String, dynamic>),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
Map<String, dynamic> _$SnAccountStatusInfoToJson(
 | 
			
		||||
@@ -219,7 +221,41 @@ Map<String, dynamic> _$SnAccountStatusInfoToJson(
 | 
			
		||||
      'is_disturbable': instance.isDisturbable,
 | 
			
		||||
      'is_online': instance.isOnline,
 | 
			
		||||
      'last_seen_at': instance.lastSeenAt?.toIso8601String(),
 | 
			
		||||
      'status': instance.status,
 | 
			
		||||
      'status': instance.status?.toJson(),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
_SnAccountStatus _$SnAccountStatusFromJson(Map<String, dynamic> json) =>
 | 
			
		||||
    _SnAccountStatus(
 | 
			
		||||
      id: (json['id'] as num).toInt(),
 | 
			
		||||
      createdAt: DateTime.parse(json['created_at'] as String),
 | 
			
		||||
      updatedAt: DateTime.parse(json['updated_at'] as String),
 | 
			
		||||
      deletedAt: json['deleted_at'] == null
 | 
			
		||||
          ? null
 | 
			
		||||
          : DateTime.parse(json['deleted_at'] as String),
 | 
			
		||||
      type: json['type'] as String,
 | 
			
		||||
      label: json['label'] as String,
 | 
			
		||||
      attitude: (json['attitude'] as num).toInt(),
 | 
			
		||||
      isNoDisturb: json['is_no_disturb'] as bool,
 | 
			
		||||
      isInvisible: json['is_invisible'] as bool,
 | 
			
		||||
      clearAt: json['clear_at'] == null
 | 
			
		||||
          ? null
 | 
			
		||||
          : DateTime.parse(json['clear_at'] as String),
 | 
			
		||||
      accountId: (json['account_id'] as num).toInt(),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
Map<String, dynamic> _$SnAccountStatusToJson(_SnAccountStatus instance) =>
 | 
			
		||||
    <String, dynamic>{
 | 
			
		||||
      'id': instance.id,
 | 
			
		||||
      'created_at': instance.createdAt.toIso8601String(),
 | 
			
		||||
      'updated_at': instance.updatedAt.toIso8601String(),
 | 
			
		||||
      'deleted_at': instance.deletedAt?.toIso8601String(),
 | 
			
		||||
      'type': instance.type,
 | 
			
		||||
      'label': instance.label,
 | 
			
		||||
      'attitude': instance.attitude,
 | 
			
		||||
      'is_no_disturb': instance.isNoDisturb,
 | 
			
		||||
      'is_invisible': instance.isInvisible,
 | 
			
		||||
      'clear_at': instance.clearAt?.toIso8601String(),
 | 
			
		||||
      'account_id': instance.accountId,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
_SnAbuseReport _$SnAbuseReportFromJson(Map<String, dynamic> json) =>
 | 
			
		||||
 
 | 
			
		||||
@@ -25,11 +25,13 @@ abstract class SnCheckInRecord with _$SnCheckInRecord {
 | 
			
		||||
    required int resultTier,
 | 
			
		||||
    required int resultExperience,
 | 
			
		||||
    required double resultCoin,
 | 
			
		||||
    @Default(0) int currentStreak,
 | 
			
		||||
    required List<int> resultModifiers,
 | 
			
		||||
    required int accountId,
 | 
			
		||||
  }) = _SnCheckInRecord;
 | 
			
		||||
 | 
			
		||||
  factory SnCheckInRecord.fromJson(Map<String, dynamic> json) => _$SnCheckInRecordFromJson(json);
 | 
			
		||||
  factory SnCheckInRecord.fromJson(Map<String, dynamic> json) =>
 | 
			
		||||
      _$SnCheckInRecordFromJson(json);
 | 
			
		||||
 | 
			
		||||
  String get symbol => kCheckInResultTierSymbols[resultTier];
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,7 @@ mixin _$SnCheckInRecord {
 | 
			
		||||
  int get resultTier;
 | 
			
		||||
  int get resultExperience;
 | 
			
		||||
  double get resultCoin;
 | 
			
		||||
  int get currentStreak;
 | 
			
		||||
  List<int> get resultModifiers;
 | 
			
		||||
  int get accountId;
 | 
			
		||||
 | 
			
		||||
@@ -54,6 +55,8 @@ mixin _$SnCheckInRecord {
 | 
			
		||||
                other.resultExperience == resultExperience) &&
 | 
			
		||||
            (identical(other.resultCoin, resultCoin) ||
 | 
			
		||||
                other.resultCoin == resultCoin) &&
 | 
			
		||||
            (identical(other.currentStreak, currentStreak) ||
 | 
			
		||||
                other.currentStreak == currentStreak) &&
 | 
			
		||||
            const DeepCollectionEquality()
 | 
			
		||||
                .equals(other.resultModifiers, resultModifiers) &&
 | 
			
		||||
            (identical(other.accountId, accountId) ||
 | 
			
		||||
@@ -71,12 +74,13 @@ mixin _$SnCheckInRecord {
 | 
			
		||||
      resultTier,
 | 
			
		||||
      resultExperience,
 | 
			
		||||
      resultCoin,
 | 
			
		||||
      currentStreak,
 | 
			
		||||
      const DeepCollectionEquality().hash(resultModifiers),
 | 
			
		||||
      accountId);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() {
 | 
			
		||||
    return 'SnCheckInRecord(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resultTier: $resultTier, resultExperience: $resultExperience, resultCoin: $resultCoin, resultModifiers: $resultModifiers, accountId: $accountId)';
 | 
			
		||||
    return 'SnCheckInRecord(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resultTier: $resultTier, resultExperience: $resultExperience, resultCoin: $resultCoin, currentStreak: $currentStreak, resultModifiers: $resultModifiers, accountId: $accountId)';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -94,6 +98,7 @@ abstract mixin class $SnCheckInRecordCopyWith<$Res> {
 | 
			
		||||
      int resultTier,
 | 
			
		||||
      int resultExperience,
 | 
			
		||||
      double resultCoin,
 | 
			
		||||
      int currentStreak,
 | 
			
		||||
      List<int> resultModifiers,
 | 
			
		||||
      int accountId});
 | 
			
		||||
}
 | 
			
		||||
@@ -118,6 +123,7 @@ class _$SnCheckInRecordCopyWithImpl<$Res>
 | 
			
		||||
    Object? resultTier = null,
 | 
			
		||||
    Object? resultExperience = null,
 | 
			
		||||
    Object? resultCoin = null,
 | 
			
		||||
    Object? currentStreak = null,
 | 
			
		||||
    Object? resultModifiers = null,
 | 
			
		||||
    Object? accountId = null,
 | 
			
		||||
  }) {
 | 
			
		||||
@@ -150,6 +156,10 @@ class _$SnCheckInRecordCopyWithImpl<$Res>
 | 
			
		||||
          ? _self.resultCoin
 | 
			
		||||
          : resultCoin // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as double,
 | 
			
		||||
      currentStreak: null == currentStreak
 | 
			
		||||
          ? _self.currentStreak
 | 
			
		||||
          : currentStreak // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as int,
 | 
			
		||||
      resultModifiers: null == resultModifiers
 | 
			
		||||
          ? _self.resultModifiers
 | 
			
		||||
          : resultModifiers // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
@@ -173,6 +183,7 @@ class _SnCheckInRecord extends SnCheckInRecord {
 | 
			
		||||
      required this.resultTier,
 | 
			
		||||
      required this.resultExperience,
 | 
			
		||||
      required this.resultCoin,
 | 
			
		||||
      this.currentStreak = 0,
 | 
			
		||||
      required final List<int> resultModifiers,
 | 
			
		||||
      required this.accountId})
 | 
			
		||||
      : _resultModifiers = resultModifiers,
 | 
			
		||||
@@ -194,6 +205,9 @@ class _SnCheckInRecord extends SnCheckInRecord {
 | 
			
		||||
  final int resultExperience;
 | 
			
		||||
  @override
 | 
			
		||||
  final double resultCoin;
 | 
			
		||||
  @override
 | 
			
		||||
  @JsonKey()
 | 
			
		||||
  final int currentStreak;
 | 
			
		||||
  final List<int> _resultModifiers;
 | 
			
		||||
  @override
 | 
			
		||||
  List<int> get resultModifiers {
 | 
			
		||||
@@ -238,6 +252,8 @@ class _SnCheckInRecord extends SnCheckInRecord {
 | 
			
		||||
                other.resultExperience == resultExperience) &&
 | 
			
		||||
            (identical(other.resultCoin, resultCoin) ||
 | 
			
		||||
                other.resultCoin == resultCoin) &&
 | 
			
		||||
            (identical(other.currentStreak, currentStreak) ||
 | 
			
		||||
                other.currentStreak == currentStreak) &&
 | 
			
		||||
            const DeepCollectionEquality()
 | 
			
		||||
                .equals(other._resultModifiers, _resultModifiers) &&
 | 
			
		||||
            (identical(other.accountId, accountId) ||
 | 
			
		||||
@@ -255,12 +271,13 @@ class _SnCheckInRecord extends SnCheckInRecord {
 | 
			
		||||
      resultTier,
 | 
			
		||||
      resultExperience,
 | 
			
		||||
      resultCoin,
 | 
			
		||||
      currentStreak,
 | 
			
		||||
      const DeepCollectionEquality().hash(_resultModifiers),
 | 
			
		||||
      accountId);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() {
 | 
			
		||||
    return 'SnCheckInRecord(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resultTier: $resultTier, resultExperience: $resultExperience, resultCoin: $resultCoin, resultModifiers: $resultModifiers, accountId: $accountId)';
 | 
			
		||||
    return 'SnCheckInRecord(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resultTier: $resultTier, resultExperience: $resultExperience, resultCoin: $resultCoin, currentStreak: $currentStreak, resultModifiers: $resultModifiers, accountId: $accountId)';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -280,6 +297,7 @@ abstract mixin class _$SnCheckInRecordCopyWith<$Res>
 | 
			
		||||
      int resultTier,
 | 
			
		||||
      int resultExperience,
 | 
			
		||||
      double resultCoin,
 | 
			
		||||
      int currentStreak,
 | 
			
		||||
      List<int> resultModifiers,
 | 
			
		||||
      int accountId});
 | 
			
		||||
}
 | 
			
		||||
@@ -304,6 +322,7 @@ class __$SnCheckInRecordCopyWithImpl<$Res>
 | 
			
		||||
    Object? resultTier = null,
 | 
			
		||||
    Object? resultExperience = null,
 | 
			
		||||
    Object? resultCoin = null,
 | 
			
		||||
    Object? currentStreak = null,
 | 
			
		||||
    Object? resultModifiers = null,
 | 
			
		||||
    Object? accountId = null,
 | 
			
		||||
  }) {
 | 
			
		||||
@@ -336,6 +355,10 @@ class __$SnCheckInRecordCopyWithImpl<$Res>
 | 
			
		||||
          ? _self.resultCoin
 | 
			
		||||
          : resultCoin // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as double,
 | 
			
		||||
      currentStreak: null == currentStreak
 | 
			
		||||
          ? _self.currentStreak
 | 
			
		||||
          : currentStreak // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as int,
 | 
			
		||||
      resultModifiers: null == resultModifiers
 | 
			
		||||
          ? _self._resultModifiers
 | 
			
		||||
          : resultModifiers // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@ _SnCheckInRecord _$SnCheckInRecordFromJson(Map<String, dynamic> json) =>
 | 
			
		||||
      resultTier: (json['result_tier'] as num).toInt(),
 | 
			
		||||
      resultExperience: (json['result_experience'] as num).toInt(),
 | 
			
		||||
      resultCoin: (json['result_coin'] as num).toDouble(),
 | 
			
		||||
      currentStreak: (json['current_streak'] as num?)?.toInt() ?? 0,
 | 
			
		||||
      resultModifiers: (json['result_modifiers'] as List<dynamic>)
 | 
			
		||||
          .map((e) => (e as num).toInt())
 | 
			
		||||
          .toList(),
 | 
			
		||||
@@ -32,6 +33,7 @@ Map<String, dynamic> _$SnCheckInRecordToJson(_SnCheckInRecord instance) =>
 | 
			
		||||
      'result_tier': instance.resultTier,
 | 
			
		||||
      'result_experience': instance.resultExperience,
 | 
			
		||||
      'result_coin': instance.resultCoin,
 | 
			
		||||
      'current_streak': instance.currentStreak,
 | 
			
		||||
      'result_modifiers': instance.resultModifiers,
 | 
			
		||||
      'account_id': instance.accountId,
 | 
			
		||||
    };
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ class AccountImage extends StatelessWidget {
 | 
			
		||||
  final double? borderRadius;
 | 
			
		||||
  final Widget? fallbackWidget;
 | 
			
		||||
  final Widget? badge;
 | 
			
		||||
  final Offset? badgeOffset;
 | 
			
		||||
 | 
			
		||||
  const AccountImage({
 | 
			
		||||
    super.key,
 | 
			
		||||
@@ -23,6 +24,7 @@ class AccountImage extends StatelessWidget {
 | 
			
		||||
    this.borderRadius,
 | 
			
		||||
    this.fallbackWidget,
 | 
			
		||||
    this.badge,
 | 
			
		||||
    this.badgeOffset,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@@ -40,7 +42,8 @@ class AccountImage extends StatelessWidget {
 | 
			
		||||
            borderRadius: BorderRadius.circular(borderRadius ?? radius ?? 20),
 | 
			
		||||
            child: (content?.isEmpty ?? true)
 | 
			
		||||
                ? Container(
 | 
			
		||||
                    color: backgroundColor ?? Theme.of(context).colorScheme.primaryContainer,
 | 
			
		||||
                    color: backgroundColor ??
 | 
			
		||||
                        Theme.of(context).colorScheme.primaryContainer,
 | 
			
		||||
                    child: (fallbackWidget ??
 | 
			
		||||
                            Icon(
 | 
			
		||||
                              Symbols.account_circle,
 | 
			
		||||
@@ -58,8 +61,8 @@ class AccountImage extends StatelessWidget {
 | 
			
		||||
        ),
 | 
			
		||||
        if (badge != null)
 | 
			
		||||
          Positioned(
 | 
			
		||||
            right: -4,
 | 
			
		||||
            bottom: -2,
 | 
			
		||||
            right: badgeOffset?.dx ?? -4,
 | 
			
		||||
            bottom: badgeOffset?.dy ?? -2,
 | 
			
		||||
            child: badge!,
 | 
			
		||||
          ),
 | 
			
		||||
      ],
 | 
			
		||||
 
 | 
			
		||||
@@ -8,9 +8,9 @@ import 'package:relative_time/relative_time.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/experience.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/screens/account/profile_page.dart';
 | 
			
		||||
import 'package:surface/types/account.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
import 'package:surface/widgets/account/badge.dart';
 | 
			
		||||
import 'package:surface/widgets/universal_image.dart';
 | 
			
		||||
 | 
			
		||||
class AccountPopoverCard extends StatelessWidget {
 | 
			
		||||
@@ -72,37 +72,21 @@ class AccountPopoverCard extends StatelessWidget {
 | 
			
		||||
            const Gap(8)
 | 
			
		||||
          ],
 | 
			
		||||
        ).padding(horizontal: 16),
 | 
			
		||||
        if (data.badges.isNotEmpty) const Gap(12),
 | 
			
		||||
        if (data.badges.isNotEmpty)
 | 
			
		||||
          Wrap(
 | 
			
		||||
            spacing: 4,
 | 
			
		||||
            children: data.badges
 | 
			
		||||
                .map(
 | 
			
		||||
                  (ele) => Tooltip(
 | 
			
		||||
                    richMessage: TextSpan(
 | 
			
		||||
                      children: [
 | 
			
		||||
                        TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()),
 | 
			
		||||
                        if (ele.metadata['title'] != null)
 | 
			
		||||
                          TextSpan(
 | 
			
		||||
                            text: '\n${ele.metadata['title']}',
 | 
			
		||||
                            style: const TextStyle(fontWeight: FontWeight.bold),
 | 
			
		||||
                          ),
 | 
			
		||||
                        TextSpan(text: '\n'),
 | 
			
		||||
                        TextSpan(
 | 
			
		||||
                          text: DateFormat.yMEd().format(ele.createdAt),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ],
 | 
			
		||||
                    ),
 | 
			
		||||
                    child: Icon(
 | 
			
		||||
                      kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark,
 | 
			
		||||
                      color: kBadgesMeta[ele.type]?.$3,
 | 
			
		||||
                      fill: 1,
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                  (ele) => AccountBadge(badge: ele),
 | 
			
		||||
                )
 | 
			
		||||
                .toList(),
 | 
			
		||||
          ).padding(horizontal: 24),
 | 
			
		||||
        const Gap(8),
 | 
			
		||||
          ).padding(horizontal: 24, bottom: 12, top: 12),
 | 
			
		||||
        if (data.profile?.description.isNotEmpty ?? false)
 | 
			
		||||
          Text(
 | 
			
		||||
            data.profile?.description ?? '',
 | 
			
		||||
            maxLines: 2,
 | 
			
		||||
            overflow: TextOverflow.ellipsis,
 | 
			
		||||
          ).padding(horizontal: 26, bottom: 8),
 | 
			
		||||
        Row(
 | 
			
		||||
          crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
          children: [
 | 
			
		||||
@@ -110,7 +94,9 @@ class AccountPopoverCard extends StatelessWidget {
 | 
			
		||||
            const Gap(8),
 | 
			
		||||
            Text('Lv${getLevelFromExp(data.profile?.experience ?? 0)}'),
 | 
			
		||||
            const Gap(8),
 | 
			
		||||
            Text(calcLevelUpProgressLevel(data.profile?.experience ?? 0)).fontSize(11).opacity(0.5),
 | 
			
		||||
            Text(calcLevelUpProgressLevel(data.profile?.experience ?? 0))
 | 
			
		||||
                .fontSize(11)
 | 
			
		||||
                .opacity(0.5),
 | 
			
		||||
            const Gap(8),
 | 
			
		||||
            Container(
 | 
			
		||||
              width: double.infinity,
 | 
			
		||||
@@ -126,25 +112,36 @@ class AccountPopoverCard extends StatelessWidget {
 | 
			
		||||
        FutureBuilder(
 | 
			
		||||
          future: sn.client.get('/cgi/id/users/${data.name}/status'),
 | 
			
		||||
          builder: (context, snapshot) {
 | 
			
		||||
            final SnAccountStatusInfo? status =
 | 
			
		||||
                snapshot.hasData ? SnAccountStatusInfo.fromJson(snapshot.data!.data) : null;
 | 
			
		||||
            final SnAccountStatusInfo? status = snapshot.hasData
 | 
			
		||||
                ? SnAccountStatusInfo.fromJson(snapshot.data!.data)
 | 
			
		||||
                : null;
 | 
			
		||||
            return Row(
 | 
			
		||||
              children: [
 | 
			
		||||
                Icon(
 | 
			
		||||
                  Symbols.circle,
 | 
			
		||||
                  fill: 1,
 | 
			
		||||
                  (status?.isDisturbable ?? true)
 | 
			
		||||
                      ? Symbols.circle
 | 
			
		||||
                      : Symbols.do_not_disturb_on,
 | 
			
		||||
                  fill: (status?.isOnline ?? false) ? 1 : 0,
 | 
			
		||||
                  size: 16,
 | 
			
		||||
                  color: (status?.isOnline ?? false) ? Colors.green : Colors.grey,
 | 
			
		||||
                  color: (status?.isOnline ?? false)
 | 
			
		||||
                      ? (status?.isDisturbable ?? true)
 | 
			
		||||
                          ? Colors.green
 | 
			
		||||
                          : Colors.red
 | 
			
		||||
                      : Colors.grey,
 | 
			
		||||
                ).padding(all: 4),
 | 
			
		||||
                const Gap(8),
 | 
			
		||||
                Text(
 | 
			
		||||
                  status != null
 | 
			
		||||
                      ? status.isOnline
 | 
			
		||||
                          ? 'accountStatusOnline'.tr()
 | 
			
		||||
                          : 'accountStatusOffline'.tr()
 | 
			
		||||
                      ? (status.status?.label.isNotEmpty ?? false)
 | 
			
		||||
                          ? status.status!.label
 | 
			
		||||
                          : status.isOnline
 | 
			
		||||
                              ? 'accountStatusOnline'.tr()
 | 
			
		||||
                              : 'accountStatusOffline'.tr()
 | 
			
		||||
                      : 'loading'.tr(),
 | 
			
		||||
                ),
 | 
			
		||||
                if (status != null && !status.isOnline && status.lastSeenAt != null)
 | 
			
		||||
                if (status != null &&
 | 
			
		||||
                    !status.isOnline &&
 | 
			
		||||
                    status.lastSeenAt != null)
 | 
			
		||||
                  Text(
 | 
			
		||||
                    'accountStatusLastSeen'.tr(args: [
 | 
			
		||||
                      status.lastSeenAt != null
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										391
									
								
								lib/widgets/account/account_status.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										391
									
								
								lib/widgets/account/account_status.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,391 @@
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.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:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/types/account.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
			
		||||
 | 
			
		||||
final Map<String, (Widget, String, String?)> kPresetStatus = {
 | 
			
		||||
  'online': (
 | 
			
		||||
    const Icon(Symbols.circle, color: Colors.green, fill: 1),
 | 
			
		||||
    'accountStatusOnline'.tr(),
 | 
			
		||||
    null,
 | 
			
		||||
  ),
 | 
			
		||||
  'silent': (
 | 
			
		||||
    const Icon(Symbols.do_not_disturb_on, color: Colors.red),
 | 
			
		||||
    'accountStatusSilent'.tr(),
 | 
			
		||||
    'accountStatusSilentDesc'.tr(),
 | 
			
		||||
  ),
 | 
			
		||||
  'invisible': (
 | 
			
		||||
    const Icon(Symbols.circle, color: Colors.grey),
 | 
			
		||||
    'accountStatusInvisible'.tr(),
 | 
			
		||||
    'accountStatusInvisibleDesc'.tr(),
 | 
			
		||||
  ),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class AccountStatusActionPopup extends StatefulWidget {
 | 
			
		||||
  final SnAccountStatusInfo? currentStatus;
 | 
			
		||||
  const AccountStatusActionPopup({super.key, this.currentStatus});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<AccountStatusActionPopup> createState() =>
 | 
			
		||||
      _AccountStatusActionPopupState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _AccountStatusActionPopupState extends State<AccountStatusActionPopup> {
 | 
			
		||||
  bool _isBusy = false;
 | 
			
		||||
 | 
			
		||||
  Future<void> setStatus(
 | 
			
		||||
    String type,
 | 
			
		||||
    String? label,
 | 
			
		||||
    int attitude, {
 | 
			
		||||
    bool isUpdate = false,
 | 
			
		||||
    bool isSilent = false,
 | 
			
		||||
    bool isInvisible = false,
 | 
			
		||||
    DateTime? clearAt,
 | 
			
		||||
  }) async {
 | 
			
		||||
    setState(() => _isBusy = true);
 | 
			
		||||
    final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
 | 
			
		||||
    final payload = {
 | 
			
		||||
      'type': type,
 | 
			
		||||
      'label': label,
 | 
			
		||||
      'attitude': attitude,
 | 
			
		||||
      'is_no_disturb': isSilent,
 | 
			
		||||
      'is_invisible': isInvisible,
 | 
			
		||||
      'clear_at': clearAt?.toUtc().toIso8601String()
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      await sn.client.request(
 | 
			
		||||
        '/cgi/id/users/me/status',
 | 
			
		||||
        data: payload,
 | 
			
		||||
        options: Options(method: isUpdate ? 'PUT' : 'POST'),
 | 
			
		||||
      );
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      Navigator.pop(context, true);
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showErrorDialog(err);
 | 
			
		||||
    } finally {
 | 
			
		||||
      setState(() => _isBusy = false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _clearStatus() async {
 | 
			
		||||
    if (_isBusy) return;
 | 
			
		||||
 | 
			
		||||
    setState(() => _isBusy = true);
 | 
			
		||||
    try {
 | 
			
		||||
      final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
      await sn.client.delete('/cgi/id/users/me/status');
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      Navigator.of(context).pop(true);
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showErrorDialog(err);
 | 
			
		||||
    } finally {
 | 
			
		||||
      setState(() => _isBusy = false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Column(
 | 
			
		||||
      crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
      children: [
 | 
			
		||||
        Row(
 | 
			
		||||
          crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
          children: [
 | 
			
		||||
            const Icon(Symbols.mood, size: 24),
 | 
			
		||||
            const Gap(16),
 | 
			
		||||
            Text('accountChangeStatus',
 | 
			
		||||
                    style: Theme.of(context).textTheme.titleLarge)
 | 
			
		||||
                .tr(),
 | 
			
		||||
          ],
 | 
			
		||||
        ).padding(horizontal: 20, top: 16, bottom: 12),
 | 
			
		||||
        LoadingIndicator(isActive: _isBusy),
 | 
			
		||||
        SizedBox(
 | 
			
		||||
          height: 48,
 | 
			
		||||
          child: ListView(
 | 
			
		||||
            padding: EdgeInsets.symmetric(horizontal: 18),
 | 
			
		||||
            scrollDirection: Axis.horizontal,
 | 
			
		||||
            children: kPresetStatus.entries
 | 
			
		||||
                .map(
 | 
			
		||||
                  (x) => StyledWidget(ActionChip(
 | 
			
		||||
                    avatar: x.value.$1,
 | 
			
		||||
                    label: Text(x.value.$2),
 | 
			
		||||
                    tooltip: x.value.$3,
 | 
			
		||||
                    onPressed: _isBusy
 | 
			
		||||
                        ? null
 | 
			
		||||
                        : () {
 | 
			
		||||
                            setStatus(
 | 
			
		||||
                              x.key,
 | 
			
		||||
                              x.value.$2,
 | 
			
		||||
                              0,
 | 
			
		||||
                              isInvisible: x.key == 'invisible',
 | 
			
		||||
                              isSilent: x.key == 'silent',
 | 
			
		||||
                            );
 | 
			
		||||
                          },
 | 
			
		||||
                  )).padding(right: 6),
 | 
			
		||||
                )
 | 
			
		||||
                .toList(),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        const Gap(16),
 | 
			
		||||
        const Divider(thickness: 0.3, height: 0.3),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
          leading: widget.currentStatus != null
 | 
			
		||||
              ? const Icon(Icons.edit)
 | 
			
		||||
              : const Icon(Icons.add),
 | 
			
		||||
          title: Text('accountCustomStatus').tr(),
 | 
			
		||||
          subtitle: Text('accountCustomStatusDescription').tr(),
 | 
			
		||||
          onTap: _isBusy
 | 
			
		||||
              ? null
 | 
			
		||||
              : () async {
 | 
			
		||||
                  final val = await showDialog(
 | 
			
		||||
                    context: context,
 | 
			
		||||
                    builder: (context) => _AccountStatusEditorDialog(
 | 
			
		||||
                      currentStatus: widget.currentStatus,
 | 
			
		||||
                    ),
 | 
			
		||||
                  );
 | 
			
		||||
                  if (val == true && context.mounted) {
 | 
			
		||||
                    Navigator.of(context).pop(true);
 | 
			
		||||
                  }
 | 
			
		||||
                },
 | 
			
		||||
        ),
 | 
			
		||||
        if (widget.currentStatus != null)
 | 
			
		||||
          ListTile(
 | 
			
		||||
            contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
            leading: const Icon(Icons.clear),
 | 
			
		||||
            title: Text('accountClearStatus').tr(),
 | 
			
		||||
            subtitle: Text('accountClearStatusDescription').tr(),
 | 
			
		||||
            onTap: _isBusy
 | 
			
		||||
                ? null
 | 
			
		||||
                : () {
 | 
			
		||||
                    _clearStatus();
 | 
			
		||||
                  },
 | 
			
		||||
          ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _AccountStatusEditorDialog extends StatefulWidget {
 | 
			
		||||
  final SnAccountStatusInfo? currentStatus;
 | 
			
		||||
  const _AccountStatusEditorDialog({this.currentStatus});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<_AccountStatusEditorDialog> createState() =>
 | 
			
		||||
      _AccountStatusEditorDialogState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _AccountStatusEditorDialogState
 | 
			
		||||
    extends State<_AccountStatusEditorDialog> {
 | 
			
		||||
  bool _isBusy = false;
 | 
			
		||||
 | 
			
		||||
  final TextEditingController _labelController = TextEditingController();
 | 
			
		||||
  final TextEditingController _clearAtController = TextEditingController();
 | 
			
		||||
 | 
			
		||||
  int _attitude = 0;
 | 
			
		||||
  bool _isSilent = false;
 | 
			
		||||
  bool _isInvisible = false;
 | 
			
		||||
  DateTime? _clearAt;
 | 
			
		||||
 | 
			
		||||
  Future<void> _selectClearAt() async {
 | 
			
		||||
    final DateTime? pickedDate = await showDatePicker(
 | 
			
		||||
      context: context,
 | 
			
		||||
      initialDate: _clearAt?.toLocal() ?? DateTime.now(),
 | 
			
		||||
      firstDate: DateTime.now(),
 | 
			
		||||
      lastDate: DateTime.now().add(const Duration(days: 365)),
 | 
			
		||||
    );
 | 
			
		||||
    if (pickedDate == null) return;
 | 
			
		||||
    if (!mounted) return;
 | 
			
		||||
    final TimeOfDay? pickedTime = await showTimePicker(
 | 
			
		||||
      context: context,
 | 
			
		||||
      initialTime: TimeOfDay.now(),
 | 
			
		||||
    );
 | 
			
		||||
    if (pickedTime == null) return;
 | 
			
		||||
    if (!mounted) return;
 | 
			
		||||
    final picked = pickedDate.copyWith(
 | 
			
		||||
      hour: pickedTime.hour,
 | 
			
		||||
      minute: pickedTime.minute,
 | 
			
		||||
    );
 | 
			
		||||
    setState(() {
 | 
			
		||||
      _clearAt = picked;
 | 
			
		||||
      _clearAtController.text = DateFormat('y/M/d HH:mm').format(_clearAt!);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _applyStatus() async {
 | 
			
		||||
    if (_isBusy) return;
 | 
			
		||||
 | 
			
		||||
    setState(() => _isBusy = true);
 | 
			
		||||
    try {
 | 
			
		||||
      final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
      await sn.client.request(
 | 
			
		||||
        '/cgi/id/users/me/status',
 | 
			
		||||
        data: {
 | 
			
		||||
          'type': 'custom',
 | 
			
		||||
          'label': _labelController.text,
 | 
			
		||||
          'attitude': _attitude,
 | 
			
		||||
          'is_no_disturb': _isSilent,
 | 
			
		||||
          'is_invisible': _isInvisible,
 | 
			
		||||
          'clear_at': _clearAt?.toUtc().toIso8601String(),
 | 
			
		||||
        },
 | 
			
		||||
        options: Options(
 | 
			
		||||
          method: widget.currentStatus?.status != null ? 'PUT' : 'POST',
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      Navigator.of(context).pop(true);
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showErrorDialog(err);
 | 
			
		||||
    } finally {
 | 
			
		||||
      setState(() => _isBusy = false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _syncWidget() {
 | 
			
		||||
    if (widget.currentStatus?.status != null) {
 | 
			
		||||
      _clearAt = widget.currentStatus!.status!.clearAt;
 | 
			
		||||
      if (_clearAt != null) {
 | 
			
		||||
        _clearAtController.text = DateFormat('y/M/d HH:mm').format(_clearAt!);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      _labelController.text = widget.currentStatus!.status!.label;
 | 
			
		||||
      _attitude = widget.currentStatus!.status!.attitude;
 | 
			
		||||
      _isInvisible = widget.currentStatus!.status!.isInvisible;
 | 
			
		||||
      _isSilent = widget.currentStatus!.status!.isNoDisturb;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    _syncWidget();
 | 
			
		||||
    super.initState();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AlertDialog(
 | 
			
		||||
      title: Text('accountCustomStatus').tr(),
 | 
			
		||||
      content: Column(
 | 
			
		||||
        mainAxisSize: MainAxisSize.min,
 | 
			
		||||
        children: [
 | 
			
		||||
          LoadingIndicator(isActive: _isBusy),
 | 
			
		||||
          TextField(
 | 
			
		||||
            controller: _labelController,
 | 
			
		||||
            decoration: InputDecoration(
 | 
			
		||||
              isDense: true,
 | 
			
		||||
              prefixIcon: const Icon(Icons.label),
 | 
			
		||||
              border: const OutlineInputBorder(),
 | 
			
		||||
              labelText: 'fieldAccountStatusLabel'.tr(),
 | 
			
		||||
            ),
 | 
			
		||||
            onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
          ),
 | 
			
		||||
          const Gap(8),
 | 
			
		||||
          TextField(
 | 
			
		||||
            controller: _clearAtController,
 | 
			
		||||
            readOnly: true,
 | 
			
		||||
            decoration: InputDecoration(
 | 
			
		||||
              isDense: true,
 | 
			
		||||
              prefixIcon: const Icon(Icons.event_busy),
 | 
			
		||||
              border: const OutlineInputBorder(),
 | 
			
		||||
              labelText: 'fieldAccountStatusClearAt'.tr(),
 | 
			
		||||
            ),
 | 
			
		||||
            onTap: () => _selectClearAt(),
 | 
			
		||||
          ),
 | 
			
		||||
          const Gap(8),
 | 
			
		||||
          SingleChildScrollView(
 | 
			
		||||
            scrollDirection: Axis.horizontal,
 | 
			
		||||
            child: Wrap(
 | 
			
		||||
              spacing: 6,
 | 
			
		||||
              runSpacing: 0,
 | 
			
		||||
              children: [
 | 
			
		||||
                ChoiceChip(
 | 
			
		||||
                  avatar: Icon(
 | 
			
		||||
                    Symbols.radio_button_unchecked,
 | 
			
		||||
                    color: Theme.of(context).colorScheme.onSurfaceVariant,
 | 
			
		||||
                  ),
 | 
			
		||||
                  selected: _attitude == 2,
 | 
			
		||||
                  label: Text('accountStatusNegative'.tr()),
 | 
			
		||||
                  onSelected: (val) {
 | 
			
		||||
                    if (val) setState(() => _attitude = 2);
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                ChoiceChip(
 | 
			
		||||
                  avatar: Icon(
 | 
			
		||||
                    Symbols.contrast,
 | 
			
		||||
                    color: Theme.of(context).colorScheme.onSurfaceVariant,
 | 
			
		||||
                  ),
 | 
			
		||||
                  selected: _attitude == 0,
 | 
			
		||||
                  label: Text('accountStatusNeutral'.tr()),
 | 
			
		||||
                  onSelected: (val) {
 | 
			
		||||
                    if (val) setState(() => _attitude = 0);
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                ChoiceChip(
 | 
			
		||||
                  avatar: Icon(
 | 
			
		||||
                    Symbols.circle,
 | 
			
		||||
                    color: Theme.of(context).colorScheme.onSurfaceVariant,
 | 
			
		||||
                  ),
 | 
			
		||||
                  selected: _attitude == 1,
 | 
			
		||||
                  label: Text('accountStatusPositive'.tr()),
 | 
			
		||||
                  onSelected: (val) {
 | 
			
		||||
                    if (val) setState(() => _attitude = 1);
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          const Gap(4),
 | 
			
		||||
          SingleChildScrollView(
 | 
			
		||||
            scrollDirection: Axis.horizontal,
 | 
			
		||||
            child: Wrap(
 | 
			
		||||
              spacing: 6,
 | 
			
		||||
              runSpacing: 0,
 | 
			
		||||
              children: [
 | 
			
		||||
                ChoiceChip(
 | 
			
		||||
                  selected: _isSilent,
 | 
			
		||||
                  label: Text('accountStatusSilent').tr(),
 | 
			
		||||
                  onSelected: (val) {
 | 
			
		||||
                    setState(() => _isSilent = val);
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                ChoiceChip(
 | 
			
		||||
                  selected: _isInvisible,
 | 
			
		||||
                  label: Text('accountStatusInvisible').tr(),
 | 
			
		||||
                  onSelected: (val) {
 | 
			
		||||
                    setState(() => _isInvisible = val);
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
      actions: <Widget>[
 | 
			
		||||
        TextButton(
 | 
			
		||||
          style: TextButton.styleFrom(
 | 
			
		||||
            foregroundColor:
 | 
			
		||||
                Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
 | 
			
		||||
          ),
 | 
			
		||||
          onPressed: _isBusy ? null : () => Navigator.pop(context),
 | 
			
		||||
          child: Text('dialogCancel').tr(),
 | 
			
		||||
        ),
 | 
			
		||||
        TextButton(
 | 
			
		||||
          onPressed: _isBusy ? null : () => _applyStatus(),
 | 
			
		||||
          child: Text('dialogConfirm').tr(),
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										50
									
								
								lib/widgets/account/badge.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								lib/widgets/account/badge.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:material_symbols_icons/symbols.dart';
 | 
			
		||||
import 'package:surface/screens/account/profile_page.dart' show kBadgesMeta;
 | 
			
		||||
import 'package:surface/types/account.dart';
 | 
			
		||||
 | 
			
		||||
class AccountBadge extends StatelessWidget {
 | 
			
		||||
  final SnAccountBadge badge;
 | 
			
		||||
  final double radius;
 | 
			
		||||
  final EdgeInsets? padding;
 | 
			
		||||
  const AccountBadge({
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.badge,
 | 
			
		||||
    this.radius = 20,
 | 
			
		||||
    this.padding,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Tooltip(
 | 
			
		||||
      richMessage: TextSpan(
 | 
			
		||||
        children: [
 | 
			
		||||
          TextSpan(text: kBadgesMeta[badge.type]?.$1.tr() ?? 'unknown'.tr()),
 | 
			
		||||
          if (badge.metadata['title'] != null)
 | 
			
		||||
            TextSpan(
 | 
			
		||||
              text: '\n${badge.metadata['title']}',
 | 
			
		||||
              style: const TextStyle(fontWeight: FontWeight.bold),
 | 
			
		||||
            ),
 | 
			
		||||
          TextSpan(text: '\n'),
 | 
			
		||||
          TextSpan(
 | 
			
		||||
            text: DateFormat.yMEd().format(badge.createdAt),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
      child: Container(
 | 
			
		||||
        padding: padding ?? EdgeInsets.all(3),
 | 
			
		||||
        decoration: BoxDecoration(
 | 
			
		||||
          borderRadius: BorderRadius.circular(radius),
 | 
			
		||||
          color: kBadgesMeta[badge.type]?.$3,
 | 
			
		||||
        ),
 | 
			
		||||
        child: Icon(
 | 
			
		||||
          kBadgesMeta[badge.type]?.$2 ?? Symbols.question_mark,
 | 
			
		||||
          color: Colors.white,
 | 
			
		||||
          fill: 1,
 | 
			
		||||
          size: radius - 4,
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -22,12 +22,14 @@ class AttachmentItem extends StatelessWidget {
 | 
			
		||||
  final SnAttachment? data;
 | 
			
		||||
  final String? heroTag;
 | 
			
		||||
  final BoxFit fit;
 | 
			
		||||
  final FilterQuality? filterQuality;
 | 
			
		||||
 | 
			
		||||
  const AttachmentItem({
 | 
			
		||||
    super.key,
 | 
			
		||||
    this.fit = BoxFit.cover,
 | 
			
		||||
    required this.data,
 | 
			
		||||
    required this.heroTag,
 | 
			
		||||
    this.filterQuality,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  Widget _buildContent(BuildContext context) {
 | 
			
		||||
@@ -47,6 +49,7 @@ class AttachmentItem extends StatelessWidget {
 | 
			
		||||
            sn.getAttachmentUrl(data!.rid),
 | 
			
		||||
            key: Key('attachment-${data!.rid}-$tag'),
 | 
			
		||||
            fit: fit,
 | 
			
		||||
            filterQuality: filterQuality,
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
      case 'video':
 | 
			
		||||
@@ -83,13 +86,16 @@ class _AttachmentItemSensitiveBlur extends StatefulWidget {
 | 
			
		||||
  final Widget child;
 | 
			
		||||
  final bool isCompact;
 | 
			
		||||
 | 
			
		||||
  const _AttachmentItemSensitiveBlur({required this.child, this.isCompact = false});
 | 
			
		||||
  const _AttachmentItemSensitiveBlur(
 | 
			
		||||
      {required this.child, this.isCompact = false});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<_AttachmentItemSensitiveBlur> createState() => _AttachmentItemSensitiveBlurState();
 | 
			
		||||
  State<_AttachmentItemSensitiveBlur> createState() =>
 | 
			
		||||
      _AttachmentItemSensitiveBlurState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _AttachmentItemSensitiveBlurState extends State<_AttachmentItemSensitiveBlur> {
 | 
			
		||||
class _AttachmentItemSensitiveBlurState
 | 
			
		||||
    extends State<_AttachmentItemSensitiveBlur> {
 | 
			
		||||
  bool _doesShow = false;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@@ -124,10 +130,15 @@ class _AttachmentItemSensitiveBlurState extends State<_AttachmentItemSensitiveBl
 | 
			
		||||
                      Text(
 | 
			
		||||
                        'sensitiveContentDescription',
 | 
			
		||||
                        textAlign: TextAlign.center,
 | 
			
		||||
                      ).tr().fontSize(14).textColor(Colors.white.withOpacity(0.8)),
 | 
			
		||||
                      )
 | 
			
		||||
                          .tr()
 | 
			
		||||
                          .fontSize(14)
 | 
			
		||||
                          .textColor(Colors.white.withOpacity(0.8)),
 | 
			
		||||
                    if (!widget.isCompact) const Gap(16),
 | 
			
		||||
                    InkWell(
 | 
			
		||||
                      child: Text('sensitiveContentReveal').tr().textColor(Colors.white),
 | 
			
		||||
                      child: Text('sensitiveContentReveal')
 | 
			
		||||
                          .tr()
 | 
			
		||||
                          .textColor(Colors.white),
 | 
			
		||||
                      onTap: () {
 | 
			
		||||
                        setState(() => _doesShow = !_doesShow);
 | 
			
		||||
                      },
 | 
			
		||||
@@ -137,7 +148,9 @@ class _AttachmentItemSensitiveBlurState extends State<_AttachmentItemSensitiveBl
 | 
			
		||||
              ).center(),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ).opacity(_doesShow ? 0 : 1, animate: true).animate(const Duration(milliseconds: 300), Curves.easeInOut),
 | 
			
		||||
        )
 | 
			
		||||
            .opacity(_doesShow ? 0 : 1, animate: true)
 | 
			
		||||
            .animate(const Duration(milliseconds: 300), Curves.easeInOut),
 | 
			
		||||
        if (_doesShow)
 | 
			
		||||
          Positioned(
 | 
			
		||||
            top: 0,
 | 
			
		||||
@@ -174,10 +187,12 @@ class _AttachmentItemContentVideo extends StatefulWidget {
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<_AttachmentItemContentVideo> createState() => _AttachmentItemContentVideoState();
 | 
			
		||||
  State<_AttachmentItemContentVideo> createState() =>
 | 
			
		||||
      _AttachmentItemContentVideoState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo> {
 | 
			
		||||
class _AttachmentItemContentVideoState
 | 
			
		||||
    extends State<_AttachmentItemContentVideo> {
 | 
			
		||||
  bool _showContent = false;
 | 
			
		||||
  bool _showOriginal = false;
 | 
			
		||||
 | 
			
		||||
@@ -188,7 +203,9 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
 | 
			
		||||
    setState(() => _showContent = true);
 | 
			
		||||
    MediaKit.ensureInitialized();
 | 
			
		||||
    final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
    final url = _showOriginal ? sn.getAttachmentUrl(widget.data.rid) : sn.getAttachmentUrl(widget.data.compressed!.rid);
 | 
			
		||||
    final url = _showOriginal
 | 
			
		||||
        ? sn.getAttachmentUrl(widget.data.rid)
 | 
			
		||||
        : sn.getAttachmentUrl(widget.data.compressed!.rid);
 | 
			
		||||
    _videoPlayer = Player();
 | 
			
		||||
    _videoController = VideoController(_videoPlayer!);
 | 
			
		||||
    _videoPlayer!.open(Media(url), play: !widget.isAutoload);
 | 
			
		||||
@@ -201,7 +218,9 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
 | 
			
		||||
    final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
    _videoPlayer?.open(
 | 
			
		||||
      Media(
 | 
			
		||||
        _showOriginal ? sn.getAttachmentUrl(widget.data.rid) : sn.getAttachmentUrl(widget.data.compressed!.rid),
 | 
			
		||||
        _showOriginal
 | 
			
		||||
            ? sn.getAttachmentUrl(widget.data.rid)
 | 
			
		||||
            : sn.getAttachmentUrl(widget.data.compressed!.rid),
 | 
			
		||||
      ),
 | 
			
		||||
      play: true,
 | 
			
		||||
    );
 | 
			
		||||
@@ -283,7 +302,9 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
 | 
			
		||||
                          ),
 | 
			
		||||
                          Text(
 | 
			
		||||
                            Duration(
 | 
			
		||||
                              milliseconds: (widget.data.data['duration'] ?? 0).toInt() * 1000,
 | 
			
		||||
                              milliseconds:
 | 
			
		||||
                                  (widget.data.data['duration'] ?? 0).toInt() *
 | 
			
		||||
                                      1000,
 | 
			
		||||
                            ).toString(),
 | 
			
		||||
                            style: GoogleFonts.robotoMono(
 | 
			
		||||
                              fontSize: 12,
 | 
			
		||||
@@ -346,7 +367,9 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
 | 
			
		||||
            MaterialDesktopCustomButton(
 | 
			
		||||
              iconSize: 24,
 | 
			
		||||
              onPressed: _toggleOriginal,
 | 
			
		||||
              icon: _showOriginal ? const Icon(Symbols.high_quality, size: 24) : const Icon(Symbols.sd, size: 24),
 | 
			
		||||
              icon: _showOriginal
 | 
			
		||||
                  ? const Icon(Symbols.high_quality, size: 24)
 | 
			
		||||
                  : const Icon(Symbols.sd, size: 24),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
@@ -354,8 +377,9 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
 | 
			
		||||
        child: Video(
 | 
			
		||||
          controller: _videoController!,
 | 
			
		||||
          aspectRatio: ratio,
 | 
			
		||||
          controls:
 | 
			
		||||
              !kIsWeb && (Platform.isAndroid || Platform.isIOS) ? MaterialVideoControls : MaterialDesktopVideoControls,
 | 
			
		||||
          controls: !kIsWeb && (Platform.isAndroid || Platform.isIOS)
 | 
			
		||||
              ? MaterialVideoControls
 | 
			
		||||
              : MaterialDesktopVideoControls,
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
@@ -378,10 +402,12 @@ class _AttachmentItemContentAudio extends StatefulWidget {
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<_AttachmentItemContentAudio> createState() => _AttachmentItemContentAudioState();
 | 
			
		||||
  State<_AttachmentItemContentAudio> createState() =>
 | 
			
		||||
      _AttachmentItemContentAudioState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _AttachmentItemContentAudioState extends State<_AttachmentItemContentAudio> {
 | 
			
		||||
class _AttachmentItemContentAudioState
 | 
			
		||||
    extends State<_AttachmentItemContentAudio> {
 | 
			
		||||
  bool _showContent = false;
 | 
			
		||||
 | 
			
		||||
  double? _draggingValue;
 | 
			
		||||
@@ -552,8 +578,12 @@ class _AttachmentItemContentAudioState extends State<_AttachmentItemContentAudio
 | 
			
		||||
                            overlayShape: SliderComponentShape.noOverlay,
 | 
			
		||||
                          ),
 | 
			
		||||
                          child: Slider(
 | 
			
		||||
                            secondaryTrackValue: _bufferedPosition.inMilliseconds.abs().toDouble(),
 | 
			
		||||
                            value: _draggingValue?.abs() ?? _position.inMilliseconds.toDouble().abs(),
 | 
			
		||||
                            secondaryTrackValue: _bufferedPosition
 | 
			
		||||
                                .inMilliseconds
 | 
			
		||||
                                .abs()
 | 
			
		||||
                                .toDouble(),
 | 
			
		||||
                            value: _draggingValue?.abs() ??
 | 
			
		||||
                                _position.inMilliseconds.toDouble().abs(),
 | 
			
		||||
                            min: 0,
 | 
			
		||||
                            max: math
 | 
			
		||||
                                .max(
 | 
			
		||||
@@ -593,7 +623,9 @@ class _AttachmentItemContentAudioState extends State<_AttachmentItemContentAudio
 | 
			
		||||
                  ),
 | 
			
		||||
                  const Gap(16),
 | 
			
		||||
                  IconButton.filled(
 | 
			
		||||
                    icon: _isPlaying ? const Icon(Symbols.pause) : const Icon(Symbols.play_arrow),
 | 
			
		||||
                    icon: _isPlaying
 | 
			
		||||
                        ? const Icon(Symbols.pause)
 | 
			
		||||
                        : const Icon(Symbols.play_arrow),
 | 
			
		||||
                    onPressed: () {
 | 
			
		||||
                      _audioPlayer!.playOrPause();
 | 
			
		||||
                    },
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@ class AttachmentList extends StatefulWidget {
 | 
			
		||||
  final double? minWidth;
 | 
			
		||||
  final double? maxWidth;
 | 
			
		||||
  final EdgeInsets? padding;
 | 
			
		||||
  final FilterQuality? filterQuality;
 | 
			
		||||
 | 
			
		||||
  const AttachmentList({
 | 
			
		||||
    super.key,
 | 
			
		||||
@@ -33,23 +34,27 @@ class AttachmentList extends StatefulWidget {
 | 
			
		||||
    this.minWidth,
 | 
			
		||||
    this.maxWidth,
 | 
			
		||||
    this.padding,
 | 
			
		||||
    this.filterQuality,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  static const BorderRadius kDefaultRadius = BorderRadius.all(Radius.circular(8));
 | 
			
		||||
  static const BorderRadius kDefaultRadius =
 | 
			
		||||
      BorderRadius.all(Radius.circular(8));
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<AttachmentList> createState() => _AttachmentListState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _AttachmentListState extends State<AttachmentList> {
 | 
			
		||||
  late final List<String> heroTags = List.generate(widget.data.length, (_) => const Uuid().v4());
 | 
			
		||||
  late final List<String> heroTags =
 | 
			
		||||
      List.generate(widget.data.length, (_) => const Uuid().v4());
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return LayoutBuilder(
 | 
			
		||||
      builder: (context, layoutConstraints) {
 | 
			
		||||
        final borderSide =
 | 
			
		||||
            widget.bordered ? BorderSide(width: 1, color: Theme.of(context).dividerColor) : BorderSide.none;
 | 
			
		||||
        final borderSide = widget.bordered
 | 
			
		||||
            ? BorderSide(width: 1, color: Theme.of(context).dividerColor)
 | 
			
		||||
            : BorderSide.none;
 | 
			
		||||
        final backgroundColor = Theme.of(context).colorScheme.surfaceContainer;
 | 
			
		||||
        final constraints = BoxConstraints(
 | 
			
		||||
          minWidth: widget.minWidth ?? 80,
 | 
			
		||||
@@ -58,13 +63,13 @@ class _AttachmentListState extends State<AttachmentList> {
 | 
			
		||||
 | 
			
		||||
        if (widget.data.isEmpty) return const SizedBox.shrink();
 | 
			
		||||
        if (widget.data.length == 1) {
 | 
			
		||||
          final singleAspectRatio =
 | 
			
		||||
              widget.data[0]?.data['ratio']?.toDouble() ??
 | 
			
		||||
          final singleAspectRatio = widget.data[0]?.data['ratio']?.toDouble() ??
 | 
			
		||||
              switch (widget.data[0]?.mimetype.split('/').firstOrNull) {
 | 
			
		||||
                'audio' => 16 / 9,
 | 
			
		||||
                'video' => 16 / 9,
 | 
			
		||||
                _ => 1,
 | 
			
		||||
              }.toDouble();
 | 
			
		||||
              }
 | 
			
		||||
                  .toDouble();
 | 
			
		||||
 | 
			
		||||
          return Container(
 | 
			
		||||
            padding: widget.padding ?? EdgeInsets.zero,
 | 
			
		||||
@@ -80,12 +85,18 @@ class _AttachmentListState extends State<AttachmentList> {
 | 
			
		||||
                  ),
 | 
			
		||||
                  child: ClipRRect(
 | 
			
		||||
                    borderRadius: AttachmentList.kDefaultRadius,
 | 
			
		||||
                    child: AttachmentItem(data: widget.data[0], heroTag: heroTags[0], fit: widget.fit),
 | 
			
		||||
                    child: AttachmentItem(
 | 
			
		||||
                      data: widget.data[0],
 | 
			
		||||
                      heroTag: heroTags[0],
 | 
			
		||||
                      fit: widget.fit,
 | 
			
		||||
                      filterQuality: widget.filterQuality,
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
              onTap: () {
 | 
			
		||||
                if (widget.data.firstOrNull?.mediaType != SnMediaType.image) return;
 | 
			
		||||
                if (widget.data.firstOrNull?.mediaType != SnMediaType.image)
 | 
			
		||||
                  return;
 | 
			
		||||
                context.pushTransparentRoute(
 | 
			
		||||
                  AttachmentZoomView(
 | 
			
		||||
                    data: widget.data.where((ele) => ele != null).cast(),
 | 
			
		||||
@@ -100,8 +111,10 @@ class _AttachmentListState extends State<AttachmentList> {
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        final fullOfImage =
 | 
			
		||||
            widget.data.where((ele) => ele?.mediaType == SnMediaType.image).length == widget.data.length;
 | 
			
		||||
        final fullOfImage = widget.data
 | 
			
		||||
                .where((ele) => ele?.mediaType == SnMediaType.image)
 | 
			
		||||
                .length ==
 | 
			
		||||
            widget.data.length;
 | 
			
		||||
 | 
			
		||||
        if (widget.gridded && fullOfImage) {
 | 
			
		||||
          return Container(
 | 
			
		||||
@@ -117,29 +130,38 @@ class _AttachmentListState extends State<AttachmentList> {
 | 
			
		||||
                crossAxisCount: math.min(widget.data.length, 2),
 | 
			
		||||
                crossAxisSpacing: 4,
 | 
			
		||||
                mainAxisSpacing: 4,
 | 
			
		||||
                children:
 | 
			
		||||
                    widget.data
 | 
			
		||||
                        .mapIndexed(
 | 
			
		||||
                          (idx, ele) => GestureDetector(
 | 
			
		||||
                            child: Container(
 | 
			
		||||
                              constraints: constraints,
 | 
			
		||||
                              child: AttachmentItem(data: ele, heroTag: heroTags[idx], fit: BoxFit.cover),
 | 
			
		||||
                            ),
 | 
			
		||||
                            onTap: () {
 | 
			
		||||
                              if (widget.data[idx]!.mediaType != SnMediaType.image) return;
 | 
			
		||||
                              context.pushTransparentRoute(
 | 
			
		||||
                                AttachmentZoomView(
 | 
			
		||||
                                  data: widget.data.where((ele) => ele != null).cast(),
 | 
			
		||||
                                  initialIndex: idx,
 | 
			
		||||
                                  heroTags: heroTags,
 | 
			
		||||
                                ),
 | 
			
		||||
                                backgroundColor: Colors.black.withOpacity(0.7),
 | 
			
		||||
                                rootNavigator: true,
 | 
			
		||||
                              );
 | 
			
		||||
                            },
 | 
			
		||||
                children: widget.data
 | 
			
		||||
                    .mapIndexed(
 | 
			
		||||
                      (idx, ele) => GestureDetector(
 | 
			
		||||
                        child: Container(
 | 
			
		||||
                          constraints: constraints,
 | 
			
		||||
                          child: AttachmentItem(
 | 
			
		||||
                            data: ele,
 | 
			
		||||
                            heroTag: heroTags[idx],
 | 
			
		||||
                            fit: BoxFit.cover,
 | 
			
		||||
                            filterQuality: widget.filterQuality,
 | 
			
		||||
                          ),
 | 
			
		||||
                        )
 | 
			
		||||
                        .toList(),
 | 
			
		||||
                        ),
 | 
			
		||||
                        onTap: () {
 | 
			
		||||
                          if (widget.data[idx]!.mediaType !=
 | 
			
		||||
                              SnMediaType.image) {
 | 
			
		||||
                            return;
 | 
			
		||||
                          }
 | 
			
		||||
                          context.pushTransparentRoute(
 | 
			
		||||
                            AttachmentZoomView(
 | 
			
		||||
                              data: widget.data
 | 
			
		||||
                                  .where((ele) => ele != null)
 | 
			
		||||
                                  .cast(),
 | 
			
		||||
                              initialIndex: idx,
 | 
			
		||||
                              heroTags: heroTags,
 | 
			
		||||
                            ),
 | 
			
		||||
                            backgroundColor: Colors.black.withOpacity(0.7),
 | 
			
		||||
                            rootNavigator: true,
 | 
			
		||||
                          );
 | 
			
		||||
                        },
 | 
			
		||||
                      ),
 | 
			
		||||
                    )
 | 
			
		||||
                    .toList(),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
@@ -156,22 +178,26 @@ class _AttachmentListState extends State<AttachmentList> {
 | 
			
		||||
            child: ClipRRect(
 | 
			
		||||
              borderRadius: AttachmentList.kDefaultRadius,
 | 
			
		||||
              child: Column(
 | 
			
		||||
                children:
 | 
			
		||||
                    widget.data
 | 
			
		||||
                        .mapIndexed(
 | 
			
		||||
                          (idx, ele) => GestureDetector(
 | 
			
		||||
                            child: AspectRatio(
 | 
			
		||||
                              aspectRatio: ele?.data['ratio']?.toDouble() ?? 1,
 | 
			
		||||
                              child: Container(
 | 
			
		||||
                                constraints: constraints,
 | 
			
		||||
                                child: AttachmentItem(data: ele, heroTag: heroTags[idx], fit: BoxFit.cover),
 | 
			
		||||
                              ),
 | 
			
		||||
                children: widget.data
 | 
			
		||||
                    .mapIndexed(
 | 
			
		||||
                      (idx, ele) => GestureDetector(
 | 
			
		||||
                        child: AspectRatio(
 | 
			
		||||
                          aspectRatio: ele?.data['ratio']?.toDouble() ?? 1,
 | 
			
		||||
                          child: Container(
 | 
			
		||||
                            constraints: constraints,
 | 
			
		||||
                            child: AttachmentItem(
 | 
			
		||||
                              data: ele,
 | 
			
		||||
                              heroTag: heroTags[idx],
 | 
			
		||||
                              fit: BoxFit.cover,
 | 
			
		||||
                              filterQuality: widget.filterQuality,
 | 
			
		||||
                            ),
 | 
			
		||||
                          ),
 | 
			
		||||
                        )
 | 
			
		||||
                        .expand((ele) => [ele, const Divider(height: 1)])
 | 
			
		||||
                        .toList()
 | 
			
		||||
                      ..removeLast(),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    )
 | 
			
		||||
                    .expand((ele) => [ele, const Divider(height: 1)])
 | 
			
		||||
                    .toList()
 | 
			
		||||
                  ..removeLast(),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
@@ -179,6 +205,7 @@ class _AttachmentListState extends State<AttachmentList> {
 | 
			
		||||
 | 
			
		||||
        return Container(
 | 
			
		||||
          constraints: BoxConstraints(maxHeight: constraints.maxHeight),
 | 
			
		||||
          width: double.infinity,
 | 
			
		||||
          child: AspectRatio(
 | 
			
		||||
            aspectRatio: widget.data[0]?.data['ratio']?.toDouble() ?? 1,
 | 
			
		||||
            child: ScrollConfiguration(
 | 
			
		||||
@@ -189,16 +216,22 @@ class _AttachmentListState extends State<AttachmentList> {
 | 
			
		||||
                itemCount: widget.data.length,
 | 
			
		||||
                itemBuilder: (context, idx) {
 | 
			
		||||
                  return Container(
 | 
			
		||||
                    constraints: constraints.copyWith(maxWidth: widget.maxWidth),
 | 
			
		||||
                    constraints:
 | 
			
		||||
                        constraints.copyWith(maxWidth: widget.maxWidth),
 | 
			
		||||
                    child: AspectRatio(
 | 
			
		||||
                      aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
 | 
			
		||||
                      aspectRatio:
 | 
			
		||||
                          (widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
 | 
			
		||||
                      child: GestureDetector(
 | 
			
		||||
                        onTap: () {
 | 
			
		||||
                          if (widget.data[idx]?.mediaType != SnMediaType.image) return;
 | 
			
		||||
                          if (widget.data[idx]?.mediaType != SnMediaType.image)
 | 
			
		||||
                            return;
 | 
			
		||||
                          context.pushTransparentRoute(
 | 
			
		||||
                            AttachmentZoomView(
 | 
			
		||||
                              data:
 | 
			
		||||
                                  widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(),
 | 
			
		||||
                              data: widget.data
 | 
			
		||||
                                  .where((ele) =>
 | 
			
		||||
                                      ele != null &&
 | 
			
		||||
                                      ele.mediaType == SnMediaType.image)
 | 
			
		||||
                                  .cast(),
 | 
			
		||||
                              initialIndex: idx,
 | 
			
		||||
                              heroTags: heroTags,
 | 
			
		||||
                            ),
 | 
			
		||||
@@ -212,18 +245,25 @@ class _AttachmentListState extends State<AttachmentList> {
 | 
			
		||||
                            Container(
 | 
			
		||||
                              decoration: BoxDecoration(
 | 
			
		||||
                                color: backgroundColor,
 | 
			
		||||
                                border: Border(top: borderSide, bottom: borderSide),
 | 
			
		||||
                                border:
 | 
			
		||||
                                    Border(top: borderSide, bottom: borderSide),
 | 
			
		||||
                                borderRadius: AttachmentList.kDefaultRadius,
 | 
			
		||||
                              ),
 | 
			
		||||
                              child: ClipRRect(
 | 
			
		||||
                                borderRadius: AttachmentList.kDefaultRadius,
 | 
			
		||||
                                child: AttachmentItem(data: widget.data[idx], heroTag: heroTags[idx]),
 | 
			
		||||
                                child: AttachmentItem(
 | 
			
		||||
                                  data: widget.data[idx],
 | 
			
		||||
                                  heroTag: heroTags[idx],
 | 
			
		||||
                                  filterQuality: widget.filterQuality,
 | 
			
		||||
                                ),
 | 
			
		||||
                              ),
 | 
			
		||||
                            ),
 | 
			
		||||
                            Positioned(
 | 
			
		||||
                              right: 8,
 | 
			
		||||
                              bottom: 8,
 | 
			
		||||
                              child: Chip(label: Text('${idx + 1}/${widget.data.length}')),
 | 
			
		||||
                              child: Chip(
 | 
			
		||||
                                  label:
 | 
			
		||||
                                      Text('${idx + 1}/${widget.data.length}')),
 | 
			
		||||
                            ),
 | 
			
		||||
                          ],
 | 
			
		||||
                        ),
 | 
			
		||||
@@ -245,5 +285,6 @@ class _AttachmentListState extends State<AttachmentList> {
 | 
			
		||||
 | 
			
		||||
class _AttachmentListScrollBehavior extends MaterialScrollBehavior {
 | 
			
		||||
  @override
 | 
			
		||||
  Set<PointerDeviceKind> get dragDevices => {PointerDeviceKind.touch, PointerDeviceKind.mouse};
 | 
			
		||||
  Set<PointerDeviceKind> get dragDevices =>
 | 
			
		||||
      {PointerDeviceKind.touch, PointerDeviceKind.mouse};
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
import 'dart:math' show max;
 | 
			
		||||
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:dismissible_page/dismissible_page.dart';
 | 
			
		||||
@@ -48,11 +47,14 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
 | 
			
		||||
  bool _showOverlay = true;
 | 
			
		||||
  bool _dismissable = true;
 | 
			
		||||
 | 
			
		||||
  int _page = 0;
 | 
			
		||||
 | 
			
		||||
  void _updatePage() {
 | 
			
		||||
    setState(() {
 | 
			
		||||
      if (_isCompletedDownload) {
 | 
			
		||||
        setState(() => _isCompletedDownload = false);
 | 
			
		||||
      }
 | 
			
		||||
      _page = _pageController.page?.round() ?? 0;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -155,7 +157,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
 | 
			
		||||
        Navigator.of(context).pop();
 | 
			
		||||
      },
 | 
			
		||||
      direction: _dismissable
 | 
			
		||||
          ? DismissiblePageDismissDirection.multi
 | 
			
		||||
          ? DismissiblePageDismissDirection.down
 | 
			
		||||
          : DismissiblePageDismissDirection.none,
 | 
			
		||||
      backgroundColor: Colors.transparent,
 | 
			
		||||
      isFullScreen: true,
 | 
			
		||||
@@ -222,31 +224,11 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
 | 
			
		||||
                      BoxDecoration(color: Colors.transparent),
 | 
			
		||||
                );
 | 
			
		||||
              }),
 | 
			
		||||
              Positioned(
 | 
			
		||||
                top: max(MediaQuery.of(context).padding.top, 8),
 | 
			
		||||
                left: 14,
 | 
			
		||||
                child: IgnorePointer(
 | 
			
		||||
                  ignoring: !_showOverlay,
 | 
			
		||||
                  child: IconButton(
 | 
			
		||||
                    constraints: const BoxConstraints(),
 | 
			
		||||
                    icon: const Icon(Icons.close),
 | 
			
		||||
                    style: ButtonStyle(
 | 
			
		||||
                      backgroundColor: MaterialStateProperty.all(
 | 
			
		||||
                        Theme.of(context).colorScheme.surface.withOpacity(0.5),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                    onPressed: () {
 | 
			
		||||
                      Navigator.of(context).pop();
 | 
			
		||||
                    },
 | 
			
		||||
                  ).opacity(_showOverlay ? 1 : 0, animate: true).animate(
 | 
			
		||||
                      const Duration(milliseconds: 300), Curves.easeInOut),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
              Align(
 | 
			
		||||
                alignment: Alignment.bottomCenter,
 | 
			
		||||
                child: IgnorePointer(
 | 
			
		||||
                  child: Container(
 | 
			
		||||
                    height: 300,
 | 
			
		||||
                    height: 200,
 | 
			
		||||
                    decoration: BoxDecoration(
 | 
			
		||||
                      gradient: LinearGradient(
 | 
			
		||||
                        begin: Alignment.bottomCenter,
 | 
			
		||||
@@ -269,153 +251,130 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
 | 
			
		||||
                child: Material(
 | 
			
		||||
                  color: Colors.transparent,
 | 
			
		||||
                  child: Builder(builder: (context) {
 | 
			
		||||
                    final ud = context.read<UserDirectoryProvider>();
 | 
			
		||||
                    final item = widget.data.elementAt(
 | 
			
		||||
                      widget.data.length > 1
 | 
			
		||||
                          ? _pageController.page?.round() ?? 0
 | 
			
		||||
                          : 0,
 | 
			
		||||
                    );
 | 
			
		||||
                    final account = ud.getFromCache(item.accountId);
 | 
			
		||||
 | 
			
		||||
                    return Column(
 | 
			
		||||
                      crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                    return Row(
 | 
			
		||||
                      children: [
 | 
			
		||||
                        if (item.accountId > 0)
 | 
			
		||||
                          Row(
 | 
			
		||||
                            children: [
 | 
			
		||||
                              IgnorePointer(
 | 
			
		||||
                                child: AccountImage(
 | 
			
		||||
                                  content: account?.avatar,
 | 
			
		||||
                                  radius: 19,
 | 
			
		||||
                                ),
 | 
			
		||||
                              ),
 | 
			
		||||
                              const Gap(8),
 | 
			
		||||
                              Expanded(
 | 
			
		||||
                                child: IgnorePointer(
 | 
			
		||||
                                  child: Column(
 | 
			
		||||
                                    crossAxisAlignment:
 | 
			
		||||
                                        CrossAxisAlignment.start,
 | 
			
		||||
                                    children: [
 | 
			
		||||
                                      Text(
 | 
			
		||||
                                        'attachmentUploadBy'.tr(),
 | 
			
		||||
                                        style: Theme.of(context)
 | 
			
		||||
                                            .textTheme
 | 
			
		||||
                                            .bodySmall,
 | 
			
		||||
                                      ),
 | 
			
		||||
                                      Text(
 | 
			
		||||
                                        account?.nick ?? 'unknown'.tr(),
 | 
			
		||||
                                        style: Theme.of(context)
 | 
			
		||||
                                            .textTheme
 | 
			
		||||
                                            .bodyMedium,
 | 
			
		||||
                                      ),
 | 
			
		||||
                                    ],
 | 
			
		||||
                                  ),
 | 
			
		||||
                                ),
 | 
			
		||||
                              ),
 | 
			
		||||
                              if (widget.data.length > 1)
 | 
			
		||||
                                IgnorePointer(
 | 
			
		||||
                                  child: Text(
 | 
			
		||||
                                    '${(_pageController.page?.round() ?? 0) + 1}/${widget.data.length}',
 | 
			
		||||
                                    style: GoogleFonts.robotoMono(fontSize: 13),
 | 
			
		||||
                                  ).padding(right: 8),
 | 
			
		||||
                                ),
 | 
			
		||||
                              InkWell(
 | 
			
		||||
                                borderRadius:
 | 
			
		||||
                                    const BorderRadius.all(Radius.circular(16)),
 | 
			
		||||
                                onTap: _isDownloading
 | 
			
		||||
                                    ? null
 | 
			
		||||
                                    : () => _saveToAlbum(widget.data.length > 1
 | 
			
		||||
                                        ? _pageController.page?.round() ?? 0
 | 
			
		||||
                                        : 0),
 | 
			
		||||
                                child: Container(
 | 
			
		||||
                                  padding: const EdgeInsets.all(6),
 | 
			
		||||
                                  child: !_isDownloading
 | 
			
		||||
                                      ? !_isCompletedDownload
 | 
			
		||||
                                          ? const Icon(Symbols.save_alt)
 | 
			
		||||
                                          : const Icon(Symbols.download_done)
 | 
			
		||||
                                      : SizedBox(
 | 
			
		||||
                                          width: 24,
 | 
			
		||||
                                          height: 24,
 | 
			
		||||
                                          child: CircularProgressIndicator(
 | 
			
		||||
                                            value: _progressOfDownload,
 | 
			
		||||
                                            strokeWidth: 3,
 | 
			
		||||
                                          ),
 | 
			
		||||
                                        ),
 | 
			
		||||
                                ),
 | 
			
		||||
                              ),
 | 
			
		||||
                            ],
 | 
			
		||||
                          ),
 | 
			
		||||
                        const Gap(4),
 | 
			
		||||
                        IgnorePointer(
 | 
			
		||||
                          child: Text(
 | 
			
		||||
                            item.alt,
 | 
			
		||||
                            maxLines: 2,
 | 
			
		||||
                            overflow: TextOverflow.ellipsis,
 | 
			
		||||
                            style: const TextStyle(
 | 
			
		||||
                              fontSize: 15,
 | 
			
		||||
                              fontWeight: FontWeight.w500,
 | 
			
		||||
                        IconButton(
 | 
			
		||||
                          iconSize: 18,
 | 
			
		||||
                          constraints: const BoxConstraints(),
 | 
			
		||||
                          icon: const Icon(Icons.close),
 | 
			
		||||
                          style: ButtonStyle(
 | 
			
		||||
                            backgroundColor: MaterialStateProperty.all(
 | 
			
		||||
                              Theme.of(context)
 | 
			
		||||
                                  .colorScheme
 | 
			
		||||
                                  .surface
 | 
			
		||||
                                  .withOpacity(0.5),
 | 
			
		||||
                            ),
 | 
			
		||||
                          ),
 | 
			
		||||
                          onPressed: () {
 | 
			
		||||
                            Navigator.of(context).pop();
 | 
			
		||||
                          },
 | 
			
		||||
                        ),
 | 
			
		||||
                        const Gap(2),
 | 
			
		||||
                        IgnorePointer(
 | 
			
		||||
                          child: Wrap(
 | 
			
		||||
                            spacing: 6,
 | 
			
		||||
                            children: [
 | 
			
		||||
                              if (item.metadata['exif'] == null)
 | 
			
		||||
                                Text(
 | 
			
		||||
                                  '#${item.rid}',
 | 
			
		||||
                                  style: metaTextStyle,
 | 
			
		||||
                                ),
 | 
			
		||||
                              if (item.metadata['exif']?['Model'] != null)
 | 
			
		||||
                                Text(
 | 
			
		||||
                                  'attachmentShotOn'.tr(args: [
 | 
			
		||||
                                    item.metadata['exif']?['Model'],
 | 
			
		||||
                                  ]),
 | 
			
		||||
                                  style: metaTextStyle,
 | 
			
		||||
                                ).padding(right: 2),
 | 
			
		||||
                              if (item.metadata['exif']?['Megapixels'] !=
 | 
			
		||||
                                      null &&
 | 
			
		||||
                                  item.metadata['exif']?['Model'] != null)
 | 
			
		||||
                                Text(
 | 
			
		||||
                                  '${item.metadata['exif']?['Megapixels']}MP',
 | 
			
		||||
                                  style: metaTextStyle,
 | 
			
		||||
                                )
 | 
			
		||||
                              else
 | 
			
		||||
                                Text(
 | 
			
		||||
                                  item.size.formatBytes(),
 | 
			
		||||
                                  style: metaTextStyle,
 | 
			
		||||
                                ),
 | 
			
		||||
                              if (item.metadata['width'] != null &&
 | 
			
		||||
                                  item.metadata['height'] != null)
 | 
			
		||||
                                Text(
 | 
			
		||||
                                  '${item.metadata['width']}x${item.metadata['height']}',
 | 
			
		||||
                                  style: metaTextStyle,
 | 
			
		||||
                                ),
 | 
			
		||||
                            ],
 | 
			
		||||
                        IconButton(
 | 
			
		||||
                            iconSize: 20,
 | 
			
		||||
                            constraints: const BoxConstraints(),
 | 
			
		||||
                            padding: EdgeInsets.zero,
 | 
			
		||||
                            visualDensity: VisualDensity.compact,
 | 
			
		||||
                            icon: const Icon(Symbols.hide).padding(all: 6),
 | 
			
		||||
                            onPressed: () {
 | 
			
		||||
                              setState(() => _showOverlay = false);
 | 
			
		||||
                            }),
 | 
			
		||||
                        Expanded(
 | 
			
		||||
                          child: IgnorePointer(
 | 
			
		||||
                            child: Builder(builder: (context) {
 | 
			
		||||
                              final item = widget.data.elementAt(_page);
 | 
			
		||||
                              final doShowCameraInfo =
 | 
			
		||||
                                  item.metadata['exif']?['Model'] != null;
 | 
			
		||||
                              final exif = item.metadata['exif'];
 | 
			
		||||
                              return Column(
 | 
			
		||||
                                children: [
 | 
			
		||||
                                  if (widget.data.length > 1)
 | 
			
		||||
                                    Text(
 | 
			
		||||
                                      '${_page + 1}/${widget.data.length}',
 | 
			
		||||
                                      style:
 | 
			
		||||
                                          GoogleFonts.robotoMono(fontSize: 13),
 | 
			
		||||
                                    ).padding(right: 8),
 | 
			
		||||
                                  if (doShowCameraInfo)
 | 
			
		||||
                                    Text(
 | 
			
		||||
                                      'attachmentShotOn'
 | 
			
		||||
                                          .tr(args: [exif?['Model']]),
 | 
			
		||||
                                      style: metaTextStyle,
 | 
			
		||||
                                      textAlign: TextAlign.center,
 | 
			
		||||
                                    ),
 | 
			
		||||
                                  if (doShowCameraInfo)
 | 
			
		||||
                                    Row(
 | 
			
		||||
                                      spacing: 4,
 | 
			
		||||
                                      mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                                      children: [
 | 
			
		||||
                                        if (exif?['Megapixels'] != null)
 | 
			
		||||
                                          Text(
 | 
			
		||||
                                            '${exif?['Megapixels']}MP',
 | 
			
		||||
                                            style: metaTextStyle,
 | 
			
		||||
                                          ),
 | 
			
		||||
                                        if (exif?['ISO'] != null)
 | 
			
		||||
                                          Text(
 | 
			
		||||
                                            'ISO${exif['ISO']}',
 | 
			
		||||
                                            style: metaTextStyle,
 | 
			
		||||
                                          ),
 | 
			
		||||
                                        if (exif?['FNumber'] != null)
 | 
			
		||||
                                          Text(
 | 
			
		||||
                                            'f/${exif['FNumber']}',
 | 
			
		||||
                                            style: metaTextStyle,
 | 
			
		||||
                                          ),
 | 
			
		||||
                                      ],
 | 
			
		||||
                                    )
 | 
			
		||||
                                ],
 | 
			
		||||
                              );
 | 
			
		||||
                            }),
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                        const Gap(4),
 | 
			
		||||
                        InkWell(
 | 
			
		||||
                          onTap: () {
 | 
			
		||||
                        IconButton(
 | 
			
		||||
                          constraints: const BoxConstraints(),
 | 
			
		||||
                          padding: EdgeInsets.zero,
 | 
			
		||||
                          visualDensity: VisualDensity.compact,
 | 
			
		||||
                          icon: Container(
 | 
			
		||||
                            padding: const EdgeInsets.all(6),
 | 
			
		||||
                            child: !_isDownloading
 | 
			
		||||
                                ? !_isCompletedDownload
 | 
			
		||||
                                    ? const Icon(Symbols.save_alt)
 | 
			
		||||
                                    : const Icon(Symbols.download_done)
 | 
			
		||||
                                : SizedBox(
 | 
			
		||||
                                    width: 20,
 | 
			
		||||
                                    height: 20,
 | 
			
		||||
                                    child: CircularProgressIndicator(
 | 
			
		||||
                                      backgroundColor: Theme.of(context)
 | 
			
		||||
                                          .colorScheme
 | 
			
		||||
                                          .surfaceContainerHighest,
 | 
			
		||||
                                      value: _progressOfDownload,
 | 
			
		||||
                                      strokeWidth: 3,
 | 
			
		||||
                                    ),
 | 
			
		||||
                                  ),
 | 
			
		||||
                          ),
 | 
			
		||||
                          onPressed:
 | 
			
		||||
                              _isDownloading ? null : () => _saveToAlbum(_page),
 | 
			
		||||
                        ),
 | 
			
		||||
                        IconButton(
 | 
			
		||||
                          iconSize: 18,
 | 
			
		||||
                          constraints: const BoxConstraints(),
 | 
			
		||||
                          icon: const Icon(Icons.info_outline),
 | 
			
		||||
                          style: ButtonStyle(
 | 
			
		||||
                            backgroundColor: MaterialStateProperty.all(
 | 
			
		||||
                              Theme.of(context)
 | 
			
		||||
                                  .colorScheme
 | 
			
		||||
                                  .surface
 | 
			
		||||
                                  .withOpacity(0.5),
 | 
			
		||||
                            ),
 | 
			
		||||
                          ),
 | 
			
		||||
                          onPressed: () {
 | 
			
		||||
                            _showDetail = true;
 | 
			
		||||
                            showModalBottomSheet(
 | 
			
		||||
                              context: context,
 | 
			
		||||
                              builder: (context) => _AttachmentZoomDetailPopup(
 | 
			
		||||
                                data: widget.data.elementAt(
 | 
			
		||||
                                    widget.data.length > 1
 | 
			
		||||
                                        ? _pageController.page?.round() ?? 0
 | 
			
		||||
                                        : 0),
 | 
			
		||||
                                data: widget.data.elementAt(_page),
 | 
			
		||||
                              ),
 | 
			
		||||
                            ).then((_) {
 | 
			
		||||
                              _showDetail = false;
 | 
			
		||||
                            });
 | 
			
		||||
                          },
 | 
			
		||||
                          child: Text(
 | 
			
		||||
                            'viewDetailedAttachment'.tr(),
 | 
			
		||||
                            style: metaTextStyle.copyWith(
 | 
			
		||||
                                decoration: TextDecoration.underline),
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ],
 | 
			
		||||
                    );
 | 
			
		||||
@@ -427,18 +386,20 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        onTap: () {
 | 
			
		||||
          if (_showOverlay) {
 | 
			
		||||
            Navigator.pop(context);
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
          setState(() => _showOverlay = !_showOverlay);
 | 
			
		||||
        },
 | 
			
		||||
        onVerticalDragUpdate: (details) {
 | 
			
		||||
          if (_showDetail) return;
 | 
			
		||||
          if (_showDetail || !_dismissable) return;
 | 
			
		||||
          if (details.delta.dy <= -20) {
 | 
			
		||||
            _showDetail = true;
 | 
			
		||||
            showModalBottomSheet(
 | 
			
		||||
              context: context,
 | 
			
		||||
              builder: (context) => _AttachmentZoomDetailPopup(
 | 
			
		||||
                data: widget.data.elementAt(widget.data.length > 1
 | 
			
		||||
                    ? _pageController.page?.round() ?? 0
 | 
			
		||||
                    : 0),
 | 
			
		||||
                data: widget.data.elementAt(_page),
 | 
			
		||||
              ),
 | 
			
		||||
            ).then((_) {
 | 
			
		||||
              _showDetail = false;
 | 
			
		||||
 
 | 
			
		||||
@@ -12,10 +12,10 @@ import 'package:surface/providers/config.dart';
 | 
			
		||||
import 'package:surface/providers/keypair.dart';
 | 
			
		||||
import 'package:surface/providers/user_directory.dart';
 | 
			
		||||
import 'package:surface/providers/userinfo.dart';
 | 
			
		||||
import 'package:surface/screens/account/profile_page.dart';
 | 
			
		||||
import 'package:surface/types/chat.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_popover.dart';
 | 
			
		||||
import 'package:surface/widgets/account/badge.dart';
 | 
			
		||||
import 'package:surface/widgets/attachment/attachment_list.dart';
 | 
			
		||||
import 'package:surface/widgets/context_menu.dart';
 | 
			
		||||
import 'package:surface/widgets/link_preview.dart';
 | 
			
		||||
@@ -109,18 +109,10 @@ class ChatMessage extends StatelessWidget {
 | 
			
		||||
                      child: AccountImage(
 | 
			
		||||
                        content: user?.avatar,
 | 
			
		||||
                        badge: (user?.badges.isNotEmpty ?? false)
 | 
			
		||||
                            ? Icon(
 | 
			
		||||
                                kBadgesMeta[user!.badges.first.type]?.$2 ??
 | 
			
		||||
                                    Symbols.question_mark,
 | 
			
		||||
                                color: kBadgesMeta[user.badges.first.type]?.$3,
 | 
			
		||||
                                fill: 1,
 | 
			
		||||
                                size: 18,
 | 
			
		||||
                                shadows: [
 | 
			
		||||
                                  Shadow(
 | 
			
		||||
                                      offset: Offset(1, 1),
 | 
			
		||||
                                      blurRadius: 5.0,
 | 
			
		||||
                                      color: Color.fromARGB(200, 0, 0, 0)),
 | 
			
		||||
                                ],
 | 
			
		||||
                            ? AccountBadge(
 | 
			
		||||
                                badge: user!.badges.first,
 | 
			
		||||
                                radius: 16,
 | 
			
		||||
                                padding: EdgeInsets.all(2),
 | 
			
		||||
                              )
 | 
			
		||||
                            : null,
 | 
			
		||||
                      ),
 | 
			
		||||
@@ -214,7 +206,8 @@ class ChatMessage extends StatelessWidget {
 | 
			
		||||
                data.type == 'messages.new' &&
 | 
			
		||||
                (data.body['text']?.isNotEmpty ?? false) &&
 | 
			
		||||
                (cfg.prefs.getBool(kAppExpandChatLink) ?? true))
 | 
			
		||||
              LinkPreviewWidget(text: data.body['text']!).padding(left: 48),
 | 
			
		||||
              LinkPreviewWidget(text: data.body['text']!)
 | 
			
		||||
                  .padding(left: isCompact ? 0 : 48),
 | 
			
		||||
            if (data.preload?.attachments?.isNotEmpty ?? false)
 | 
			
		||||
              AttachmentList(
 | 
			
		||||
                data: data.preload!.attachments!,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										103
									
								
								lib/widgets/menu_bar.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								lib/widgets/menu_bar.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,103 @@
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter/services.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:surface/providers/navigation.dart';
 | 
			
		||||
import 'package:surface/router.dart';
 | 
			
		||||
 | 
			
		||||
// https://api.flutter.dev/flutter/widgets/PlatformMenuBar-class.html
 | 
			
		||||
// All the code following is only works on macOS
 | 
			
		||||
class AppSystemMenuBar extends StatelessWidget {
 | 
			
		||||
  final Function? onQuit;
 | 
			
		||||
  final Widget child;
 | 
			
		||||
  const AppSystemMenuBar({super.key, this.onQuit, required this.child});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    if (kIsWeb || !Platform.isMacOS) return child;
 | 
			
		||||
 | 
			
		||||
    final nav = context.watch<NavigationProvider>();
 | 
			
		||||
 | 
			
		||||
    return PlatformMenuBar(
 | 
			
		||||
      menus: <PlatformMenuItem>[
 | 
			
		||||
        PlatformMenu(
 | 
			
		||||
          label: 'Solian',
 | 
			
		||||
          menus: <PlatformMenuItem>[
 | 
			
		||||
            PlatformMenuItemGroup(
 | 
			
		||||
              members: <PlatformMenuItem>[
 | 
			
		||||
                PlatformMenuItem(
 | 
			
		||||
                  label: 'screenAbout'.tr(),
 | 
			
		||||
                  onSelected: () {
 | 
			
		||||
                    appRouter.goNamed('about');
 | 
			
		||||
                    nav.autoDetectIndex(appRouter);
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
            PlatformMenuItemGroup(
 | 
			
		||||
              members: [
 | 
			
		||||
                PlatformMenuItem(
 | 
			
		||||
                  label: 'screenHome'.tr(),
 | 
			
		||||
                  shortcut: const SingleActivator(
 | 
			
		||||
                    LogicalKeyboardKey.digit1,
 | 
			
		||||
                    meta: true,
 | 
			
		||||
                  ),
 | 
			
		||||
                  onSelected: () {
 | 
			
		||||
                    appRouter.goNamed('home');
 | 
			
		||||
                    nav.autoDetectIndex(appRouter);
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                PlatformMenuItem(
 | 
			
		||||
                  label: 'screenExplore'.tr(),
 | 
			
		||||
                  shortcut: const SingleActivator(
 | 
			
		||||
                    LogicalKeyboardKey.digit2,
 | 
			
		||||
                    meta: true,
 | 
			
		||||
                  ),
 | 
			
		||||
                  onSelected: () {
 | 
			
		||||
                    appRouter.goNamed('explore');
 | 
			
		||||
                    nav.autoDetectIndex(appRouter);
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                PlatformMenuItem(
 | 
			
		||||
                  label: 'screenChat'.tr(),
 | 
			
		||||
                  shortcut: const SingleActivator(
 | 
			
		||||
                    LogicalKeyboardKey.digit3,
 | 
			
		||||
                    meta: true,
 | 
			
		||||
                  ),
 | 
			
		||||
                  onSelected: () {
 | 
			
		||||
                    appRouter.goNamed('chat');
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                PlatformMenuItem(
 | 
			
		||||
                  label: 'screenAccount'.tr(),
 | 
			
		||||
                  shortcut: const SingleActivator(
 | 
			
		||||
                    LogicalKeyboardKey.digit4,
 | 
			
		||||
                    meta: true,
 | 
			
		||||
                  ),
 | 
			
		||||
                  onSelected: () {
 | 
			
		||||
                    appRouter.goNamed('account');
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
            if (onQuit != null)
 | 
			
		||||
              PlatformMenuItem(
 | 
			
		||||
                shortcut: const SingleActivator(
 | 
			
		||||
                  LogicalKeyboardKey.keyQ,
 | 
			
		||||
                  meta: true,
 | 
			
		||||
                ),
 | 
			
		||||
                label: 'trayMenuExit'.tr(),
 | 
			
		||||
                onSelected: () {
 | 
			
		||||
                  onQuit?.call();
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
      child: child,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -29,6 +29,7 @@ import 'package:surface/types/attachment.dart';
 | 
			
		||||
import 'package:surface/types/post.dart';
 | 
			
		||||
import 'package:surface/types/reaction.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
import 'package:surface/widgets/account/badge.dart';
 | 
			
		||||
import 'package:surface/widgets/attachment/attachment_item.dart';
 | 
			
		||||
import 'package:surface/widgets/attachment/attachment_list.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
@@ -43,8 +44,6 @@ import 'package:surface/widgets/post/publisher_popover.dart';
 | 
			
		||||
import 'package:surface/widgets/universal_image.dart';
 | 
			
		||||
import 'package:xml/xml.dart';
 | 
			
		||||
 | 
			
		||||
import '../../screens/account/profile_page.dart' show kBadgesMeta;
 | 
			
		||||
 | 
			
		||||
class OpenablePostItem extends StatelessWidget {
 | 
			
		||||
  final SnPost data;
 | 
			
		||||
  final bool showReactions;
 | 
			
		||||
@@ -149,7 +148,6 @@ class PostItem extends StatelessWidget {
 | 
			
		||||
 | 
			
		||||
  void _doShareViaPicture(BuildContext context) async {
 | 
			
		||||
    final box = context.findRenderObject() as RenderBox?;
 | 
			
		||||
    context.showSnackbar('postSharingViaPicture'.tr());
 | 
			
		||||
 | 
			
		||||
    final controller = ScreenshotController();
 | 
			
		||||
    final capturedImage = await controller.captureFromLongWidget(
 | 
			
		||||
@@ -160,9 +158,9 @@ class PostItem extends StatelessWidget {
 | 
			
		||||
          child: Material(
 | 
			
		||||
            child: MultiProvider(
 | 
			
		||||
              providers: [
 | 
			
		||||
                // Create a copy of environments
 | 
			
		||||
                Provider<SnNetworkProvider>(create: (_) => context.read()),
 | 
			
		||||
                ChangeNotifierProvider<ConfigProvider>(
 | 
			
		||||
                    create: (_) => context.read()),
 | 
			
		||||
                Provider<UserDirectoryProvider>(create: (_) => context.read()),
 | 
			
		||||
              ],
 | 
			
		||||
              child: ResponsiveBreakpoints.builder(
 | 
			
		||||
                breakpoints: ResponsiveBreakpoints.of(context).breakpoints,
 | 
			
		||||
@@ -224,7 +222,7 @@ class PostItem extends StatelessWidget {
 | 
			
		||||
                  onShareImage: () => _doShareViaPicture(context),
 | 
			
		||||
                  onSelectAnswer: onSelectAnswer,
 | 
			
		||||
                  onDeleted: () {
 | 
			
		||||
                    if (onDeleted != null) {}
 | 
			
		||||
                    onDeleted?.call();
 | 
			
		||||
                  },
 | 
			
		||||
                ).padding(bottom: 8),
 | 
			
		||||
                if (data.preload?.video != null)
 | 
			
		||||
@@ -273,7 +271,7 @@ class PostItem extends StatelessWidget {
 | 
			
		||||
              onShareImage: () => _doShareViaPicture(context),
 | 
			
		||||
              onSelectAnswer: onSelectAnswer,
 | 
			
		||||
              onDeleted: () {
 | 
			
		||||
                if (onDeleted != null) {}
 | 
			
		||||
                onDeleted?.call();
 | 
			
		||||
              },
 | 
			
		||||
            ).padding(horizontal: 12, top: 8, bottom: 8),
 | 
			
		||||
            if (data.preload?.video != null)
 | 
			
		||||
@@ -364,7 +362,7 @@ class PostItem extends StatelessWidget {
 | 
			
		||||
                onShareImage: () => _doShareViaPicture(context),
 | 
			
		||||
                onSelectAnswer: onSelectAnswer,
 | 
			
		||||
                onDeleted: () {
 | 
			
		||||
                  if (onDeleted != null) onDeleted!();
 | 
			
		||||
                  onDeleted?.call();
 | 
			
		||||
                },
 | 
			
		||||
              ).padding(horizontal: 12, vertical: 8),
 | 
			
		||||
              if (data.preload?.video != null)
 | 
			
		||||
@@ -507,6 +505,8 @@ class PostShareImageWidget extends StatelessWidget {
 | 
			
		||||
            StyledWidget(AttachmentList(
 | 
			
		||||
              data: data.preload!.attachments!,
 | 
			
		||||
              columned: true,
 | 
			
		||||
              fit: BoxFit.contain,
 | 
			
		||||
              filterQuality: FilterQuality.high,
 | 
			
		||||
            )).padding(horizontal: 16, bottom: 8),
 | 
			
		||||
          Column(
 | 
			
		||||
            crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
@@ -886,7 +886,7 @@ class _PostContentHeader extends StatelessWidget {
 | 
			
		||||
        'publisherId': data.publisherId,
 | 
			
		||||
      });
 | 
			
		||||
      if (!context.mounted) return;
 | 
			
		||||
      context.showSnackbar('postDeleted'.tr(args: ['#${data.id}']));
 | 
			
		||||
      onDeleted.call();
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!context.mounted) return;
 | 
			
		||||
      context.showErrorDialog(err);
 | 
			
		||||
@@ -922,27 +922,39 @@ class _PostContentHeader extends StatelessWidget {
 | 
			
		||||
    return Row(
 | 
			
		||||
      children: [
 | 
			
		||||
        GestureDetector(
 | 
			
		||||
          child: AccountImage(
 | 
			
		||||
            content: data.publisher.avatar,
 | 
			
		||||
            radius: isCompact ? 12 : 20,
 | 
			
		||||
            borderRadius: data.publisher.type == 1 ? (isCompact ? 4 : 8) : 20,
 | 
			
		||||
            badge: (user?.badges.isNotEmpty ?? false)
 | 
			
		||||
                ? Icon(
 | 
			
		||||
                    kBadgesMeta[user!.badges.first.type]?.$2 ??
 | 
			
		||||
                        Symbols.question_mark,
 | 
			
		||||
                    color: kBadgesMeta[user.badges.first.type]?.$3,
 | 
			
		||||
                    fill: 1,
 | 
			
		||||
                    size: 18,
 | 
			
		||||
                    shadows: [
 | 
			
		||||
                      Shadow(
 | 
			
		||||
                        offset: Offset(1, 1),
 | 
			
		||||
                        blurRadius: 5.0,
 | 
			
		||||
                        color: Color.fromARGB(200, 0, 0, 0),
 | 
			
		||||
          child: data.preload?.realm == null
 | 
			
		||||
              ? AccountImage(
 | 
			
		||||
                  content: data.publisher.avatar,
 | 
			
		||||
                  radius: isCompact ? 12 : 20,
 | 
			
		||||
                  borderRadius:
 | 
			
		||||
                      data.publisher.type == 1 ? (isCompact ? 4 : 8) : 20,
 | 
			
		||||
                  badge: (user?.badges.isNotEmpty ?? false)
 | 
			
		||||
                      ? AccountBadge(
 | 
			
		||||
                          badge: user!.badges.first,
 | 
			
		||||
                          radius: 16,
 | 
			
		||||
                          padding: EdgeInsets.all(2),
 | 
			
		||||
                        )
 | 
			
		||||
                      : null,
 | 
			
		||||
                )
 | 
			
		||||
              : AccountImage(
 | 
			
		||||
                  content: data.preload!.realm!.avatar,
 | 
			
		||||
                  radius: isCompact ? 12 : 20,
 | 
			
		||||
                  borderRadius: isCompact ? 4 : 8,
 | 
			
		||||
                  badgeOffset: Offset(-6, -4),
 | 
			
		||||
                  badge: Container(
 | 
			
		||||
                    decoration: BoxDecoration(
 | 
			
		||||
                      border: Border.all(
 | 
			
		||||
                        color: Theme.of(context).colorScheme.surface,
 | 
			
		||||
                        width: 2,
 | 
			
		||||
                      ),
 | 
			
		||||
                    ],
 | 
			
		||||
                  )
 | 
			
		||||
                : null,
 | 
			
		||||
          ),
 | 
			
		||||
                      borderRadius: BorderRadius.circular(10),
 | 
			
		||||
                    ),
 | 
			
		||||
                    child: AccountImage(
 | 
			
		||||
                      content: data.publisher.avatar,
 | 
			
		||||
                      radius: 10,
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
          onTap: () {
 | 
			
		||||
            showPopover(
 | 
			
		||||
              backgroundColor: Theme.of(context).colorScheme.surface,
 | 
			
		||||
@@ -987,7 +999,17 @@ class _PostContentHeader extends StatelessWidget {
 | 
			
		||||
            child: Column(
 | 
			
		||||
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
              children: [
 | 
			
		||||
                Text(data.publisher.nick).bold(),
 | 
			
		||||
                Row(
 | 
			
		||||
                  children: [
 | 
			
		||||
                    Text(data.publisher.nick).bold(),
 | 
			
		||||
                    if (data.preload?.realm != null)
 | 
			
		||||
                      const Icon(Symbols.arrow_right, size: 16)
 | 
			
		||||
                          .padding(horizontal: 2)
 | 
			
		||||
                          .opacity(0.5),
 | 
			
		||||
                    if (data.preload?.realm != null)
 | 
			
		||||
                      Text(data.preload!.realm!.name),
 | 
			
		||||
                  ],
 | 
			
		||||
                ),
 | 
			
		||||
                Row(
 | 
			
		||||
                  children: [
 | 
			
		||||
                    Text('@${data.publisher.name}').fontSize(13),
 | 
			
		||||
@@ -1037,8 +1059,10 @@ class _PostContentHeader extends StatelessWidget {
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    GoRouter.of(context).pushNamed(
 | 
			
		||||
                      'postEditor',
 | 
			
		||||
                      pathParameters: {'mode': data.typePlural},
 | 
			
		||||
                      queryParameters: {'editing': data.id.toString()},
 | 
			
		||||
                      queryParameters: {
 | 
			
		||||
                        'editing': data.id.toString(),
 | 
			
		||||
                        'mode': data.typePlural,
 | 
			
		||||
                      },
 | 
			
		||||
                    );
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
@@ -1065,8 +1089,10 @@ class _PostContentHeader extends StatelessWidget {
 | 
			
		||||
                onTap: () {
 | 
			
		||||
                  GoRouter.of(context).pushNamed(
 | 
			
		||||
                    'postEditor',
 | 
			
		||||
                    pathParameters: {'mode': 'stories'},
 | 
			
		||||
                    queryParameters: {'replying': data.id.toString()},
 | 
			
		||||
                    queryParameters: {
 | 
			
		||||
                      'replying': data.id.toString(),
 | 
			
		||||
                      'mode': data.typePlural,
 | 
			
		||||
                    },
 | 
			
		||||
                  );
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
@@ -1081,8 +1107,10 @@ class _PostContentHeader extends StatelessWidget {
 | 
			
		||||
                onTap: () {
 | 
			
		||||
                  GoRouter.of(context).pushNamed(
 | 
			
		||||
                    'postEditor',
 | 
			
		||||
                    pathParameters: {'mode': 'stories'},
 | 
			
		||||
                    queryParameters: {'reposting': data.id.toString()},
 | 
			
		||||
                    queryParameters: {
 | 
			
		||||
                      'reposting': data.id.toString(),
 | 
			
		||||
                      'mode': 'stories',
 | 
			
		||||
                    },
 | 
			
		||||
                  );
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,8 @@ class PostMiniEditor extends StatefulWidget {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _PostMiniEditorState extends State<PostMiniEditor> {
 | 
			
		||||
  final PostWriteController _writeController = PostWriteController(doLoadFromTemporary: false);
 | 
			
		||||
  final PostWriteController _writeController =
 | 
			
		||||
      PostWriteController(doLoadFromTemporary: false);
 | 
			
		||||
 | 
			
		||||
  bool _isFetching = false;
 | 
			
		||||
 | 
			
		||||
@@ -44,8 +45,9 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
 | 
			
		||||
        resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
 | 
			
		||||
      );
 | 
			
		||||
      final beforeId = config.prefs.getInt('int_last_publisher_id');
 | 
			
		||||
      _writeController
 | 
			
		||||
          .setPublisher(_publishers?.where((ele) => ele.id == beforeId).firstOrNull ?? _publishers?.firstOrNull);
 | 
			
		||||
      _writeController.setPublisher(
 | 
			
		||||
          _publishers?.where((ele) => ele.id == beforeId).firstOrNull ??
 | 
			
		||||
              _publishers?.firstOrNull);
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showErrorDialog(err);
 | 
			
		||||
@@ -99,11 +101,17 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
 | 
			
		||||
                                Expanded(
 | 
			
		||||
                                  child: Column(
 | 
			
		||||
                                    mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                                    crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                                    crossAxisAlignment:
 | 
			
		||||
                                        CrossAxisAlignment.start,
 | 
			
		||||
                                    children: [
 | 
			
		||||
                                      Text(item.nick).textStyle(Theme.of(context).textTheme.bodyMedium!),
 | 
			
		||||
                                      Text(item.nick).textStyle(
 | 
			
		||||
                                          Theme.of(context)
 | 
			
		||||
                                              .textTheme
 | 
			
		||||
                                              .bodyMedium!),
 | 
			
		||||
                                      Text('@${item.name}')
 | 
			
		||||
                                          .textStyle(Theme.of(context).textTheme.bodySmall!)
 | 
			
		||||
                                          .textStyle(Theme.of(context)
 | 
			
		||||
                                              .textTheme
 | 
			
		||||
                                              .bodySmall!)
 | 
			
		||||
                                          .fontSize(12),
 | 
			
		||||
                                    ],
 | 
			
		||||
                                  ),
 | 
			
		||||
@@ -120,7 +128,8 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
 | 
			
		||||
                          CircleAvatar(
 | 
			
		||||
                            radius: 16,
 | 
			
		||||
                            backgroundColor: Colors.transparent,
 | 
			
		||||
                            foregroundColor: Theme.of(context).colorScheme.onSurface,
 | 
			
		||||
                            foregroundColor:
 | 
			
		||||
                                Theme.of(context).colorScheme.onSurface,
 | 
			
		||||
                            child: const Icon(Symbols.add),
 | 
			
		||||
                          ),
 | 
			
		||||
                          const Gap(8),
 | 
			
		||||
@@ -129,7 +138,8 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
 | 
			
		||||
                              mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                              crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                              children: [
 | 
			
		||||
                                Text('publishersNew').tr().textStyle(Theme.of(context).textTheme.bodyMedium!),
 | 
			
		||||
                                Text('publishersNew').tr().textStyle(
 | 
			
		||||
                                    Theme.of(context).textTheme.bodyMedium!),
 | 
			
		||||
                              ],
 | 
			
		||||
                            ),
 | 
			
		||||
                          ),
 | 
			
		||||
@@ -140,7 +150,9 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
 | 
			
		||||
                  value: _writeController.publisher,
 | 
			
		||||
                  onChanged: (SnPublisher? value) {
 | 
			
		||||
                    if (value == null) {
 | 
			
		||||
                      GoRouter.of(context).pushNamed('accountPublisherNew').then((value) {
 | 
			
		||||
                      GoRouter.of(context)
 | 
			
		||||
                          .pushNamed('accountPublisherNew')
 | 
			
		||||
                          .then((value) {
 | 
			
		||||
                        if (value == true) {
 | 
			
		||||
                          _publishers = null;
 | 
			
		||||
                          _fetchPublishers();
 | 
			
		||||
@@ -176,7 +188,8 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
 | 
			
		||||
                    ),
 | 
			
		||||
                    border: InputBorder.none,
 | 
			
		||||
                  ),
 | 
			
		||||
                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                  onTapOutside: (_) =>
 | 
			
		||||
                      FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
              const Gap(8),
 | 
			
		||||
@@ -185,7 +198,8 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
 | 
			
		||||
                TweenAnimationBuilder<double>(
 | 
			
		||||
                  tween: Tween(begin: 0, end: _writeController.progress),
 | 
			
		||||
                  duration: Duration(milliseconds: 300),
 | 
			
		||||
                  builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2),
 | 
			
		||||
                  builder: (context, value, _) =>
 | 
			
		||||
                      LinearProgressIndicator(value: value, minHeight: 2),
 | 
			
		||||
                )
 | 
			
		||||
              else if (_writeController.isBusy)
 | 
			
		||||
                const LinearProgressIndicator(value: null, minHeight: 2),
 | 
			
		||||
@@ -200,15 +214,17 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
 | 
			
		||||
                    onPressed: () {
 | 
			
		||||
                      GoRouter.of(context).pushNamed(
 | 
			
		||||
                        'postEditor',
 | 
			
		||||
                        pathParameters: {'mode': 'stories'},
 | 
			
		||||
                        queryParameters: {
 | 
			
		||||
                          if (widget.postReplyId != null) 'replying': widget.postReplyId.toString(),
 | 
			
		||||
                          if (widget.postReplyId != null)
 | 
			
		||||
                            'replying': widget.postReplyId.toString(),
 | 
			
		||||
                          'mode': 'stories',
 | 
			
		||||
                        },
 | 
			
		||||
                      );
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
                  TextButton.icon(
 | 
			
		||||
                    onPressed: (_writeController.isBusy || _writeController.publisher == null)
 | 
			
		||||
                    onPressed: (_writeController.isBusy ||
 | 
			
		||||
                            _writeController.publisher == null)
 | 
			
		||||
                        ? null
 | 
			
		||||
                        : () {
 | 
			
		||||
                            _writeController.sendPost(context).then((_) {
 | 
			
		||||
 
 | 
			
		||||
@@ -9,10 +9,9 @@ import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/providers/user_directory.dart';
 | 
			
		||||
import 'package:surface/types/post.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
import 'package:surface/widgets/account/badge.dart';
 | 
			
		||||
import 'package:surface/widgets/universal_image.dart';
 | 
			
		||||
 | 
			
		||||
import '../../screens/account/profile_page.dart' show kBadgesMeta;
 | 
			
		||||
 | 
			
		||||
class PublisherPopoverCard extends StatelessWidget {
 | 
			
		||||
  final SnPublisher data;
 | 
			
		||||
 | 
			
		||||
@@ -76,39 +75,22 @@ class PublisherPopoverCard extends StatelessWidget {
 | 
			
		||||
            const Gap(8)
 | 
			
		||||
          ],
 | 
			
		||||
        ).padding(horizontal: 16),
 | 
			
		||||
        if (user != null && user.badges.isNotEmpty) const Gap(16),
 | 
			
		||||
        if (user != null && user.badges.isNotEmpty)
 | 
			
		||||
          Wrap(
 | 
			
		||||
            spacing: 4,
 | 
			
		||||
            children: user.badges
 | 
			
		||||
                .map(
 | 
			
		||||
                  (ele) => Tooltip(
 | 
			
		||||
                    richMessage: TextSpan(
 | 
			
		||||
                      children: [
 | 
			
		||||
                        TextSpan(
 | 
			
		||||
                            text: kBadgesMeta[ele.type]?.$1.tr() ??
 | 
			
		||||
                                'unknown'.tr()),
 | 
			
		||||
                        if (ele.metadata['title'] != null)
 | 
			
		||||
                          TextSpan(
 | 
			
		||||
                            text: '\n${ele.metadata['title']}',
 | 
			
		||||
                            style: const TextStyle(fontWeight: FontWeight.bold),
 | 
			
		||||
                          ),
 | 
			
		||||
                        TextSpan(text: '\n'),
 | 
			
		||||
                        TextSpan(
 | 
			
		||||
                          text: DateFormat.yMEd().format(ele.createdAt),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ],
 | 
			
		||||
                    ),
 | 
			
		||||
                    child: Icon(
 | 
			
		||||
                      kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark,
 | 
			
		||||
                      color: kBadgesMeta[ele.type]?.$3,
 | 
			
		||||
                      fill: 1,
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                  (ele) => AccountBadge(badge: ele),
 | 
			
		||||
                )
 | 
			
		||||
                .toList(),
 | 
			
		||||
          ).padding(horizontal: 24),
 | 
			
		||||
          ).padding(horizontal: 24, top: 16),
 | 
			
		||||
        const Gap(16),
 | 
			
		||||
        if (data.description.isNotEmpty)
 | 
			
		||||
          Text(
 | 
			
		||||
            data.description,
 | 
			
		||||
            maxLines: 2,
 | 
			
		||||
            overflow: TextOverflow.ellipsis,
 | 
			
		||||
          ).padding(horizontal: 26, bottom: 20),
 | 
			
		||||
        Row(
 | 
			
		||||
          children: [
 | 
			
		||||
            Expanded(
 | 
			
		||||
 
 | 
			
		||||
@@ -34,11 +34,14 @@ class UniversalImage extends StatelessWidget {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
 | 
			
		||||
    final double? resizeHeight = cacheHeight != null ? (cacheHeight! * devicePixelRatio) : null;
 | 
			
		||||
    final double? resizeWidth = cacheWidth != null ? (cacheWidth! * devicePixelRatio) : null;
 | 
			
		||||
    final double? resizeHeight =
 | 
			
		||||
        cacheHeight != null ? (cacheHeight! * devicePixelRatio) : null;
 | 
			
		||||
    final double? resizeWidth =
 | 
			
		||||
        cacheWidth != null ? (cacheWidth! * devicePixelRatio) : null;
 | 
			
		||||
 | 
			
		||||
    return Image(
 | 
			
		||||
      filterQuality: filterQuality ?? context.read<ConfigProvider>().imageQuality,
 | 
			
		||||
      filterQuality:
 | 
			
		||||
          filterQuality ?? context.read<ConfigProvider>().imageQuality,
 | 
			
		||||
      image: kIsWeb
 | 
			
		||||
          ? UniversalImage.provider(url)
 | 
			
		||||
          : ResizeImage(
 | 
			
		||||
@@ -52,7 +55,8 @@ class UniversalImage extends StatelessWidget {
 | 
			
		||||
      fit: fit,
 | 
			
		||||
      loadingBuilder: noProgressIndicator
 | 
			
		||||
          ? null
 | 
			
		||||
          : (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
 | 
			
		||||
          : (BuildContext context, Widget child,
 | 
			
		||||
              ImageChunkEvent? loadingProgress) {
 | 
			
		||||
              if (loadingProgress == null) return child;
 | 
			
		||||
              return Container(
 | 
			
		||||
                constraints: BoxConstraints(maxHeight: 80),
 | 
			
		||||
@@ -61,12 +65,15 @@ class UniversalImage extends StatelessWidget {
 | 
			
		||||
                    tween: Tween(
 | 
			
		||||
                      begin: 0,
 | 
			
		||||
                      end: loadingProgress.expectedTotalBytes != null
 | 
			
		||||
                          ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
 | 
			
		||||
                          ? loadingProgress.cumulativeBytesLoaded /
 | 
			
		||||
                              loadingProgress.expectedTotalBytes!
 | 
			
		||||
                          : 0,
 | 
			
		||||
                    ),
 | 
			
		||||
                    duration: const Duration(milliseconds: 300),
 | 
			
		||||
                    builder: (context, value, _) => CircularProgressIndicator(
 | 
			
		||||
                      value: loadingProgress.expectedTotalBytes != null ? value.toDouble() : null,
 | 
			
		||||
                      value: loadingProgress.expectedTotalBytes != null
 | 
			
		||||
                          ? value.toDouble()
 | 
			
		||||
                          : null,
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
@@ -114,6 +121,7 @@ class AutoResizeUniversalImage extends StatelessWidget {
 | 
			
		||||
  final BoxFit? fit;
 | 
			
		||||
  final bool noProgressIndicator;
 | 
			
		||||
  final bool noErrorWidget;
 | 
			
		||||
  final FilterQuality? filterQuality;
 | 
			
		||||
 | 
			
		||||
  const AutoResizeUniversalImage(
 | 
			
		||||
    this.url, {
 | 
			
		||||
@@ -123,6 +131,7 @@ class AutoResizeUniversalImage extends StatelessWidget {
 | 
			
		||||
    this.fit,
 | 
			
		||||
    this.noProgressIndicator = false,
 | 
			
		||||
    this.noErrorWidget = false,
 | 
			
		||||
    this.filterQuality,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@@ -137,6 +146,7 @@ class AutoResizeUniversalImage extends StatelessWidget {
 | 
			
		||||
        noErrorWidget: noErrorWidget,
 | 
			
		||||
        cacheHeight: constraints.maxHeight,
 | 
			
		||||
        cacheWidth: constraints.maxWidth,
 | 
			
		||||
        filterQuality: filterQuality,
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										56
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										56
									
								
								pubspec.lock
									
									
									
									
									
								
							@@ -173,10 +173,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: built_value
 | 
			
		||||
      sha256: "8b158ab94ec6913e480dc3f752418348b5ae099eb75868b5f4775f0572999c61"
 | 
			
		||||
      sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "8.9.4"
 | 
			
		||||
    version: "8.9.5"
 | 
			
		||||
  cached_network_image:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -301,10 +301,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: croppy
 | 
			
		||||
      sha256: bf99b00023df0d7d047e04d27d496d87cbefd968f578d0bd30f342ff75570a12
 | 
			
		||||
      sha256: "99f4fbb4a4b44d2712e8dcd61c57c1acac151bd53cab11de3babec80407ed266"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.3.3"
 | 
			
		||||
    version: "1.3.5"
 | 
			
		||||
  cross_file:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -429,18 +429,18 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: drift
 | 
			
		||||
      sha256: "97d5832657d49f26e7a8e07de397ddc63790b039372878d5117af816d0fdb5cb"
 | 
			
		||||
      sha256: "14a61af39d4584faf1d73b5b35e4b758a43008cf4c0fdb0576ec8e7032c0d9a5"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.25.1"
 | 
			
		||||
    version: "2.26.0"
 | 
			
		||||
  drift_dev:
 | 
			
		||||
    dependency: "direct dev"
 | 
			
		||||
    description:
 | 
			
		||||
      name: drift_dev
 | 
			
		||||
      sha256: f1db88482dbb016b9bbddddf746d5d0a6938b156ff20e07320052981f97388cc
 | 
			
		||||
      sha256: "0d3f8b33b76cf1c6a82ee34d9511c40957549c4674b8f1688609e6d6c7306588"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.25.2"
 | 
			
		||||
    version: "2.26.0"
 | 
			
		||||
  drift_flutter:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -710,6 +710,14 @@ packages:
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.4.1"
 | 
			
		||||
  flutter_card_swiper:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_card_swiper
 | 
			
		||||
      sha256: "1eacbfab31b572223042e03409726553aec431abe48af48c8d591d376d070d3d"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "7.0.2"
 | 
			
		||||
  flutter_colorpicker:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -831,10 +839,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_map
 | 
			
		||||
      sha256: bbf145e8220531f2f727608c431871c7457f3b134e513543913afd00fdc1cd47
 | 
			
		||||
      sha256: f7d0379477274f323c3f3bc12d369a2b42eb86d1e7bd2970ae1ea3cff782449a
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "8.1.0"
 | 
			
		||||
    version: "8.1.1"
 | 
			
		||||
  flutter_markdown:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -1177,10 +1185,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: image_picker_android
 | 
			
		||||
      sha256: "82652a75e3dd667a91187769a6a2cc81bd8c111bbead698d8e938d2b63e5e89a"
 | 
			
		||||
      sha256: "8bd392ba8b0c8957a157ae0dc9fcf48c58e6c20908d5880aea1d79734df090e9"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.8.12+21"
 | 
			
		||||
    version: "0.8.12+22"
 | 
			
		||||
  image_picker_for_web:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -1409,10 +1417,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: material_symbols_icons
 | 
			
		||||
      sha256: "1403944c2a68dbdf934fca2b2115a372e70a4a185c136ab71a9d16e2108d0b14"
 | 
			
		||||
      sha256: ca30ccbd97763353bde6bb1076aa4f4d17a40db0804384da77df142102aa225d
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "4.2805.1"
 | 
			
		||||
    version: "4.2808.0"
 | 
			
		||||
  media_kit:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -1601,10 +1609,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: path_provider_android
 | 
			
		||||
      sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2"
 | 
			
		||||
      sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.2.15"
 | 
			
		||||
    version: "2.2.16"
 | 
			
		||||
  path_provider_foundation:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -1785,10 +1793,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: pub_semver
 | 
			
		||||
      sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd"
 | 
			
		||||
      sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.1.5"
 | 
			
		||||
    version: "2.2.0"
 | 
			
		||||
  pubspec_parse:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -1953,10 +1961,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: shared_preferences_android
 | 
			
		||||
      sha256: a768fc8ede5f0c8e6150476e14f38e2417c0864ca36bb4582be8e21925a03c22
 | 
			
		||||
      sha256: "3ec7210872c4ba945e3244982918e502fa2bfb5230dff6832459ca0e1879b7ad"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.4.6"
 | 
			
		||||
    version: "2.4.8"
 | 
			
		||||
  shared_preferences_foundation:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -2118,10 +2126,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: sqlite3
 | 
			
		||||
      sha256: "32b632dda27d664f85520093ed6f735ae5c49b5b75345afb8b19411bc59bb53d"
 | 
			
		||||
      sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.7.4"
 | 
			
		||||
    version: "2.7.5"
 | 
			
		||||
  sqlite3_flutter_libs:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -2326,10 +2334,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: url_launcher_android
 | 
			
		||||
      sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193"
 | 
			
		||||
      sha256: "1d0eae19bd7606ef60fe69ef3b312a437a16549476c42321d5dc1506c9ca3bf4"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "6.3.14"
 | 
			
		||||
    version: "6.3.15"
 | 
			
		||||
  url_launcher_ios:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										23
									
								
								pubspec.yaml
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								pubspec.yaml
									
									
									
									
									
								
							@@ -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
 | 
			
		||||
# 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.
 | 
			
		||||
version: 2.4.2+76
 | 
			
		||||
version: 2.4.2+79
 | 
			
		||||
 | 
			
		||||
environment:
 | 
			
		||||
  sdk: ^3.5.4
 | 
			
		||||
@@ -138,6 +138,7 @@ dependencies:
 | 
			
		||||
  flutter_map: ^8.1.0
 | 
			
		||||
  geolocator: ^13.0.2
 | 
			
		||||
  fast_rsa: ^3.8.0
 | 
			
		||||
  flutter_card_swiper: ^7.0.2
 | 
			
		||||
 | 
			
		||||
dev_dependencies:
 | 
			
		||||
  flutter_test:
 | 
			
		||||
@@ -187,18 +188,14 @@ flutter:
 | 
			
		||||
  # "family" key with the font family name, and a "fonts" key with a
 | 
			
		||||
  # list giving the asset and other descriptors for the font. For
 | 
			
		||||
  # example:
 | 
			
		||||
  # fonts:
 | 
			
		||||
  #   - family: Schyler
 | 
			
		||||
  #     fonts:
 | 
			
		||||
  #       - asset: fonts/Schyler-Regular.ttf
 | 
			
		||||
  #       - asset: fonts/Schyler-Italic.ttf
 | 
			
		||||
  #         style: italic
 | 
			
		||||
  #   - family: Trajan Pro
 | 
			
		||||
  #     fonts:
 | 
			
		||||
  #       - asset: fonts/TrajanPro.ttf
 | 
			
		||||
  #       - asset: fonts/TrajanPro_Bold.ttf
 | 
			
		||||
  #         weight: 700
 | 
			
		||||
  #
 | 
			
		||||
  fonts:
 | 
			
		||||
    - family: Nunito
 | 
			
		||||
      fonts:
 | 
			
		||||
        - asset: assets/fonts/Nunito-Regular.ttf
 | 
			
		||||
        - asset: assets/fonts/Nunito-Bold.ttf
 | 
			
		||||
          weight: 700
 | 
			
		||||
        - asset: assets/fonts/Nunito-Italic.ttf
 | 
			
		||||
          style: italic
 | 
			
		||||
  # For details regarding fonts from package dependencies,
 | 
			
		||||
  # see https://flutter.dev/to/font-from-package
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,192 +0,0 @@
 | 
			
		||||
 | 
			
		||||
var CanvasKitInit = (() => {
 | 
			
		||||
  var _scriptName = import.meta.url;
 | 
			
		||||
  
 | 
			
		||||
  return (
 | 
			
		||||
function(moduleArg = {}) {
 | 
			
		||||
  var moduleRtn;
 | 
			
		||||
 | 
			
		||||
var r=moduleArg,aa,ca,da=new Promise((a,b)=>{aa=a;ca=b}),ea="object"==typeof window,ha="function"==typeof importScripts;
 | 
			
		||||
(function(a){a.ce=a.ce||[];a.ce.push(function(){a.MakeSWCanvasSurface=function(b){var c=b,e="undefined"!==typeof OffscreenCanvas&&c instanceof OffscreenCanvas;if(!("undefined"!==typeof HTMLCanvasElement&&c instanceof HTMLCanvasElement||e||(c=document.getElementById(b),c)))throw"Canvas with id "+b+" was not found";if(b=a.MakeSurface(c.width,c.height))b.Ae=c;return b};a.MakeCanvasSurface||(a.MakeCanvasSurface=a.MakeSWCanvasSurface);a.MakeSurface=function(b,c){var e={width:b,height:c,colorType:a.ColorType.RGBA_8888,
 | 
			
		||||
alphaType:a.AlphaType.Unpremul,colorSpace:a.ColorSpace.SRGB},f=b*c*4,k=a._malloc(f);if(e=a.Surface._makeRasterDirect(e,k,4*b))e.Ae=null,e.$e=b,e.Xe=c,e.Ye=f,e.He=k,e.getCanvas().clear(a.TRANSPARENT);return e};a.MakeRasterDirectSurface=function(b,c,e){return a.Surface._makeRasterDirect(b,c.byteOffset,e)};a.Surface.prototype.flush=function(b){a.$d(this.Zd);this._flush();if(this.Ae){var c=new Uint8ClampedArray(a.HEAPU8.buffer,this.He,this.Ye);c=new ImageData(c,this.$e,this.Xe);b?this.Ae.getContext("2d").putImageData(c,
 | 
			
		||||
0,0,b[0],b[1],b[2]-b[0],b[3]-b[1]):this.Ae.getContext("2d").putImageData(c,0,0)}};a.Surface.prototype.dispose=function(){this.He&&a._free(this.He);this.delete()};a.$d=a.$d||function(){};a.Be=a.Be||function(){return null}})})(r);
 | 
			
		||||
(function(a){a.ce=a.ce||[];a.ce.push(function(){function b(l,p,v){return l&&l.hasOwnProperty(p)?l[p]:v}function c(l){var p=ja(ka);ka[p]=l;return p}function e(l){return l.naturalHeight||l.videoHeight||l.displayHeight||l.height}function f(l){return l.naturalWidth||l.videoWidth||l.displayWidth||l.width}function k(l,p,v,w){l.bindTexture(l.TEXTURE_2D,p);w||v.alphaType!==a.AlphaType.Premul||l.pixelStorei(l.UNPACK_PREMULTIPLY_ALPHA_WEBGL,!0);return p}function n(l,p,v){v||p.alphaType!==a.AlphaType.Premul||
 | 
			
		||||
l.pixelStorei(l.UNPACK_PREMULTIPLY_ALPHA_WEBGL,!1);l.bindTexture(l.TEXTURE_2D,null)}a.GetWebGLContext=function(l,p){if(!l)throw"null canvas passed into makeWebGLContext";var v={alpha:b(p,"alpha",1),depth:b(p,"depth",1),stencil:b(p,"stencil",8),antialias:b(p,"antialias",0),premultipliedAlpha:b(p,"premultipliedAlpha",1),preserveDrawingBuffer:b(p,"preserveDrawingBuffer",0),preferLowPowerToHighPerformance:b(p,"preferLowPowerToHighPerformance",0),failIfMajorPerformanceCaveat:b(p,"failIfMajorPerformanceCaveat",
 | 
			
		||||
0),enableExtensionsByDefault:b(p,"enableExtensionsByDefault",1),explicitSwapControl:b(p,"explicitSwapControl",0),renderViaOffscreenBackBuffer:b(p,"renderViaOffscreenBackBuffer",0)};v.majorVersion=p&&p.majorVersion?p.majorVersion:"undefined"!==typeof WebGL2RenderingContext?2:1;if(v.explicitSwapControl)throw"explicitSwapControl is not supported";l=la(l,v);if(!l)return 0;oa(l);z.le.getExtension("WEBGL_debug_renderer_info");return l};a.deleteContext=function(l){z===pa[l]&&(z=null);"object"==typeof JSEvents&&
 | 
			
		||||
JSEvents.Af(pa[l].le.canvas);pa[l]&&pa[l].le.canvas&&(pa[l].le.canvas.Ve=void 0);pa[l]=null};a._setTextureCleanup({deleteTexture:function(l,p){var v=ka[p];v&&pa[l].le.deleteTexture(v);ka[p]=null}});a.MakeWebGLContext=function(l){if(!this.$d(l))return null;var p=this._MakeGrContext();if(!p)return null;p.Zd=l;var v=p.delete.bind(p);p["delete"]=function(){a.$d(this.Zd);v()}.bind(p);return z.Je=p};a.MakeGrContext=a.MakeWebGLContext;a.GrDirectContext.prototype.getResourceCacheLimitBytes=function(){a.$d(this.Zd);
 | 
			
		||||
this._getResourceCacheLimitBytes()};a.GrDirectContext.prototype.getResourceCacheUsageBytes=function(){a.$d(this.Zd);this._getResourceCacheUsageBytes()};a.GrDirectContext.prototype.releaseResourcesAndAbandonContext=function(){a.$d(this.Zd);this._releaseResourcesAndAbandonContext()};a.GrDirectContext.prototype.setResourceCacheLimitBytes=function(l){a.$d(this.Zd);this._setResourceCacheLimitBytes(l)};a.MakeOnScreenGLSurface=function(l,p,v,w,A,D){if(!this.$d(l.Zd))return null;p=void 0===A||void 0===D?
 | 
			
		||||
this._MakeOnScreenGLSurface(l,p,v,w):this._MakeOnScreenGLSurface(l,p,v,w,A,D);if(!p)return null;p.Zd=l.Zd;return p};a.MakeRenderTarget=function(){var l=arguments[0];if(!this.$d(l.Zd))return null;if(3===arguments.length){var p=this._MakeRenderTargetWH(l,arguments[1],arguments[2]);if(!p)return null}else if(2===arguments.length){if(p=this._MakeRenderTargetII(l,arguments[1]),!p)return null}else return null;p.Zd=l.Zd;return p};a.MakeWebGLCanvasSurface=function(l,p,v){p=p||null;var w=l,A="undefined"!==
 | 
			
		||||
typeof OffscreenCanvas&&w instanceof OffscreenCanvas;if(!("undefined"!==typeof HTMLCanvasElement&&w instanceof HTMLCanvasElement||A||(w=document.getElementById(l),w)))throw"Canvas with id "+l+" was not found";l=this.GetWebGLContext(w,v);if(!l||0>l)throw"failed to create webgl context: err "+l;l=this.MakeWebGLContext(l);p=this.MakeOnScreenGLSurface(l,w.width,w.height,p);return p?p:(p=w.cloneNode(!0),w.parentNode.replaceChild(p,w),p.classList.add("ck-replaced"),a.MakeSWCanvasSurface(p))};a.MakeCanvasSurface=
 | 
			
		||||
a.MakeWebGLCanvasSurface;a.Surface.prototype.makeImageFromTexture=function(l,p){a.$d(this.Zd);l=c(l);if(p=this._makeImageFromTexture(this.Zd,l,p))p.ue=l;return p};a.Surface.prototype.makeImageFromTextureSource=function(l,p,v){p||={height:e(l),width:f(l),colorType:a.ColorType.RGBA_8888,alphaType:v?a.AlphaType.Premul:a.AlphaType.Unpremul};p.colorSpace||(p.colorSpace=a.ColorSpace.SRGB);a.$d(this.Zd);var w=z.le;v=k(w,w.createTexture(),p,v);2===z.version?w.texImage2D(w.TEXTURE_2D,0,w.RGBA,p.width,p.height,
 | 
			
		||||
0,w.RGBA,w.UNSIGNED_BYTE,l):w.texImage2D(w.TEXTURE_2D,0,w.RGBA,w.RGBA,w.UNSIGNED_BYTE,l);n(w,p);this._resetContext();return this.makeImageFromTexture(v,p)};a.Surface.prototype.updateTextureFromSource=function(l,p,v){if(l.ue){a.$d(this.Zd);var w=l.getImageInfo(),A=z.le,D=k(A,ka[l.ue],w,v);2===z.version?A.texImage2D(A.TEXTURE_2D,0,A.RGBA,f(p),e(p),0,A.RGBA,A.UNSIGNED_BYTE,p):A.texImage2D(A.TEXTURE_2D,0,A.RGBA,A.RGBA,A.UNSIGNED_BYTE,p);n(A,w,v);this._resetContext();ka[l.ue]=null;l.ue=c(D);w.colorSpace=
 | 
			
		||||
l.getColorSpace();p=this._makeImageFromTexture(this.Zd,l.ue,w);v=l.Yd.ae;A=l.Yd.ee;l.Yd.ae=p.Yd.ae;l.Yd.ee=p.Yd.ee;p.Yd.ae=v;p.Yd.ee=A;p.delete();w.colorSpace.delete()}};a.MakeLazyImageFromTextureSource=function(l,p,v){p||={height:e(l),width:f(l),colorType:a.ColorType.RGBA_8888,alphaType:v?a.AlphaType.Premul:a.AlphaType.Unpremul};p.colorSpace||(p.colorSpace=a.ColorSpace.SRGB);var w={makeTexture:function(){var A=z,D=A.le,I=k(D,D.createTexture(),p,v);2===A.version?D.texImage2D(D.TEXTURE_2D,0,D.RGBA,
 | 
			
		||||
p.width,p.height,0,D.RGBA,D.UNSIGNED_BYTE,l):D.texImage2D(D.TEXTURE_2D,0,D.RGBA,D.RGBA,D.UNSIGNED_BYTE,l);n(D,p,v);return c(I)},freeSrc:function(){}};"VideoFrame"===l.constructor.name&&(w.freeSrc=function(){l.close()});return a.Image._makeFromGenerator(p,w)};a.$d=function(l){return l?oa(l):!1};a.Be=function(){return z&&z.Je&&!z.Je.isDeleted()?z.Je:null}})})(r);
 | 
			
		||||
(function(a){function b(g){return(f(255*g[3])<<24|f(255*g[0])<<16|f(255*g[1])<<8|f(255*g[2])<<0)>>>0}function c(g){if(g&&g._ck)return g;if(g instanceof Float32Array){for(var d=Math.floor(g.length/4),h=new Uint32Array(d),m=0;m<d;m++)h[m]=b(g.slice(4*m,4*(m+1)));return h}if(g instanceof Uint32Array)return g;if(g instanceof Array&&g[0]instanceof Float32Array)return g.map(b)}function e(g){if(void 0===g)return 1;var d=parseFloat(g);return g&&-1!==g.indexOf("%")?d/100:d}function f(g){return Math.round(Math.max(0,
 | 
			
		||||
Math.min(g||0,255)))}function k(g,d){d&&d._ck||a._free(g)}function n(g,d,h){if(!g||!g.length)return K;if(g&&g._ck)return g.byteOffset;var m=a[d].BYTES_PER_ELEMENT;h||=a._malloc(g.length*m);a[d].set(g,h/m);return h}function l(g){var d={he:K,count:g.length,colorType:a.ColorType.RGBA_F32};if(g instanceof Float32Array)d.he=n(g,"HEAPF32"),d.count=g.length/4;else if(g instanceof Uint32Array)d.he=n(g,"HEAPU32"),d.colorType=a.ColorType.RGBA_8888;else if(g instanceof Array){if(g&&g.length){for(var h=a._malloc(16*
 | 
			
		||||
g.length),m=0,t=h/4,u=0;u<g.length;u++)for(var x=0;4>x;x++)a.HEAPF32[t+m]=g[u][x],m++;g=h}else g=K;d.he=g}else throw"Invalid argument to copyFlexibleColorArray, Not a color array "+typeof g;return d}function p(g){if(!g)return K;var d=ba.toTypedArray();if(g.length){if(6===g.length||9===g.length)return n(g,"HEAPF32",P),6===g.length&&a.HEAPF32.set(Wc,6+P/4),P;if(16===g.length)return d[0]=g[0],d[1]=g[1],d[2]=g[3],d[3]=g[4],d[4]=g[5],d[5]=g[7],d[6]=g[12],d[7]=g[13],d[8]=g[15],P;throw"invalid matrix size";
 | 
			
		||||
}if(void 0===g.m11)throw"invalid matrix argument";d[0]=g.m11;d[1]=g.m21;d[2]=g.m41;d[3]=g.m12;d[4]=g.m22;d[5]=g.m42;d[6]=g.m14;d[7]=g.m24;d[8]=g.m44;return P}function v(g){if(!g)return K;var d=Y.toTypedArray();if(g.length){if(16!==g.length&&6!==g.length&&9!==g.length)throw"invalid matrix size";if(16===g.length)return n(g,"HEAPF32",ma);d.fill(0);d[0]=g[0];d[1]=g[1];d[3]=g[2];d[4]=g[3];d[5]=g[4];d[7]=g[5];d[10]=1;d[12]=g[6];d[13]=g[7];d[15]=g[8];6===g.length&&(d[12]=0,d[13]=0,d[15]=1);return ma}if(void 0===
 | 
			
		||||
g.m11)throw"invalid matrix argument";d[0]=g.m11;d[1]=g.m21;d[2]=g.m31;d[3]=g.m41;d[4]=g.m12;d[5]=g.m22;d[6]=g.m32;d[7]=g.m42;d[8]=g.m13;d[9]=g.m23;d[10]=g.m33;d[11]=g.m43;d[12]=g.m14;d[13]=g.m24;d[14]=g.m34;d[15]=g.m44;return ma}function w(g,d){return n(g,"HEAPF32",d||ia)}function A(g,d,h,m){var t=Ea.toTypedArray();t[0]=g;t[1]=d;t[2]=h;t[3]=m;return ia}function D(g){for(var d=new Float32Array(4),h=0;4>h;h++)d[h]=a.HEAPF32[g/4+h];return d}function I(g,d){return n(g,"HEAPF32",d||W)}function R(g,d){return n(g,
 | 
			
		||||
"HEAPF32",d||ub)}a.Color=function(g,d,h,m){void 0===m&&(m=1);return a.Color4f(f(g)/255,f(d)/255,f(h)/255,m)};a.ColorAsInt=function(g,d,h,m){void 0===m&&(m=255);return(f(m)<<24|f(g)<<16|f(d)<<8|f(h)<<0&268435455)>>>0};a.Color4f=function(g,d,h,m){void 0===m&&(m=1);return Float32Array.of(g,d,h,m)};Object.defineProperty(a,"TRANSPARENT",{get:function(){return a.Color4f(0,0,0,0)}});Object.defineProperty(a,"BLACK",{get:function(){return a.Color4f(0,0,0,1)}});Object.defineProperty(a,"WHITE",{get:function(){return a.Color4f(1,
 | 
			
		||||
1,1,1)}});Object.defineProperty(a,"RED",{get:function(){return a.Color4f(1,0,0,1)}});Object.defineProperty(a,"GREEN",{get:function(){return a.Color4f(0,1,0,1)}});Object.defineProperty(a,"BLUE",{get:function(){return a.Color4f(0,0,1,1)}});Object.defineProperty(a,"YELLOW",{get:function(){return a.Color4f(1,1,0,1)}});Object.defineProperty(a,"CYAN",{get:function(){return a.Color4f(0,1,1,1)}});Object.defineProperty(a,"MAGENTA",{get:function(){return a.Color4f(1,0,1,1)}});a.getColorComponents=function(g){return[Math.floor(255*
 | 
			
		||||
g[0]),Math.floor(255*g[1]),Math.floor(255*g[2]),g[3]]};a.parseColorString=function(g,d){g=g.toLowerCase();if(g.startsWith("#")){d=255;switch(g.length){case 9:d=parseInt(g.slice(7,9),16);case 7:var h=parseInt(g.slice(1,3),16);var m=parseInt(g.slice(3,5),16);var t=parseInt(g.slice(5,7),16);break;case 5:d=17*parseInt(g.slice(4,5),16);case 4:h=17*parseInt(g.slice(1,2),16),m=17*parseInt(g.slice(2,3),16),t=17*parseInt(g.slice(3,4),16)}return a.Color(h,m,t,d/255)}return g.startsWith("rgba")?(g=g.slice(5,
 | 
			
		||||
-1),g=g.split(","),a.Color(+g[0],+g[1],+g[2],e(g[3]))):g.startsWith("rgb")?(g=g.slice(4,-1),g=g.split(","),a.Color(+g[0],+g[1],+g[2],e(g[3]))):g.startsWith("gray(")||g.startsWith("hsl")||!d||(g=d[g],void 0===g)?a.BLACK:g};a.multiplyByAlpha=function(g,d){g=g.slice();g[3]=Math.max(0,Math.min(g[3]*d,1));return g};a.Malloc=function(g,d){var h=a._malloc(d*g.BYTES_PER_ELEMENT);return{_ck:!0,length:d,byteOffset:h,qe:null,subarray:function(m,t){m=this.toTypedArray().subarray(m,t);m._ck=!0;return m},toTypedArray:function(){if(this.qe&&
 | 
			
		||||
this.qe.length)return this.qe;this.qe=new g(a.HEAPU8.buffer,h,d);this.qe._ck=!0;return this.qe}}};a.Free=function(g){a._free(g.byteOffset);g.byteOffset=K;g.toTypedArray=null;g.qe=null};var P=K,ba,ma=K,Y,ia=K,Ea,fa,W=K,Ub,Ba=K,Vb,vb=K,Wb,wb=K,$a,Na=K,Xb,ub=K,Yb,Zb=K,Wc=Float32Array.of(0,0,1),K=0;a.onRuntimeInitialized=function(){function g(d,h,m,t,u,x,C){x||(x=4*t.width,t.colorType===a.ColorType.RGBA_F16?x*=2:t.colorType===a.ColorType.RGBA_F32&&(x*=4));var G=x*t.height;var F=u?u.byteOffset:a._malloc(G);
 | 
			
		||||
if(C?!d._readPixels(t,F,x,h,m,C):!d._readPixels(t,F,x,h,m))return u||a._free(F),null;if(u)return u.toTypedArray();switch(t.colorType){case a.ColorType.RGBA_8888:case a.ColorType.RGBA_F16:d=(new Uint8Array(a.HEAPU8.buffer,F,G)).slice();break;case a.ColorType.RGBA_F32:d=(new Float32Array(a.HEAPU8.buffer,F,G)).slice();break;default:return null}a._free(F);return d}Ea=a.Malloc(Float32Array,4);ia=Ea.byteOffset;Y=a.Malloc(Float32Array,16);ma=Y.byteOffset;ba=a.Malloc(Float32Array,9);P=ba.byteOffset;Xb=a.Malloc(Float32Array,
 | 
			
		||||
12);ub=Xb.byteOffset;Yb=a.Malloc(Float32Array,12);Zb=Yb.byteOffset;fa=a.Malloc(Float32Array,4);W=fa.byteOffset;Ub=a.Malloc(Float32Array,4);Ba=Ub.byteOffset;Vb=a.Malloc(Float32Array,3);vb=Vb.byteOffset;Wb=a.Malloc(Float32Array,3);wb=Wb.byteOffset;$a=a.Malloc(Int32Array,4);Na=$a.byteOffset;a.ColorSpace.SRGB=a.ColorSpace._MakeSRGB();a.ColorSpace.DISPLAY_P3=a.ColorSpace._MakeDisplayP3();a.ColorSpace.ADOBE_RGB=a.ColorSpace._MakeAdobeRGB();a.GlyphRunFlags={IsWhiteSpace:a._GlyphRunFlags_isWhiteSpace};a.Path.MakeFromCmds=
 | 
			
		||||
function(d){var h=n(d,"HEAPF32"),m=a.Path._MakeFromCmds(h,d.length);k(h,d);return m};a.Path.MakeFromVerbsPointsWeights=function(d,h,m){var t=n(d,"HEAPU8"),u=n(h,"HEAPF32"),x=n(m,"HEAPF32"),C=a.Path._MakeFromVerbsPointsWeights(t,d.length,u,h.length,x,m&&m.length||0);k(t,d);k(u,h);k(x,m);return C};a.Path.prototype.addArc=function(d,h,m){d=I(d);this._addArc(d,h,m);return this};a.Path.prototype.addCircle=function(d,h,m,t){this._addCircle(d,h,m,!!t);return this};a.Path.prototype.addOval=function(d,h,m){void 0===
 | 
			
		||||
m&&(m=1);d=I(d);this._addOval(d,!!h,m);return this};a.Path.prototype.addPath=function(){var d=Array.prototype.slice.call(arguments),h=d[0],m=!1;"boolean"===typeof d[d.length-1]&&(m=d.pop());if(1===d.length)this._addPath(h,1,0,0,0,1,0,0,0,1,m);else if(2===d.length)d=d[1],this._addPath(h,d[0],d[1],d[2],d[3],d[4],d[5],d[6]||0,d[7]||0,d[8]||1,m);else if(7===d.length||10===d.length)this._addPath(h,d[1],d[2],d[3],d[4],d[5],d[6],d[7]||0,d[8]||0,d[9]||1,m);else return null;return this};a.Path.prototype.addPoly=
 | 
			
		||||
function(d,h){var m=n(d,"HEAPF32");this._addPoly(m,d.length/2,h);k(m,d);return this};a.Path.prototype.addRect=function(d,h){d=I(d);this._addRect(d,!!h);return this};a.Path.prototype.addRRect=function(d,h){d=R(d);this._addRRect(d,!!h);return this};a.Path.prototype.addVerbsPointsWeights=function(d,h,m){var t=n(d,"HEAPU8"),u=n(h,"HEAPF32"),x=n(m,"HEAPF32");this._addVerbsPointsWeights(t,d.length,u,h.length,x,m&&m.length||0);k(t,d);k(u,h);k(x,m)};a.Path.prototype.arc=function(d,h,m,t,u,x){d=a.LTRBRect(d-
 | 
			
		||||
m,h-m,d+m,h+m);u=(u-t)/Math.PI*180-360*!!x;x=new a.Path;x.addArc(d,t/Math.PI*180,u);this.addPath(x,!0);x.delete();return this};a.Path.prototype.arcToOval=function(d,h,m,t){d=I(d);this._arcToOval(d,h,m,t);return this};a.Path.prototype.arcToRotated=function(d,h,m,t,u,x,C){this._arcToRotated(d,h,m,!!t,!!u,x,C);return this};a.Path.prototype.arcToTangent=function(d,h,m,t,u){this._arcToTangent(d,h,m,t,u);return this};a.Path.prototype.close=function(){this._close();return this};a.Path.prototype.conicTo=
 | 
			
		||||
function(d,h,m,t,u){this._conicTo(d,h,m,t,u);return this};a.Path.prototype.computeTightBounds=function(d){this._computeTightBounds(W);var h=fa.toTypedArray();return d?(d.set(h),d):h.slice()};a.Path.prototype.cubicTo=function(d,h,m,t,u,x){this._cubicTo(d,h,m,t,u,x);return this};a.Path.prototype.dash=function(d,h,m){return this._dash(d,h,m)?this:null};a.Path.prototype.getBounds=function(d){this._getBounds(W);var h=fa.toTypedArray();return d?(d.set(h),d):h.slice()};a.Path.prototype.lineTo=function(d,
 | 
			
		||||
h){this._lineTo(d,h);return this};a.Path.prototype.moveTo=function(d,h){this._moveTo(d,h);return this};a.Path.prototype.offset=function(d,h){this._transform(1,0,d,0,1,h,0,0,1);return this};a.Path.prototype.quadTo=function(d,h,m,t){this._quadTo(d,h,m,t);return this};a.Path.prototype.rArcTo=function(d,h,m,t,u,x,C){this._rArcTo(d,h,m,t,u,x,C);return this};a.Path.prototype.rConicTo=function(d,h,m,t,u){this._rConicTo(d,h,m,t,u);return this};a.Path.prototype.rCubicTo=function(d,h,m,t,u,x){this._rCubicTo(d,
 | 
			
		||||
h,m,t,u,x);return this};a.Path.prototype.rLineTo=function(d,h){this._rLineTo(d,h);return this};a.Path.prototype.rMoveTo=function(d,h){this._rMoveTo(d,h);return this};a.Path.prototype.rQuadTo=function(d,h,m,t){this._rQuadTo(d,h,m,t);return this};a.Path.prototype.stroke=function(d){d=d||{};d.width=d.width||1;d.miter_limit=d.miter_limit||4;d.cap=d.cap||a.StrokeCap.Butt;d.join=d.join||a.StrokeJoin.Miter;d.precision=d.precision||1;return this._stroke(d)?this:null};a.Path.prototype.transform=function(){if(1===
 | 
			
		||||
arguments.length){var d=arguments[0];this._transform(d[0],d[1],d[2],d[3],d[4],d[5],d[6]||0,d[7]||0,d[8]||1)}else if(6===arguments.length||9===arguments.length)d=arguments,this._transform(d[0],d[1],d[2],d[3],d[4],d[5],d[6]||0,d[7]||0,d[8]||1);else throw"transform expected to take 1 or 9 arguments. Got "+arguments.length;return this};a.Path.prototype.trim=function(d,h,m){return this._trim(d,h,!!m)?this:null};a.Image.prototype.encodeToBytes=function(d,h){var m=a.Be();d=d||a.ImageFormat.PNG;h=h||100;
 | 
			
		||||
return m?this._encodeToBytes(d,h,m):this._encodeToBytes(d,h)};a.Image.prototype.makeShaderCubic=function(d,h,m,t,u){u=p(u);return this._makeShaderCubic(d,h,m,t,u)};a.Image.prototype.makeShaderOptions=function(d,h,m,t,u){u=p(u);return this._makeShaderOptions(d,h,m,t,u)};a.Image.prototype.readPixels=function(d,h,m,t,u){var x=a.Be();return g(this,d,h,m,t,u,x)};a.Canvas.prototype.clear=function(d){a.$d(this.Zd);d=w(d);this._clear(d)};a.Canvas.prototype.clipRRect=function(d,h,m){a.$d(this.Zd);d=R(d);this._clipRRect(d,
 | 
			
		||||
h,m)};a.Canvas.prototype.clipRect=function(d,h,m){a.$d(this.Zd);d=I(d);this._clipRect(d,h,m)};a.Canvas.prototype.concat=function(d){a.$d(this.Zd);d=v(d);this._concat(d)};a.Canvas.prototype.drawArc=function(d,h,m,t,u){a.$d(this.Zd);d=I(d);this._drawArc(d,h,m,t,u)};a.Canvas.prototype.drawAtlas=function(d,h,m,t,u,x,C){if(d&&t&&h&&m&&h.length===m.length){a.$d(this.Zd);u||(u=a.BlendMode.SrcOver);var G=n(h,"HEAPF32"),F=n(m,"HEAPF32"),T=m.length/4,U=n(c(x),"HEAPU32");if(C&&"B"in C&&"C"in C)this._drawAtlasCubic(d,
 | 
			
		||||
F,G,U,T,u,C.B,C.C,t);else{let q=a.FilterMode.Linear,y=a.MipmapMode.None;C&&(q=C.filter,"mipmap"in C&&(y=C.mipmap));this._drawAtlasOptions(d,F,G,U,T,u,q,y,t)}k(G,h);k(F,m);k(U,x)}};a.Canvas.prototype.drawCircle=function(d,h,m,t){a.$d(this.Zd);this._drawCircle(d,h,m,t)};a.Canvas.prototype.drawColor=function(d,h){a.$d(this.Zd);d=w(d);void 0!==h?this._drawColor(d,h):this._drawColor(d)};a.Canvas.prototype.drawColorInt=function(d,h){a.$d(this.Zd);this._drawColorInt(d,h||a.BlendMode.SrcOver)};a.Canvas.prototype.drawColorComponents=
 | 
			
		||||
function(d,h,m,t,u){a.$d(this.Zd);d=A(d,h,m,t);void 0!==u?this._drawColor(d,u):this._drawColor(d)};a.Canvas.prototype.drawDRRect=function(d,h,m){a.$d(this.Zd);d=R(d,ub);h=R(h,Zb);this._drawDRRect(d,h,m)};a.Canvas.prototype.drawImage=function(d,h,m,t){a.$d(this.Zd);this._drawImage(d,h,m,t||null)};a.Canvas.prototype.drawImageCubic=function(d,h,m,t,u,x){a.$d(this.Zd);this._drawImageCubic(d,h,m,t,u,x||null)};a.Canvas.prototype.drawImageOptions=function(d,h,m,t,u,x){a.$d(this.Zd);this._drawImageOptions(d,
 | 
			
		||||
h,m,t,u,x||null)};a.Canvas.prototype.drawImageNine=function(d,h,m,t,u){a.$d(this.Zd);h=n(h,"HEAP32",Na);m=I(m);this._drawImageNine(d,h,m,t,u||null)};a.Canvas.prototype.drawImageRect=function(d,h,m,t,u){a.$d(this.Zd);I(h,W);I(m,Ba);this._drawImageRect(d,W,Ba,t,!!u)};a.Canvas.prototype.drawImageRectCubic=function(d,h,m,t,u,x){a.$d(this.Zd);I(h,W);I(m,Ba);this._drawImageRectCubic(d,W,Ba,t,u,x||null)};a.Canvas.prototype.drawImageRectOptions=function(d,h,m,t,u,x){a.$d(this.Zd);I(h,W);I(m,Ba);this._drawImageRectOptions(d,
 | 
			
		||||
W,Ba,t,u,x||null)};a.Canvas.prototype.drawLine=function(d,h,m,t,u){a.$d(this.Zd);this._drawLine(d,h,m,t,u)};a.Canvas.prototype.drawOval=function(d,h){a.$d(this.Zd);d=I(d);this._drawOval(d,h)};a.Canvas.prototype.drawPaint=function(d){a.$d(this.Zd);this._drawPaint(d)};a.Canvas.prototype.drawParagraph=function(d,h,m){a.$d(this.Zd);this._drawParagraph(d,h,m)};a.Canvas.prototype.drawPatch=function(d,h,m,t,u){if(24>d.length)throw"Need 12 cubic points";if(h&&4>h.length)throw"Need 4 colors";if(m&&8>m.length)throw"Need 4 shader coordinates";
 | 
			
		||||
a.$d(this.Zd);const x=n(d,"HEAPF32"),C=h?n(c(h),"HEAPU32"):K,G=m?n(m,"HEAPF32"):K;t||(t=a.BlendMode.Modulate);this._drawPatch(x,C,G,t,u);k(G,m);k(C,h);k(x,d)};a.Canvas.prototype.drawPath=function(d,h){a.$d(this.Zd);this._drawPath(d,h)};a.Canvas.prototype.drawPicture=function(d){a.$d(this.Zd);this._drawPicture(d)};a.Canvas.prototype.drawPoints=function(d,h,m){a.$d(this.Zd);var t=n(h,"HEAPF32");this._drawPoints(d,t,h.length/2,m);k(t,h)};a.Canvas.prototype.drawRRect=function(d,h){a.$d(this.Zd);d=R(d);
 | 
			
		||||
this._drawRRect(d,h)};a.Canvas.prototype.drawRect=function(d,h){a.$d(this.Zd);d=I(d);this._drawRect(d,h)};a.Canvas.prototype.drawRect4f=function(d,h,m,t,u){a.$d(this.Zd);this._drawRect4f(d,h,m,t,u)};a.Canvas.prototype.drawShadow=function(d,h,m,t,u,x,C){a.$d(this.Zd);var G=n(u,"HEAPF32"),F=n(x,"HEAPF32");h=n(h,"HEAPF32",vb);m=n(m,"HEAPF32",wb);this._drawShadow(d,h,m,t,G,F,C);k(G,u);k(F,x)};a.getShadowLocalBounds=function(d,h,m,t,u,x,C){d=p(d);m=n(m,"HEAPF32",vb);t=n(t,"HEAPF32",wb);if(!this._getShadowLocalBounds(d,
 | 
			
		||||
h,m,t,u,x,W))return null;h=fa.toTypedArray();return C?(C.set(h),C):h.slice()};a.Canvas.prototype.drawTextBlob=function(d,h,m,t){a.$d(this.Zd);this._drawTextBlob(d,h,m,t)};a.Canvas.prototype.drawVertices=function(d,h,m){a.$d(this.Zd);this._drawVertices(d,h,m)};a.Canvas.prototype.getDeviceClipBounds=function(d){this._getDeviceClipBounds(Na);var h=$a.toTypedArray();d?d.set(h):d=h.slice();return d};a.Canvas.prototype.quickReject=function(d){d=I(d);return this._quickReject(d)};a.Canvas.prototype.getLocalToDevice=
 | 
			
		||||
function(){this._getLocalToDevice(ma);for(var d=ma,h=Array(16),m=0;16>m;m++)h[m]=a.HEAPF32[d/4+m];return h};a.Canvas.prototype.getTotalMatrix=function(){this._getTotalMatrix(P);for(var d=Array(9),h=0;9>h;h++)d[h]=a.HEAPF32[P/4+h];return d};a.Canvas.prototype.makeSurface=function(d){d=this._makeSurface(d);d.Zd=this.Zd;return d};a.Canvas.prototype.readPixels=function(d,h,m,t,u){a.$d(this.Zd);return g(this,d,h,m,t,u)};a.Canvas.prototype.saveLayer=function(d,h,m,t,u){h=I(h);return this._saveLayer(d||
 | 
			
		||||
null,h,m||null,t||0,u||a.TileMode.Clamp)};a.Canvas.prototype.writePixels=function(d,h,m,t,u,x,C,G){if(d.byteLength%(h*m))throw"pixels length must be a multiple of the srcWidth * srcHeight";a.$d(this.Zd);var F=d.byteLength/(h*m);x=x||a.AlphaType.Unpremul;C=C||a.ColorType.RGBA_8888;G=G||a.ColorSpace.SRGB;var T=F*h;F=n(d,"HEAPU8");h=this._writePixels({width:h,height:m,colorType:C,alphaType:x,colorSpace:G},F,T,t,u);k(F,d);return h};a.ColorFilter.MakeBlend=function(d,h,m){d=w(d);m=m||a.ColorSpace.SRGB;
 | 
			
		||||
return a.ColorFilter._MakeBlend(d,h,m)};a.ColorFilter.MakeMatrix=function(d){if(!d||20!==d.length)throw"invalid color matrix";var h=n(d,"HEAPF32"),m=a.ColorFilter._makeMatrix(h);k(h,d);return m};a.ContourMeasure.prototype.getPosTan=function(d,h){this._getPosTan(d,W);d=fa.toTypedArray();return h?(h.set(d),h):d.slice()};a.ImageFilter.prototype.getOutputBounds=function(d,h,m){d=I(d,W);h=p(h);this._getOutputBounds(d,h,Na);h=$a.toTypedArray();return m?(m.set(h),m):h.slice()};a.ImageFilter.MakeDropShadow=
 | 
			
		||||
function(d,h,m,t,u,x){u=w(u,ia);return a.ImageFilter._MakeDropShadow(d,h,m,t,u,x)};a.ImageFilter.MakeDropShadowOnly=function(d,h,m,t,u,x){u=w(u,ia);return a.ImageFilter._MakeDropShadowOnly(d,h,m,t,u,x)};a.ImageFilter.MakeImage=function(d,h,m,t){m=I(m,W);t=I(t,Ba);if("B"in h&&"C"in h)return a.ImageFilter._MakeImageCubic(d,h.B,h.C,m,t);const u=h.filter;let x=a.MipmapMode.None;"mipmap"in h&&(x=h.mipmap);return a.ImageFilter._MakeImageOptions(d,u,x,m,t)};a.ImageFilter.MakeMatrixTransform=function(d,h,
 | 
			
		||||
m){d=p(d);if("B"in h&&"C"in h)return a.ImageFilter._MakeMatrixTransformCubic(d,h.B,h.C,m);const t=h.filter;let u=a.MipmapMode.None;"mipmap"in h&&(u=h.mipmap);return a.ImageFilter._MakeMatrixTransformOptions(d,t,u,m)};a.Paint.prototype.getColor=function(){this._getColor(ia);return D(ia)};a.Paint.prototype.setColor=function(d,h){h=h||null;d=w(d);this._setColor(d,h)};a.Paint.prototype.setColorComponents=function(d,h,m,t,u){u=u||null;d=A(d,h,m,t);this._setColor(d,u)};a.Path.prototype.getPoint=function(d,
 | 
			
		||||
h){this._getPoint(d,W);d=fa.toTypedArray();return h?(h[0]=d[0],h[1]=d[1],h):d.slice(0,2)};a.Picture.prototype.makeShader=function(d,h,m,t,u){t=p(t);u=I(u);return this._makeShader(d,h,m,t,u)};a.Picture.prototype.cullRect=function(d){this._cullRect(W);var h=fa.toTypedArray();return d?(d.set(h),d):h.slice()};a.PictureRecorder.prototype.beginRecording=function(d,h){d=I(d);return this._beginRecording(d,!!h)};a.Surface.prototype.getCanvas=function(){var d=this._getCanvas();d.Zd=this.Zd;return d};a.Surface.prototype.makeImageSnapshot=
 | 
			
		||||
function(d){a.$d(this.Zd);d=n(d,"HEAP32",Na);return this._makeImageSnapshot(d)};a.Surface.prototype.makeSurface=function(d){a.$d(this.Zd);d=this._makeSurface(d);d.Zd=this.Zd;return d};a.Surface.prototype.Ze=function(d,h){this.te||(this.te=this.getCanvas());return requestAnimationFrame(function(){a.$d(this.Zd);d(this.te);this.flush(h)}.bind(this))};a.Surface.prototype.requestAnimationFrame||(a.Surface.prototype.requestAnimationFrame=a.Surface.prototype.Ze);a.Surface.prototype.We=function(d,h){this.te||
 | 
			
		||||
(this.te=this.getCanvas());requestAnimationFrame(function(){a.$d(this.Zd);d(this.te);this.flush(h);this.dispose()}.bind(this))};a.Surface.prototype.drawOnce||(a.Surface.prototype.drawOnce=a.Surface.prototype.We);a.PathEffect.MakeDash=function(d,h){h||=0;if(!d.length||1===d.length%2)throw"Intervals array must have even length";var m=n(d,"HEAPF32");h=a.PathEffect._MakeDash(m,d.length,h);k(m,d);return h};a.PathEffect.MakeLine2D=function(d,h){h=p(h);return a.PathEffect._MakeLine2D(d,h)};a.PathEffect.MakePath2D=
 | 
			
		||||
function(d,h){d=p(d);return a.PathEffect._MakePath2D(d,h)};a.Shader.MakeColor=function(d,h){h=h||null;d=w(d);return a.Shader._MakeColor(d,h)};a.Shader.Blend=a.Shader.MakeBlend;a.Shader.Color=a.Shader.MakeColor;a.Shader.MakeLinearGradient=function(d,h,m,t,u,x,C,G){G=G||null;var F=l(m),T=n(t,"HEAPF32");C=C||0;x=p(x);var U=fa.toTypedArray();U.set(d);U.set(h,2);d=a.Shader._MakeLinearGradient(W,F.he,F.colorType,T,F.count,u,C,x,G);k(F.he,m);t&&k(T,t);return d};a.Shader.MakeRadialGradient=function(d,h,m,
 | 
			
		||||
t,u,x,C,G){G=G||null;var F=l(m),T=n(t,"HEAPF32");C=C||0;x=p(x);d=a.Shader._MakeRadialGradient(d[0],d[1],h,F.he,F.colorType,T,F.count,u,C,x,G);k(F.he,m);t&&k(T,t);return d};a.Shader.MakeSweepGradient=function(d,h,m,t,u,x,C,G,F,T){T=T||null;var U=l(m),q=n(t,"HEAPF32");C=C||0;G=G||0;F=F||360;x=p(x);d=a.Shader._MakeSweepGradient(d,h,U.he,U.colorType,q,U.count,u,G,F,C,x,T);k(U.he,m);t&&k(q,t);return d};a.Shader.MakeTwoPointConicalGradient=function(d,h,m,t,u,x,C,G,F,T){T=T||null;var U=l(u),q=n(x,"HEAPF32");
 | 
			
		||||
F=F||0;G=p(G);var y=fa.toTypedArray();y.set(d);y.set(m,2);d=a.Shader._MakeTwoPointConicalGradient(W,h,t,U.he,U.colorType,q,U.count,C,F,G,T);k(U.he,u);x&&k(q,x);return d};a.Vertices.prototype.bounds=function(d){this._bounds(W);var h=fa.toTypedArray();return d?(d.set(h),d):h.slice()};a.ce&&a.ce.forEach(function(d){d()})};a.computeTonalColors=function(g){var d=n(g.ambient,"HEAPF32"),h=n(g.spot,"HEAPF32");this._computeTonalColors(d,h);var m={ambient:D(d),spot:D(h)};k(d,g.ambient);k(h,g.spot);return m};
 | 
			
		||||
a.LTRBRect=function(g,d,h,m){return Float32Array.of(g,d,h,m)};a.XYWHRect=function(g,d,h,m){return Float32Array.of(g,d,g+h,d+m)};a.LTRBiRect=function(g,d,h,m){return Int32Array.of(g,d,h,m)};a.XYWHiRect=function(g,d,h,m){return Int32Array.of(g,d,g+h,d+m)};a.RRectXY=function(g,d,h){return Float32Array.of(g[0],g[1],g[2],g[3],d,h,d,h,d,h,d,h)};a.MakeAnimatedImageFromEncoded=function(g){g=new Uint8Array(g);var d=a._malloc(g.byteLength);a.HEAPU8.set(g,d);return(g=a._decodeAnimatedImage(d,g.byteLength))?
 | 
			
		||||
g:null};a.MakeImageFromEncoded=function(g){g=new Uint8Array(g);var d=a._malloc(g.byteLength);a.HEAPU8.set(g,d);return(g=a._decodeImage(d,g.byteLength))?g:null};var ab=null;a.MakeImageFromCanvasImageSource=function(g){var d=g.width,h=g.height;ab||=document.createElement("canvas");ab.width=d;ab.height=h;var m=ab.getContext("2d",{willReadFrequently:!0});m.drawImage(g,0,0);g=m.getImageData(0,0,d,h);return a.MakeImage({width:d,height:h,alphaType:a.AlphaType.Unpremul,colorType:a.ColorType.RGBA_8888,colorSpace:a.ColorSpace.SRGB},
 | 
			
		||||
g.data,4*d)};a.MakeImage=function(g,d,h){var m=a._malloc(d.length);a.HEAPU8.set(d,m);return a._MakeImage(g,m,d.length,h)};a.MakeVertices=function(g,d,h,m,t,u){var x=t&&t.length||0,C=0;h&&h.length&&(C|=1);m&&m.length&&(C|=2);void 0===u||u||(C|=4);g=new a._VerticesBuilder(g,d.length/2,x,C);n(d,"HEAPF32",g.positions());g.texCoords()&&n(h,"HEAPF32",g.texCoords());g.colors()&&n(c(m),"HEAPU32",g.colors());g.indices()&&n(t,"HEAPU16",g.indices());return g.detach()};(function(g){g.ce=g.ce||[];g.ce.push(function(){function d(q){q&&
 | 
			
		||||
(q.dir=0===q.dir?g.TextDirection.RTL:g.TextDirection.LTR);return q}function h(q){if(!q||!q.length)return[];for(var y=[],N=0;N<q.length;N+=5){var X=g.LTRBRect(q[N],q[N+1],q[N+2],q[N+3]),xa=g.TextDirection.LTR;0===q[N+4]&&(xa=g.TextDirection.RTL);y.push({rect:X,dir:xa})}g._free(q.byteOffset);return y}function m(q){q=q||{};void 0===q.weight&&(q.weight=g.FontWeight.Normal);q.width=q.width||g.FontWidth.Normal;q.slant=q.slant||g.FontSlant.Upright;return q}function t(q){if(!q||!q.length)return K;for(var y=
 | 
			
		||||
[],N=0;N<q.length;N++){var X=u(q[N]);y.push(X)}return n(y,"HEAPU32")}function u(q){if(G[q])return G[q];var y=qa(q)+1,N=g._malloc(y);ra(q,N,y);return G[q]=N}function x(q){q._colorPtr=w(q.color);q._foregroundColorPtr=K;q._backgroundColorPtr=K;q._decorationColorPtr=K;q.foregroundColor&&(q._foregroundColorPtr=w(q.foregroundColor,F));q.backgroundColor&&(q._backgroundColorPtr=w(q.backgroundColor,T));q.decorationColor&&(q._decorationColorPtr=w(q.decorationColor,U));Array.isArray(q.fontFamilies)&&q.fontFamilies.length?
 | 
			
		||||
(q._fontFamiliesPtr=t(q.fontFamilies),q._fontFamiliesLen=q.fontFamilies.length):(q._fontFamiliesPtr=K,q._fontFamiliesLen=0);if(q.locale){var y=q.locale;q._localePtr=u(y);q._localeLen=qa(y)}else q._localePtr=K,q._localeLen=0;if(Array.isArray(q.shadows)&&q.shadows.length){y=q.shadows;var N=y.map(function(na){return na.color||g.BLACK}),X=y.map(function(na){return na.blurRadius||0});q._shadowLen=y.length;for(var xa=g._malloc(8*y.length),xb=xa/4,yb=0;yb<y.length;yb++){var $b=y[yb].offset||[0,0];g.HEAPF32[xb]=
 | 
			
		||||
$b[0];g.HEAPF32[xb+1]=$b[1];xb+=2}q._shadowColorsPtr=l(N).he;q._shadowOffsetsPtr=xa;q._shadowBlurRadiiPtr=n(X,"HEAPF32")}else q._shadowLen=0,q._shadowColorsPtr=K,q._shadowOffsetsPtr=K,q._shadowBlurRadiiPtr=K;Array.isArray(q.fontFeatures)&&q.fontFeatures.length?(y=q.fontFeatures,N=y.map(function(na){return na.name}),X=y.map(function(na){return na.value}),q._fontFeatureLen=y.length,q._fontFeatureNamesPtr=t(N),q._fontFeatureValuesPtr=n(X,"HEAPU32")):(q._fontFeatureLen=0,q._fontFeatureNamesPtr=K,q._fontFeatureValuesPtr=
 | 
			
		||||
K);Array.isArray(q.fontVariations)&&q.fontVariations.length?(y=q.fontVariations,N=y.map(function(na){return na.axis}),X=y.map(function(na){return na.value}),q._fontVariationLen=y.length,q._fontVariationAxesPtr=t(N),q._fontVariationValuesPtr=n(X,"HEAPF32")):(q._fontVariationLen=0,q._fontVariationAxesPtr=K,q._fontVariationValuesPtr=K)}function C(q){g._free(q._fontFamiliesPtr);g._free(q._shadowColorsPtr);g._free(q._shadowOffsetsPtr);g._free(q._shadowBlurRadiiPtr);g._free(q._fontFeatureNamesPtr);g._free(q._fontFeatureValuesPtr);
 | 
			
		||||
g._free(q._fontVariationAxesPtr);g._free(q._fontVariationValuesPtr)}g.Paragraph.prototype.getRectsForRange=function(q,y,N,X){q=this._getRectsForRange(q,y,N,X);return h(q)};g.Paragraph.prototype.getRectsForPlaceholders=function(){var q=this._getRectsForPlaceholders();return h(q)};g.Paragraph.prototype.getGlyphInfoAt=function(q){return d(this._getGlyphInfoAt(q))};g.Paragraph.prototype.getClosestGlyphInfoAtCoordinate=function(q,y){return d(this._getClosestGlyphInfoAtCoordinate(q,y))};g.TypefaceFontProvider.prototype.registerFont=
 | 
			
		||||
function(q,y){q=g.Typeface.MakeTypefaceFromData(q);if(!q)return null;y=u(y);this._registerFont(q,y)};g.ParagraphStyle=function(q){q.disableHinting=q.disableHinting||!1;if(q.ellipsis){var y=q.ellipsis;q._ellipsisPtr=u(y);q._ellipsisLen=qa(y)}else q._ellipsisPtr=K,q._ellipsisLen=0;null==q.heightMultiplier&&(q.heightMultiplier=-1);q.maxLines=q.maxLines||0;q.replaceTabCharacters=q.replaceTabCharacters||!1;y=(y=q.strutStyle)||{};y.strutEnabled=y.strutEnabled||!1;y.strutEnabled&&Array.isArray(y.fontFamilies)&&
 | 
			
		||||
y.fontFamilies.length?(y._fontFamiliesPtr=t(y.fontFamilies),y._fontFamiliesLen=y.fontFamilies.length):(y._fontFamiliesPtr=K,y._fontFamiliesLen=0);y.fontStyle=m(y.fontStyle);null==y.fontSize&&(y.fontSize=-1);null==y.heightMultiplier&&(y.heightMultiplier=-1);y.halfLeading=y.halfLeading||!1;y.leading=y.leading||0;y.forceStrutHeight=y.forceStrutHeight||!1;q.strutStyle=y;q.textAlign=q.textAlign||g.TextAlign.Start;q.textDirection=q.textDirection||g.TextDirection.LTR;q.textHeightBehavior=q.textHeightBehavior||
 | 
			
		||||
g.TextHeightBehavior.All;q.textStyle=g.TextStyle(q.textStyle);q.applyRoundingHack=!1!==q.applyRoundingHack;return q};g.TextStyle=function(q){q.color||(q.color=g.BLACK);q.decoration=q.decoration||0;q.decorationThickness=q.decorationThickness||0;q.decorationStyle=q.decorationStyle||g.DecorationStyle.Solid;q.textBaseline=q.textBaseline||g.TextBaseline.Alphabetic;null==q.fontSize&&(q.fontSize=-1);q.letterSpacing=q.letterSpacing||0;q.wordSpacing=q.wordSpacing||0;null==q.heightMultiplier&&(q.heightMultiplier=
 | 
			
		||||
-1);q.halfLeading=q.halfLeading||!1;q.fontStyle=m(q.fontStyle);return q};var G={},F=g._malloc(16),T=g._malloc(16),U=g._malloc(16);g.ParagraphBuilder.Make=function(q,y){x(q.textStyle);y=g.ParagraphBuilder._Make(q,y);C(q.textStyle);return y};g.ParagraphBuilder.MakeFromFontProvider=function(q,y){x(q.textStyle);y=g.ParagraphBuilder._MakeFromFontProvider(q,y);C(q.textStyle);return y};g.ParagraphBuilder.MakeFromFontCollection=function(q,y){x(q.textStyle);y=g.ParagraphBuilder._MakeFromFontCollection(q,y);
 | 
			
		||||
C(q.textStyle);return y};g.ParagraphBuilder.ShapeText=function(q,y,N){let X=0;for(const xa of y)X+=xa.length;if(X!==q.length)throw"Accumulated block lengths must equal text.length";return g.ParagraphBuilder._ShapeText(q,y,N)};g.ParagraphBuilder.prototype.pushStyle=function(q){x(q);this._pushStyle(q);C(q)};g.ParagraphBuilder.prototype.pushPaintStyle=function(q,y,N){x(q);this._pushPaintStyle(q,y,N);C(q)};g.ParagraphBuilder.prototype.addPlaceholder=function(q,y,N,X,xa){N=N||g.PlaceholderAlignment.Baseline;
 | 
			
		||||
X=X||g.TextBaseline.Alphabetic;this._addPlaceholder(q||0,y||0,N,X,xa||0)};g.ParagraphBuilder.prototype.setWordsUtf8=function(q){var y=n(q,"HEAPU32");this._setWordsUtf8(y,q&&q.length||0);k(y,q)};g.ParagraphBuilder.prototype.setWordsUtf16=function(q){var y=n(q,"HEAPU32");this._setWordsUtf16(y,q&&q.length||0);k(y,q)};g.ParagraphBuilder.prototype.setGraphemeBreaksUtf8=function(q){var y=n(q,"HEAPU32");this._setGraphemeBreaksUtf8(y,q&&q.length||0);k(y,q)};g.ParagraphBuilder.prototype.setGraphemeBreaksUtf16=
 | 
			
		||||
function(q){var y=n(q,"HEAPU32");this._setGraphemeBreaksUtf16(y,q&&q.length||0);k(y,q)};g.ParagraphBuilder.prototype.setLineBreaksUtf8=function(q){var y=n(q,"HEAPU32");this._setLineBreaksUtf8(y,q&&q.length||0);k(y,q)};g.ParagraphBuilder.prototype.setLineBreaksUtf16=function(q){var y=n(q,"HEAPU32");this._setLineBreaksUtf16(y,q&&q.length||0);k(y,q)}})})(r);a.ce=a.ce||[];a.ce.push(function(){a.Path.prototype.op=function(g,d){return this._op(g,d)?this:null};a.Path.prototype.simplify=function(){return this._simplify()?
 | 
			
		||||
this:null}});a.ce=a.ce||[];a.ce.push(function(){a.Canvas.prototype.drawText=function(g,d,h,m,t){var u=qa(g),x=a._malloc(u+1);ra(g,x,u+1);this._drawSimpleText(x,u,d,h,t,m);a._free(x)};a.Canvas.prototype.drawGlyphs=function(g,d,h,m,t,u){if(!(2*g.length<=d.length))throw"Not enough positions for the array of gyphs";a.$d(this.Zd);const x=n(g,"HEAPU16"),C=n(d,"HEAPF32");this._drawGlyphs(g.length,x,C,h,m,t,u);k(C,d);k(x,g)};a.Font.prototype.getGlyphBounds=function(g,d,h){var m=n(g,"HEAPU16"),t=a._malloc(16*
 | 
			
		||||
g.length);this._getGlyphWidthBounds(m,g.length,K,t,d||null);d=new Float32Array(a.HEAPU8.buffer,t,4*g.length);k(m,g);if(h)return h.set(d),a._free(t),h;g=Float32Array.from(d);a._free(t);return g};a.Font.prototype.getGlyphIDs=function(g,d,h){d||(d=g.length);var m=qa(g)+1,t=a._malloc(m);ra(g,t,m);g=a._malloc(2*d);d=this._getGlyphIDs(t,m-1,d,g);a._free(t);if(0>d)return a._free(g),null;t=new Uint16Array(a.HEAPU8.buffer,g,d);if(h)return h.set(t),a._free(g),h;h=Uint16Array.from(t);a._free(g);return h};a.Font.prototype.getGlyphIntercepts=
 | 
			
		||||
function(g,d,h,m){var t=n(g,"HEAPU16"),u=n(d,"HEAPF32");return this._getGlyphIntercepts(t,g.length,!(g&&g._ck),u,d.length,!(d&&d._ck),h,m)};a.Font.prototype.getGlyphWidths=function(g,d,h){var m=n(g,"HEAPU16"),t=a._malloc(4*g.length);this._getGlyphWidthBounds(m,g.length,t,K,d||null);d=new Float32Array(a.HEAPU8.buffer,t,g.length);k(m,g);if(h)return h.set(d),a._free(t),h;g=Float32Array.from(d);a._free(t);return g};a.FontMgr.FromData=function(){if(!arguments.length)return null;var g=arguments;1===g.length&&
 | 
			
		||||
Array.isArray(g[0])&&(g=arguments[0]);if(!g.length)return null;for(var d=[],h=[],m=0;m<g.length;m++){var t=new Uint8Array(g[m]),u=n(t,"HEAPU8");d.push(u);h.push(t.byteLength)}d=n(d,"HEAPU32");h=n(h,"HEAPU32");g=a.FontMgr._fromData(d,h,g.length);a._free(d);a._free(h);return g};a.Typeface.MakeTypefaceFromData=function(g){g=new Uint8Array(g);var d=n(g,"HEAPU8");return(g=a.Typeface._MakeTypefaceFromData(d,g.byteLength))?g:null};a.Typeface.MakeFreeTypeFaceFromData=a.Typeface.MakeTypefaceFromData;a.Typeface.prototype.getGlyphIDs=
 | 
			
		||||
function(g,d,h){d||(d=g.length);var m=qa(g)+1,t=a._malloc(m);ra(g,t,m);g=a._malloc(2*d);d=this._getGlyphIDs(t,m-1,d,g);a._free(t);if(0>d)return a._free(g),null;t=new Uint16Array(a.HEAPU8.buffer,g,d);if(h)return h.set(t),a._free(g),h;h=Uint16Array.from(t);a._free(g);return h};a.TextBlob.MakeOnPath=function(g,d,h,m){if(g&&g.length&&d&&d.countPoints()){if(1===d.countPoints())return this.MakeFromText(g,h);m||=0;var t=h.getGlyphIDs(g);t=h.getGlyphWidths(t);var u=[];d=new a.ContourMeasureIter(d,!1,1);for(var x=
 | 
			
		||||
d.next(),C=new Float32Array(4),G=0;G<g.length&&x;G++){var F=t[G];m+=F/2;if(m>x.length()){x.delete();x=d.next();if(!x){g=g.substring(0,G);break}m=F/2}x.getPosTan(m,C);var T=C[2],U=C[3];u.push(T,U,C[0]-F/2*T,C[1]-F/2*U);m+=F/2}g=this.MakeFromRSXform(g,u,h);x&&x.delete();d.delete();return g}};a.TextBlob.MakeFromRSXform=function(g,d,h){var m=qa(g)+1,t=a._malloc(m);ra(g,t,m);g=n(d,"HEAPF32");h=a.TextBlob._MakeFromRSXform(t,m-1,g,h);a._free(t);return h?h:null};a.TextBlob.MakeFromRSXformGlyphs=function(g,
 | 
			
		||||
d,h){var m=n(g,"HEAPU16");d=n(d,"HEAPF32");h=a.TextBlob._MakeFromRSXformGlyphs(m,2*g.length,d,h);k(m,g);return h?h:null};a.TextBlob.MakeFromGlyphs=function(g,d){var h=n(g,"HEAPU16");d=a.TextBlob._MakeFromGlyphs(h,2*g.length,d);k(h,g);return d?d:null};a.TextBlob.MakeFromText=function(g,d){var h=qa(g)+1,m=a._malloc(h);ra(g,m,h);g=a.TextBlob._MakeFromText(m,h-1,d);a._free(m);return g?g:null};a.MallocGlyphIDs=function(g){return a.Malloc(Uint16Array,g)}});a.ce=a.ce||[];a.ce.push(function(){a.MakePicture=
 | 
			
		||||
function(g){g=new Uint8Array(g);var d=a._malloc(g.byteLength);a.HEAPU8.set(g,d);return(g=a._MakePicture(d,g.byteLength))?g:null}});a.ce=a.ce||[];a.ce.push(function(){a.RuntimeEffect.Make=function(g,d){return a.RuntimeEffect._Make(g,{onError:d||function(h){console.log("RuntimeEffect error",h)}})};a.RuntimeEffect.MakeForBlender=function(g,d){return a.RuntimeEffect._MakeForBlender(g,{onError:d||function(h){console.log("RuntimeEffect error",h)}})};a.RuntimeEffect.prototype.makeShader=function(g,d){var h=
 | 
			
		||||
!g._ck,m=n(g,"HEAPF32");d=p(d);return this._makeShader(m,4*g.length,h,d)};a.RuntimeEffect.prototype.makeShaderWithChildren=function(g,d,h){var m=!g._ck,t=n(g,"HEAPF32");h=p(h);for(var u=[],x=0;x<d.length;x++)u.push(d[x].Yd.ae);d=n(u,"HEAPU32");return this._makeShaderWithChildren(t,4*g.length,m,d,u.length,h)};a.RuntimeEffect.prototype.makeBlender=function(g){var d=!g._ck,h=n(g,"HEAPF32");return this._makeBlender(h,4*g.length,d)}})})(r);var sa=Object.assign({},r),ta="",ua,va;
 | 
			
		||||
if(ea||ha)ha?ta=self.location.href:"undefined"!=typeof document&&document.currentScript&&(ta=document.currentScript.src),_scriptName&&(ta=_scriptName),ta.startsWith("blob:")?ta="":ta=ta.substr(0,ta.replace(/[?#].*/,"").lastIndexOf("/")+1),ha&&(va=a=>{var b=new XMLHttpRequest;b.open("GET",a,!1);b.responseType="arraybuffer";b.send(null);return new Uint8Array(b.response)}),ua=a=>fetch(a,{credentials:"same-origin"}).then(b=>b.ok?b.arrayBuffer():Promise.reject(Error(b.status+" : "+b.url)));
 | 
			
		||||
var wa=console.log.bind(console),ya=console.error.bind(console);Object.assign(r,sa);sa=null;var za,Aa=!1,Ca,B,Da,Fa,E,H,J,Ga;function Ha(){var a=za.buffer;r.HEAP8=Ca=new Int8Array(a);r.HEAP16=Da=new Int16Array(a);r.HEAPU8=B=new Uint8Array(a);r.HEAPU16=Fa=new Uint16Array(a);r.HEAP32=E=new Int32Array(a);r.HEAPU32=H=new Uint32Array(a);r.HEAPF32=J=new Float32Array(a);r.HEAPF64=Ga=new Float64Array(a)}var Ia=[],Ja=[],Ka=[],La=0,Ma=null,Oa=null;
 | 
			
		||||
function Pa(a){a="Aborted("+a+")";ya(a);Aa=!0;a=new WebAssembly.RuntimeError(a+". Build with -sASSERTIONS for more info.");ca(a);throw a;}var Qa=a=>a.startsWith("data:application/octet-stream;base64,"),Ra;function Sa(a){return ua(a).then(b=>new Uint8Array(b),()=>{if(va)var b=va(a);else throw"both async and sync fetching of the wasm failed";return b})}function Ta(a,b,c){return Sa(a).then(e=>WebAssembly.instantiate(e,b)).then(c,e=>{ya(`failed to asynchronously prepare wasm: ${e}`);Pa(e)})}
 | 
			
		||||
function Ua(a,b){var c=Ra;return"function"!=typeof WebAssembly.instantiateStreaming||Qa(c)||"function"!=typeof fetch?Ta(c,a,b):fetch(c,{credentials:"same-origin"}).then(e=>WebAssembly.instantiateStreaming(e,a).then(b,function(f){ya(`wasm streaming compile failed: ${f}`);ya("falling back to ArrayBuffer instantiation");return Ta(c,a,b)}))}function Va(a){this.name="ExitStatus";this.message=`Program terminated with exit(${a})`;this.status=a}var Wa=a=>{a.forEach(b=>b(r))};
 | 
			
		||||
class Xa{constructor(a){this.ae=a-24}}
 | 
			
		||||
var Ya=0,Za=0,bb="undefined"!=typeof TextDecoder?new TextDecoder:void 0,cb=(a,b=0,c=NaN)=>{var e=b+c;for(c=b;a[c]&&!(c>=e);)++c;if(16<c-b&&a.buffer&&bb)return bb.decode(a.subarray(b,c));for(e="";b<c;){var f=a[b++];if(f&128){var k=a[b++]&63;if(192==(f&224))e+=String.fromCharCode((f&31)<<6|k);else{var n=a[b++]&63;f=224==(f&240)?(f&15)<<12|k<<6|n:(f&7)<<18|k<<12|n<<6|a[b++]&63;65536>f?e+=String.fromCharCode(f):(f-=65536,e+=String.fromCharCode(55296|f>>10,56320|f&1023))}}else e+=String.fromCharCode(f)}return e},
 | 
			
		||||
db={},eb=a=>{for(;a.length;){var b=a.pop();a.pop()(b)}};function fb(a){return this.fromWireType(H[a>>2])}
 | 
			
		||||
var gb={},hb={},ib={},jb,lb=(a,b,c)=>{function e(l){l=c(l);if(l.length!==a.length)throw new jb("Mismatched type converter count");for(var p=0;p<a.length;++p)kb(a[p],l[p])}a.forEach(l=>ib[l]=b);var f=Array(b.length),k=[],n=0;b.forEach((l,p)=>{hb.hasOwnProperty(l)?f[p]=hb[l]:(k.push(l),gb.hasOwnProperty(l)||(gb[l]=[]),gb[l].push(()=>{f[p]=hb[l];++n;n===k.length&&e(f)}))});0===k.length&&e(f)},mb,L=a=>{for(var b="";B[a];)b+=mb[B[a++]];return b},M;
 | 
			
		||||
function nb(a,b,c={}){var e=b.name;if(!a)throw new M(`type "${e}" must have a positive integer typeid pointer`);if(hb.hasOwnProperty(a)){if(c.lf)return;throw new M(`Cannot register type '${e}' twice`);}hb[a]=b;delete ib[a];gb.hasOwnProperty(a)&&(b=gb[a],delete gb[a],b.forEach(f=>f()))}function kb(a,b,c={}){return nb(a,b,c)}
 | 
			
		||||
var ob=a=>{throw new M(a.Yd.de.be.name+" instance already deleted");},pb=!1,qb=()=>{},rb=(a,b,c)=>{if(b===c)return a;if(void 0===c.ge)return null;a=rb(a,b,c.ge);return null===a?null:c.cf(a)},sb={},tb={},zb=(a,b)=>{if(void 0===b)throw new M("ptr should not be undefined");for(;a.ge;)b=a.ye(b),a=a.ge;return tb[b]},Bb=(a,b)=>{if(!b.de||!b.ae)throw new jb("makeClassHandle requires ptr and ptrType");if(!!b.ie!==!!b.ee)throw new jb("Both smartPtrType and smartPtr must be specified");b.count={value:1};return Ab(Object.create(a,
 | 
			
		||||
{Yd:{value:b,writable:!0}}))},Ab=a=>{if("undefined"===typeof FinalizationRegistry)return Ab=b=>b,a;pb=new FinalizationRegistry(b=>{b=b.Yd;--b.count.value;0===b.count.value&&(b.ee?b.ie.ne(b.ee):b.de.be.ne(b.ae))});Ab=b=>{var c=b.Yd;c.ee&&pb.register(b,{Yd:c},b);return b};qb=b=>{pb.unregister(b)};return Ab(a)},Cb=[];function Db(){}
 | 
			
		||||
var Eb=(a,b)=>Object.defineProperty(b,"name",{value:a}),Fb=(a,b,c)=>{if(void 0===a[b].fe){var e=a[b];a[b]=function(...f){if(!a[b].fe.hasOwnProperty(f.length))throw new M(`Function '${c}' called with an invalid number of arguments (${f.length}) - expects one of (${a[b].fe})!`);return a[b].fe[f.length].apply(this,f)};a[b].fe=[];a[b].fe[e.oe]=e}},Gb=(a,b,c)=>{if(r.hasOwnProperty(a)){if(void 0===c||void 0!==r[a].fe&&void 0!==r[a].fe[c])throw new M(`Cannot register public name '${a}' twice`);Fb(r,a,a);
 | 
			
		||||
if(r[a].fe.hasOwnProperty(c))throw new M(`Cannot register multiple overloads of a function with the same number of arguments (${c})!`);r[a].fe[c]=b}else r[a]=b,r[a].oe=c},Hb=a=>{a=a.replace(/[^a-zA-Z0-9_]/g,"$");var b=a.charCodeAt(0);return 48<=b&&57>=b?`_${a}`:a};function Ib(a,b,c,e,f,k,n,l){this.name=a;this.constructor=b;this.se=c;this.ne=e;this.ge=f;this.ff=k;this.ye=n;this.cf=l;this.pf=[]}
 | 
			
		||||
var Jb=(a,b,c)=>{for(;b!==c;){if(!b.ye)throw new M(`Expected null or instance of ${c.name}, got an instance of ${b.name}`);a=b.ye(a);b=b.ge}return a};function Kb(a,b){if(null===b){if(this.Ke)throw new M(`null is not a valid ${this.name}`);return 0}if(!b.Yd)throw new M(`Cannot pass "${Lb(b)}" as a ${this.name}`);if(!b.Yd.ae)throw new M(`Cannot pass deleted object as a pointer of type ${this.name}`);return Jb(b.Yd.ae,b.Yd.de.be,this.be)}
 | 
			
		||||
function Mb(a,b){if(null===b){if(this.Ke)throw new M(`null is not a valid ${this.name}`);if(this.De){var c=this.Le();null!==a&&a.push(this.ne,c);return c}return 0}if(!b||!b.Yd)throw new M(`Cannot pass "${Lb(b)}" as a ${this.name}`);if(!b.Yd.ae)throw new M(`Cannot pass deleted object as a pointer of type ${this.name}`);if(!this.Ce&&b.Yd.de.Ce)throw new M(`Cannot convert argument of type ${b.Yd.ie?b.Yd.ie.name:b.Yd.de.name} to parameter type ${this.name}`);c=Jb(b.Yd.ae,b.Yd.de.be,this.be);if(this.De){if(void 0===
 | 
			
		||||
b.Yd.ee)throw new M("Passing raw pointer to smart pointer is illegal");switch(this.uf){case 0:if(b.Yd.ie===this)c=b.Yd.ee;else throw new M(`Cannot convert argument of type ${b.Yd.ie?b.Yd.ie.name:b.Yd.de.name} to parameter type ${this.name}`);break;case 1:c=b.Yd.ee;break;case 2:if(b.Yd.ie===this)c=b.Yd.ee;else{var e=b.clone();c=this.qf(c,Nb(()=>e["delete"]()));null!==a&&a.push(this.ne,c)}break;default:throw new M("Unsupporting sharing policy");}}return c}
 | 
			
		||||
function Ob(a,b){if(null===b){if(this.Ke)throw new M(`null is not a valid ${this.name}`);return 0}if(!b.Yd)throw new M(`Cannot pass "${Lb(b)}" as a ${this.name}`);if(!b.Yd.ae)throw new M(`Cannot pass deleted object as a pointer of type ${this.name}`);if(b.Yd.de.Ce)throw new M(`Cannot convert argument of type ${b.Yd.de.name} to parameter type ${this.name}`);return Jb(b.Yd.ae,b.Yd.de.be,this.be)}
 | 
			
		||||
function Pb(a,b,c,e,f,k,n,l,p,v,w){this.name=a;this.be=b;this.Ke=c;this.Ce=e;this.De=f;this.nf=k;this.uf=n;this.Se=l;this.Le=p;this.qf=v;this.ne=w;f||void 0!==b.ge?this.toWireType=Mb:(this.toWireType=e?Kb:Ob,this.ke=null)}
 | 
			
		||||
var Qb=(a,b,c)=>{if(!r.hasOwnProperty(a))throw new jb("Replacing nonexistent public symbol");void 0!==r[a].fe&&void 0!==c?r[a].fe[c]=b:(r[a]=b,r[a].oe=c)},O,Rb=(a,b,c=[])=>{a.includes("j")?(a=a.replace(/p/g,"i"),b=(0,r["dynCall_"+a])(b,...c)):b=O.get(b)(...c);return b},Sb=(a,b)=>(...c)=>Rb(a,b,c),Tb=(a,b)=>{a=L(a);var c=a.includes("j")?Sb(a,b):O.get(b);if("function"!=typeof c)throw new M(`unknown function pointer with signature ${a}: ${b}`);return c},ac,dc=a=>{a=bc(a);var b=L(a);cc(a);return b},ec=
 | 
			
		||||
(a,b)=>{function c(k){f[k]||hb[k]||(ib[k]?ib[k].forEach(c):(e.push(k),f[k]=!0))}var e=[],f={};b.forEach(c);throw new ac(`${a}: `+e.map(dc).join([", "]));};function fc(a){for(var b=1;b<a.length;++b)if(null!==a[b]&&void 0===a[b].ke)return!0;return!1}
 | 
			
		||||
function gc(a,b,c,e,f){var k=b.length;if(2>k)throw new M("argTypes array size mismatch! Must at least get return value and 'this' types!");var n=null!==b[1]&&null!==c,l=fc(b),p="void"!==b[0].name,v=k-2,w=Array(v),A=[],D=[];return Eb(a,function(...I){D.length=0;A.length=n?2:1;A[0]=f;if(n){var R=b[1].toWireType(D,this);A[1]=R}for(var P=0;P<v;++P)w[P]=b[P+2].toWireType(D,I[P]),A.push(w[P]);I=e(...A);if(l)eb(D);else for(P=n?1:2;P<b.length;P++){var ba=1===P?R:w[P-2];null!==b[P].ke&&b[P].ke(ba)}R=p?b[0].fromWireType(I):
 | 
			
		||||
void 0;return R})}
 | 
			
		||||
var hc=(a,b)=>{for(var c=[],e=0;e<a;e++)c.push(H[b+4*e>>2]);return c},ic=a=>{a=a.trim();const b=a.indexOf("(");return-1!==b?a.substr(0,b):a},jc=[],kc=[],lc=a=>{9<a&&0===--kc[a+1]&&(kc[a]=void 0,jc.push(a))},mc=a=>{if(!a)throw new M("Cannot use deleted val. handle = "+a);return kc[a]},Nb=a=>{switch(a){case void 0:return 2;case null:return 4;case !0:return 6;case !1:return 8;default:const b=jc.pop()||kc.length;kc[b]=a;kc[b+1]=1;return b}},nc={name:"emscripten::val",fromWireType:a=>{var b=mc(a);lc(a);
 | 
			
		||||
return b},toWireType:(a,b)=>Nb(b),je:8,readValueFromPointer:fb,ke:null},oc=(a,b,c)=>{switch(b){case 1:return c?function(e){return this.fromWireType(Ca[e])}:function(e){return this.fromWireType(B[e])};case 2:return c?function(e){return this.fromWireType(Da[e>>1])}:function(e){return this.fromWireType(Fa[e>>1])};case 4:return c?function(e){return this.fromWireType(E[e>>2])}:function(e){return this.fromWireType(H[e>>2])};default:throw new TypeError(`invalid integer width (${b}): ${a}`);}},pc=(a,b)=>
 | 
			
		||||
{var c=hb[a];if(void 0===c)throw a=`${b} has unknown type ${dc(a)}`,new M(a);return c},Lb=a=>{if(null===a)return"null";var b=typeof a;return"object"===b||"array"===b||"function"===b?a.toString():""+a},qc=(a,b)=>{switch(b){case 4:return function(c){return this.fromWireType(J[c>>2])};case 8:return function(c){return this.fromWireType(Ga[c>>3])};default:throw new TypeError(`invalid float width (${b}): ${a}`);}},rc=(a,b,c)=>{switch(b){case 1:return c?e=>Ca[e]:e=>B[e];case 2:return c?e=>Da[e>>1]:e=>Fa[e>>
 | 
			
		||||
1];case 4:return c?e=>E[e>>2]:e=>H[e>>2];default:throw new TypeError(`invalid integer width (${b}): ${a}`);}},ra=(a,b,c)=>{var e=B;if(!(0<c))return 0;var f=b;c=b+c-1;for(var k=0;k<a.length;++k){var n=a.charCodeAt(k);if(55296<=n&&57343>=n){var l=a.charCodeAt(++k);n=65536+((n&1023)<<10)|l&1023}if(127>=n){if(b>=c)break;e[b++]=n}else{if(2047>=n){if(b+1>=c)break;e[b++]=192|n>>6}else{if(65535>=n){if(b+2>=c)break;e[b++]=224|n>>12}else{if(b+3>=c)break;e[b++]=240|n>>18;e[b++]=128|n>>12&63}e[b++]=128|n>>6&
 | 
			
		||||
63}e[b++]=128|n&63}}e[b]=0;return b-f},qa=a=>{for(var b=0,c=0;c<a.length;++c){var e=a.charCodeAt(c);127>=e?b++:2047>=e?b+=2:55296<=e&&57343>=e?(b+=4,++c):b+=3}return b},sc="undefined"!=typeof TextDecoder?new TextDecoder("utf-16le"):void 0,tc=(a,b)=>{var c=a>>1;for(var e=c+b/2;!(c>=e)&&Fa[c];)++c;c<<=1;if(32<c-a&&sc)return sc.decode(B.subarray(a,c));c="";for(e=0;!(e>=b/2);++e){var f=Da[a+2*e>>1];if(0==f)break;c+=String.fromCharCode(f)}return c},uc=(a,b,c)=>{c??=2147483647;if(2>c)return 0;c-=2;var e=
 | 
			
		||||
b;c=c<2*a.length?c/2:a.length;for(var f=0;f<c;++f)Da[b>>1]=a.charCodeAt(f),b+=2;Da[b>>1]=0;return b-e},vc=a=>2*a.length,wc=(a,b)=>{for(var c=0,e="";!(c>=b/4);){var f=E[a+4*c>>2];if(0==f)break;++c;65536<=f?(f-=65536,e+=String.fromCharCode(55296|f>>10,56320|f&1023)):e+=String.fromCharCode(f)}return e},xc=(a,b,c)=>{c??=2147483647;if(4>c)return 0;var e=b;c=e+c-4;for(var f=0;f<a.length;++f){var k=a.charCodeAt(f);if(55296<=k&&57343>=k){var n=a.charCodeAt(++f);k=65536+((k&1023)<<10)|n&1023}E[b>>2]=k;b+=
 | 
			
		||||
4;if(b+4>c)break}E[b>>2]=0;return b-e},yc=a=>{for(var b=0,c=0;c<a.length;++c){var e=a.charCodeAt(c);55296<=e&&57343>=e&&++c;b+=4}return b},zc=(a,b,c)=>{var e=[];a=a.toWireType(e,c);e.length&&(H[b>>2]=Nb(e));return a},Ac=[],Bc={},Cc=a=>{var b=Bc[a];return void 0===b?L(a):b},Dc=()=>{function a(b){b.$$$embind_global$$$=b;var c="object"==typeof $$$embind_global$$$&&b.$$$embind_global$$$==b;c||delete b.$$$embind_global$$$;return c}if("object"==typeof globalThis)return globalThis;if("object"==typeof $$$embind_global$$$)return $$$embind_global$$$;
 | 
			
		||||
"object"==typeof global&&a(global)?$$$embind_global$$$=global:"object"==typeof self&&a(self)&&($$$embind_global$$$=self);if("object"==typeof $$$embind_global$$$)return $$$embind_global$$$;throw Error("unable to get global object.");},Ec=a=>{var b=Ac.length;Ac.push(a);return b},Fc=(a,b)=>{for(var c=Array(a),e=0;e<a;++e)c[e]=pc(H[b+4*e>>2],"parameter "+e);return c},Gc=Reflect.construct,Q,Hc=a=>{var b=a.getExtension("ANGLE_instanced_arrays");b&&(a.vertexAttribDivisor=(c,e)=>b.vertexAttribDivisorANGLE(c,
 | 
			
		||||
e),a.drawArraysInstanced=(c,e,f,k)=>b.drawArraysInstancedANGLE(c,e,f,k),a.drawElementsInstanced=(c,e,f,k,n)=>b.drawElementsInstancedANGLE(c,e,f,k,n))},Ic=a=>{var b=a.getExtension("OES_vertex_array_object");b&&(a.createVertexArray=()=>b.createVertexArrayOES(),a.deleteVertexArray=c=>b.deleteVertexArrayOES(c),a.bindVertexArray=c=>b.bindVertexArrayOES(c),a.isVertexArray=c=>b.isVertexArrayOES(c))},Jc=a=>{var b=a.getExtension("WEBGL_draw_buffers");b&&(a.drawBuffers=(c,e)=>b.drawBuffersWEBGL(c,e))},Kc=a=>
 | 
			
		||||
{var b="ANGLE_instanced_arrays EXT_blend_minmax EXT_disjoint_timer_query EXT_frag_depth EXT_shader_texture_lod EXT_sRGB OES_element_index_uint OES_fbo_render_mipmap OES_standard_derivatives OES_texture_float OES_texture_half_float OES_texture_half_float_linear OES_vertex_array_object WEBGL_color_buffer_float WEBGL_depth_texture WEBGL_draw_buffers EXT_color_buffer_float EXT_conservative_depth EXT_disjoint_timer_query_webgl2 EXT_texture_norm16 NV_shader_noperspective_interpolation WEBGL_clip_cull_distance EXT_clip_control EXT_color_buffer_half_float EXT_depth_clamp EXT_float_blend EXT_polygon_offset_clamp EXT_texture_compression_bptc EXT_texture_compression_rgtc EXT_texture_filter_anisotropic KHR_parallel_shader_compile OES_texture_float_linear WEBGL_blend_func_extended WEBGL_compressed_texture_astc WEBGL_compressed_texture_etc WEBGL_compressed_texture_etc1 WEBGL_compressed_texture_s3tc WEBGL_compressed_texture_s3tc_srgb WEBGL_debug_renderer_info WEBGL_debug_shaders WEBGL_lose_context WEBGL_multi_draw WEBGL_polygon_mode".split(" ");
 | 
			
		||||
return(a.getSupportedExtensions()||[]).filter(c=>b.includes(c))},Lc=1,Mc=[],Nc=[],Oc=[],Pc=[],ka=[],Qc=[],Rc=[],pa=[],Sc=[],Tc=[],Uc=[],Vc={},Xc={},Yc=4,Zc=0,ja=a=>{for(var b=Lc++,c=a.length;c<b;c++)a[c]=null;return b},$c=(a,b,c,e)=>{for(var f=0;f<a;f++){var k=Q[c](),n=k&&ja(e);k?(k.name=n,e[n]=k):S||=1282;E[b+4*f>>2]=n}},la=(a,b)=>{a.Ne||(a.Ne=a.getContext,a.getContext=function(e,f){f=a.Ne(e,f);return"webgl"==e==f instanceof WebGLRenderingContext?f:null});var c=1<b.majorVersion?a.getContext("webgl2",
 | 
			
		||||
b):a.getContext("webgl",b);return c?ad(c,b):0},ad=(a,b)=>{var c=ja(pa),e={handle:c,attributes:b,version:b.majorVersion,le:a};a.canvas&&(a.canvas.Ve=e);pa[c]=e;("undefined"==typeof b.df||b.df)&&bd(e);return c},oa=a=>{z=pa[a];r.vf=Q=z?.le;return!(a&&!Q)},bd=a=>{a||=z;if(!a.mf){a.mf=!0;var b=a.le;b.zf=b.getExtension("WEBGL_multi_draw");b.xf=b.getExtension("EXT_polygon_offset_clamp");b.wf=b.getExtension("EXT_clip_control");b.Bf=b.getExtension("WEBGL_polygon_mode");Hc(b);Ic(b);Jc(b);b.Pe=b.getExtension("WEBGL_draw_instanced_base_vertex_base_instance");
 | 
			
		||||
b.Re=b.getExtension("WEBGL_multi_draw_instanced_base_vertex_base_instance");2<=a.version&&(b.me=b.getExtension("EXT_disjoint_timer_query_webgl2"));if(2>a.version||!b.me)b.me=b.getExtension("EXT_disjoint_timer_query");Kc(b).forEach(c=>{c.includes("lose_context")||c.includes("debug")||b.getExtension(c)})}},z,S,cd=(a,b)=>{Q.bindFramebuffer(a,Oc[b])},dd=a=>{Q.bindVertexArray(Rc[a])},ed=a=>Q.clear(a),fd=(a,b,c,e)=>Q.clearColor(a,b,c,e),gd=a=>Q.clearStencil(a),hd=(a,b)=>{for(var c=0;c<a;c++){var e=E[b+
 | 
			
		||||
4*c>>2];Q.deleteVertexArray(Rc[e]);Rc[e]=null}},jd=[],kd=(a,b)=>{$c(a,b,"createVertexArray",Rc)};function ld(){var a=Kc(Q);return a=a.concat(a.map(b=>"GL_"+b))}
 | 
			
		||||
var md=(a,b,c)=>{if(b){var e=void 0;switch(a){case 36346:e=1;break;case 36344:0!=c&&1!=c&&(S||=1280);return;case 34814:case 36345:e=0;break;case 34466:var f=Q.getParameter(34467);e=f?f.length:0;break;case 33309:if(2>z.version){S||=1282;return}e=ld().length;break;case 33307:case 33308:if(2>z.version){S||=1280;return}e=33307==a?3:0}if(void 0===e)switch(f=Q.getParameter(a),typeof f){case "number":e=f;break;case "boolean":e=f?1:0;break;case "string":S||=1280;return;case "object":if(null===f)switch(a){case 34964:case 35725:case 34965:case 36006:case 36007:case 32873:case 34229:case 36662:case 36663:case 35053:case 35055:case 36010:case 35097:case 35869:case 32874:case 36389:case 35983:case 35368:case 34068:e=
 | 
			
		||||
0;break;default:S||=1280;return}else{if(f instanceof Float32Array||f instanceof Uint32Array||f instanceof Int32Array||f instanceof Array){for(a=0;a<f.length;++a)switch(c){case 0:E[b+4*a>>2]=f[a];break;case 2:J[b+4*a>>2]=f[a];break;case 4:Ca[b+a]=f[a]?1:0}return}try{e=f.name|0}catch(k){S||=1280;ya(`GL_INVALID_ENUM in glGet${c}v: Unknown object returned from WebGL getParameter(${a})! (error: ${k})`);return}}break;default:S||=1280;ya(`GL_INVALID_ENUM in glGet${c}v: Native code calling glGet${c}v(${a}) and it returns ${f} of type ${typeof f}!`);
 | 
			
		||||
return}switch(c){case 1:c=e;H[b>>2]=c;H[b+4>>2]=(c-H[b>>2])/4294967296;break;case 0:E[b>>2]=e;break;case 2:J[b>>2]=e;break;case 4:Ca[b]=e?1:0}}else S||=1281},nd=(a,b)=>md(a,b,0),od=(a,b,c)=>{if(c){a=Sc[a];b=2>z.version?Q.me.getQueryObjectEXT(a,b):Q.getQueryParameter(a,b);var e;"boolean"==typeof b?e=b?1:0:e=b;H[c>>2]=e;H[c+4>>2]=(e-H[c>>2])/4294967296}else S||=1281},qd=a=>{var b=qa(a)+1,c=pd(b);c&&ra(a,c,b);return c},rd=a=>{var b=Vc[a];if(!b){switch(a){case 7939:b=qd(ld().join(" "));break;case 7936:case 7937:case 37445:case 37446:(b=
 | 
			
		||||
Q.getParameter(a))||(S||=1280);b=b?qd(b):0;break;case 7938:b=Q.getParameter(7938);var c=`OpenGL ES 2.0 (${b})`;2<=z.version&&(c=`OpenGL ES 3.0 (${b})`);b=qd(c);break;case 35724:b=Q.getParameter(35724);c=b.match(/^WebGL GLSL ES ([0-9]\.[0-9][0-9]?)(?:$| .*)/);null!==c&&(3==c[1].length&&(c[1]+="0"),b=`OpenGL ES GLSL ES ${c[1]} (${b})`);b=qd(b);break;default:S||=1280}Vc[a]=b}return b},sd=(a,b)=>{if(2>z.version)return S||=1282,0;var c=Xc[a];if(c)return 0>b||b>=c.length?(S||=1281,0):c[b];switch(a){case 7939:return c=
 | 
			
		||||
ld().map(qd),c=Xc[a]=c,0>b||b>=c.length?(S||=1281,0):c[b];default:return S||=1280,0}},td=a=>"]"==a.slice(-1)&&a.lastIndexOf("["),ud=a=>{a-=5120;return 0==a?Ca:1==a?B:2==a?Da:4==a?E:6==a?J:5==a||28922==a||28520==a||30779==a||30782==a?H:Fa},vd=(a,b,c,e,f)=>{a=ud(a);b=e*((Zc||c)*({5:3,6:4,8:2,29502:3,29504:4,26917:2,26918:2,29846:3,29847:4}[b-6402]||1)*a.BYTES_PER_ELEMENT+Yc-1&-Yc);return a.subarray(f>>>31-Math.clz32(a.BYTES_PER_ELEMENT),f+b>>>31-Math.clz32(a.BYTES_PER_ELEMENT))},V=a=>{var b=Q.bf;if(b){var c=
 | 
			
		||||
b.xe[a];"number"==typeof c&&(b.xe[a]=c=Q.getUniformLocation(b,b.Te[a]+(0<c?`[${c}]`:"")));return c}S||=1282},wd=[],xd=[],yd={},Ad=()=>{if(!zd){var a={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:("object"==typeof navigator&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8",_:"./this.program"},b;for(b in yd)void 0===yd[b]?delete a[b]:a[b]=yd[b];var c=[];for(b in a)c.push(`${b}=${a[b]}`);zd=c}return zd},zd,Bd=[null,[],[]];
 | 
			
		||||
jb=r.InternalError=class extends Error{constructor(a){super(a);this.name="InternalError"}};for(var Cd=Array(256),Dd=0;256>Dd;++Dd)Cd[Dd]=String.fromCharCode(Dd);mb=Cd;M=r.BindingError=class extends Error{constructor(a){super(a);this.name="BindingError"}};
 | 
			
		||||
Object.assign(Db.prototype,{isAliasOf:function(a){if(!(this instanceof Db&&a instanceof Db))return!1;var b=this.Yd.de.be,c=this.Yd.ae;a.Yd=a.Yd;var e=a.Yd.de.be;for(a=a.Yd.ae;b.ge;)c=b.ye(c),b=b.ge;for(;e.ge;)a=e.ye(a),e=e.ge;return b===e&&c===a},clone:function(){this.Yd.ae||ob(this);if(this.Yd.we)return this.Yd.count.value+=1,this;var a=Ab,b=Object,c=b.create,e=Object.getPrototypeOf(this),f=this.Yd;a=a(c.call(b,e,{Yd:{value:{count:f.count,ve:f.ve,we:f.we,ae:f.ae,de:f.de,ee:f.ee,ie:f.ie}}}));a.Yd.count.value+=
 | 
			
		||||
1;a.Yd.ve=!1;return a},["delete"](){this.Yd.ae||ob(this);if(this.Yd.ve&&!this.Yd.we)throw new M("Object already scheduled for deletion");qb(this);var a=this.Yd;--a.count.value;0===a.count.value&&(a.ee?a.ie.ne(a.ee):a.de.be.ne(a.ae));this.Yd.we||(this.Yd.ee=void 0,this.Yd.ae=void 0)},isDeleted:function(){return!this.Yd.ae},deleteLater:function(){this.Yd.ae||ob(this);if(this.Yd.ve&&!this.Yd.we)throw new M("Object already scheduled for deletion");Cb.push(this);this.Yd.ve=!0;return this}});
 | 
			
		||||
Object.assign(Pb.prototype,{gf(a){this.Se&&(a=this.Se(a));return a},Oe(a){this.ne?.(a)},je:8,readValueFromPointer:fb,fromWireType:function(a){function b(){return this.De?Bb(this.be.se,{de:this.nf,ae:c,ie:this,ee:a}):Bb(this.be.se,{de:this,ae:a})}var c=this.gf(a);if(!c)return this.Oe(a),null;var e=zb(this.be,c);if(void 0!==e){if(0===e.Yd.count.value)return e.Yd.ae=c,e.Yd.ee=a,e.clone();e=e.clone();this.Oe(a);return e}e=this.be.ff(c);e=sb[e];if(!e)return b.call(this);e=this.Ce?e.af:e.pointerType;var f=
 | 
			
		||||
rb(c,this.be,e.be);return null===f?b.call(this):this.De?Bb(e.be.se,{de:e,ae:f,ie:this,ee:a}):Bb(e.be.se,{de:e,ae:f})}});ac=r.UnboundTypeError=((a,b)=>{var c=Eb(b,function(e){this.name=b;this.message=e;e=Error(e).stack;void 0!==e&&(this.stack=this.toString()+"\n"+e.replace(/^Error(:[^\n]*)?\n/,""))});c.prototype=Object.create(a.prototype);c.prototype.constructor=c;c.prototype.toString=function(){return void 0===this.message?this.name:`${this.name}: ${this.message}`};return c})(Error,"UnboundTypeError");
 | 
			
		||||
kc.push(0,1,void 0,1,null,1,!0,1,!1,1);r.count_emval_handles=()=>kc.length/2-5-jc.length;for(var Ed=0;32>Ed;++Ed)jd.push(Array(Ed));var Fd=new Float32Array(288);for(Ed=0;288>=Ed;++Ed)wd[Ed]=Fd.subarray(0,Ed);var Gd=new Int32Array(288);for(Ed=0;288>=Ed;++Ed)xd[Ed]=Gd.subarray(0,Ed);
 | 
			
		||||
var Vd={F:(a,b,c)=>{var e=new Xa(a);H[e.ae+16>>2]=0;H[e.ae+4>>2]=b;H[e.ae+8>>2]=c;Ya=a;Za++;throw Ya;},V:function(){return 0},vd:()=>{},ud:function(){return 0},td:()=>{},sd:()=>{},U:function(){},rd:()=>{},nd:()=>{Pa("")},B:a=>{var b=db[a];delete db[a];var c=b.Le,e=b.ne,f=b.Qe,k=f.map(n=>n.kf).concat(f.map(n=>n.sf));lb([a],k,n=>{var l={};f.forEach((p,v)=>{var w=n[v],A=p.hf,D=p.jf,I=n[v+f.length],R=p.rf,P=p.tf;l[p.ef]={read:ba=>w.fromWireType(A(D,ba)),write:(ba,ma)=>{var Y=[];R(P,ba,I.toWireType(Y,
 | 
			
		||||
ma));eb(Y)}}});return[{name:b.name,fromWireType:p=>{var v={},w;for(w in l)v[w]=l[w].read(p);e(p);return v},toWireType:(p,v)=>{for(var w in l)if(!(w in v))throw new TypeError(`Missing field: "${w}"`);var A=c();for(w in l)l[w].write(A,v[w]);null!==p&&p.push(e,A);return A},je:8,readValueFromPointer:fb,ke:e}]})},Y:()=>{},md:(a,b,c,e)=>{b=L(b);kb(a,{name:b,fromWireType:function(f){return!!f},toWireType:function(f,k){return k?c:e},je:8,readValueFromPointer:function(f){return this.fromWireType(B[f])},ke:null})},
 | 
			
		||||
l:(a,b,c,e,f,k,n,l,p,v,w,A,D)=>{w=L(w);k=Tb(f,k);l&&=Tb(n,l);v&&=Tb(p,v);D=Tb(A,D);var I=Hb(w);Gb(I,function(){ec(`Cannot construct ${w} due to unbound types`,[e])});lb([a,b,c],e?[e]:[],R=>{R=R[0];if(e){var P=R.be;var ba=P.se}else ba=Db.prototype;R=Eb(w,function(...Ea){if(Object.getPrototypeOf(this)!==ma)throw new M("Use 'new' to construct "+w);if(void 0===Y.pe)throw new M(w+" has no accessible constructor");var fa=Y.pe[Ea.length];if(void 0===fa)throw new M(`Tried to invoke ctor of ${w} with invalid number of parameters (${Ea.length}) - expected (${Object.keys(Y.pe).toString()}) parameters instead!`);
 | 
			
		||||
return fa.apply(this,Ea)});var ma=Object.create(ba,{constructor:{value:R}});R.prototype=ma;var Y=new Ib(w,R,ma,D,P,k,l,v);if(Y.ge){var ia;(ia=Y.ge).ze??(ia.ze=[]);Y.ge.ze.push(Y)}P=new Pb(w,Y,!0,!1,!1);ia=new Pb(w+"*",Y,!1,!1,!1);ba=new Pb(w+" const*",Y,!1,!0,!1);sb[a]={pointerType:ia,af:ba};Qb(I,R);return[P,ia,ba]})},e:(a,b,c,e,f,k,n)=>{var l=hc(c,e);b=L(b);b=ic(b);k=Tb(f,k);lb([],[a],p=>{function v(){ec(`Cannot call ${w} due to unbound types`,l)}p=p[0];var w=`${p.name}.${b}`;b.startsWith("@@")&&
 | 
			
		||||
(b=Symbol[b.substring(2)]);var A=p.be.constructor;void 0===A[b]?(v.oe=c-1,A[b]=v):(Fb(A,b,w),A[b].fe[c-1]=v);lb([],l,D=>{D=[D[0],null].concat(D.slice(1));D=gc(w,D,null,k,n);void 0===A[b].fe?(D.oe=c-1,A[b]=D):A[b].fe[c-1]=D;if(p.be.ze)for(const I of p.be.ze)I.constructor.hasOwnProperty(b)||(I.constructor[b]=D);return[]});return[]})},z:(a,b,c,e,f,k)=>{var n=hc(b,c);f=Tb(e,f);lb([],[a],l=>{l=l[0];var p=`constructor ${l.name}`;void 0===l.be.pe&&(l.be.pe=[]);if(void 0!==l.be.pe[b-1])throw new M(`Cannot register multiple constructors with identical number of parameters (${b-
 | 
			
		||||
1}) for class '${l.name}'! Overload resolution is currently only performed using the parameter count, not actual type info!`);l.be.pe[b-1]=()=>{ec(`Cannot construct ${l.name} due to unbound types`,n)};lb([],n,v=>{v.splice(1,0,null);l.be.pe[b-1]=gc(p,v,null,f,k);return[]});return[]})},a:(a,b,c,e,f,k,n,l)=>{var p=hc(c,e);b=L(b);b=ic(b);k=Tb(f,k);lb([],[a],v=>{function w(){ec(`Cannot call ${A} due to unbound types`,p)}v=v[0];var A=`${v.name}.${b}`;b.startsWith("@@")&&(b=Symbol[b.substring(2)]);l&&v.be.pf.push(b);
 | 
			
		||||
var D=v.be.se,I=D[b];void 0===I||void 0===I.fe&&I.className!==v.name&&I.oe===c-2?(w.oe=c-2,w.className=v.name,D[b]=w):(Fb(D,b,A),D[b].fe[c-2]=w);lb([],p,R=>{R=gc(A,R,v,k,n);void 0===D[b].fe?(R.oe=c-2,D[b]=R):D[b].fe[c-2]=R;return[]});return[]})},q:(a,b,c)=>{a=L(a);lb([],[b],e=>{e=e[0];r[a]=e.fromWireType(c);return[]})},ld:a=>kb(a,nc),j:(a,b,c,e)=>{function f(){}b=L(b);f.values={};kb(a,{name:b,constructor:f,fromWireType:function(k){return this.constructor.values[k]},toWireType:(k,n)=>n.value,je:8,
 | 
			
		||||
readValueFromPointer:oc(b,c,e),ke:null});Gb(b,f)},b:(a,b,c)=>{var e=pc(a,"enum");b=L(b);a=e.constructor;e=Object.create(e.constructor.prototype,{value:{value:c},constructor:{value:Eb(`${e.name}_${b}`,function(){})}});a.values[c]=e;a[b]=e},S:(a,b,c)=>{b=L(b);kb(a,{name:b,fromWireType:e=>e,toWireType:(e,f)=>f,je:8,readValueFromPointer:qc(b,c),ke:null})},w:(a,b,c,e,f,k)=>{var n=hc(b,c);a=L(a);a=ic(a);f=Tb(e,f);Gb(a,function(){ec(`Cannot call ${a} due to unbound types`,n)},b-1);lb([],n,l=>{l=[l[0],null].concat(l.slice(1));
 | 
			
		||||
Qb(a,gc(a,l,null,f,k),b-1);return[]})},C:(a,b,c,e,f)=>{b=L(b);-1===f&&(f=4294967295);f=l=>l;if(0===e){var k=32-8*c;f=l=>l<<k>>>k}var n=b.includes("unsigned")?function(l,p){return p>>>0}:function(l,p){return p};kb(a,{name:b,fromWireType:f,toWireType:n,je:8,readValueFromPointer:rc(b,c,0!==e),ke:null})},p:(a,b,c)=>{function e(k){return new f(Ca.buffer,H[k+4>>2],H[k>>2])}var f=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array][b];c=L(c);kb(a,{name:c,fromWireType:e,
 | 
			
		||||
je:8,readValueFromPointer:e},{lf:!0})},o:(a,b,c,e,f,k,n,l,p,v,w,A)=>{c=L(c);k=Tb(f,k);l=Tb(n,l);v=Tb(p,v);A=Tb(w,A);lb([a],[b],D=>{D=D[0];return[new Pb(c,D.be,!1,!1,!0,D,e,k,l,v,A)]})},R:(a,b)=>{b=L(b);var c="std::string"===b;kb(a,{name:b,fromWireType:function(e){var f=H[e>>2],k=e+4;if(c)for(var n=k,l=0;l<=f;++l){var p=k+l;if(l==f||0==B[p]){n=n?cb(B,n,p-n):"";if(void 0===v)var v=n;else v+=String.fromCharCode(0),v+=n;n=p+1}}else{v=Array(f);for(l=0;l<f;++l)v[l]=String.fromCharCode(B[k+l]);v=v.join("")}cc(e);
 | 
			
		||||
return v},toWireType:function(e,f){f instanceof ArrayBuffer&&(f=new Uint8Array(f));var k="string"==typeof f;if(!(k||f instanceof Uint8Array||f instanceof Uint8ClampedArray||f instanceof Int8Array))throw new M("Cannot pass non-string to std::string");var n=c&&k?qa(f):f.length;var l=pd(4+n+1),p=l+4;H[l>>2]=n;if(c&&k)ra(f,p,n+1);else if(k)for(k=0;k<n;++k){var v=f.charCodeAt(k);if(255<v)throw cc(p),new M("String has UTF-16 code units that do not fit in 8 bits");B[p+k]=v}else for(k=0;k<n;++k)B[p+k]=f[k];
 | 
			
		||||
null!==e&&e.push(cc,l);return l},je:8,readValueFromPointer:fb,ke(e){cc(e)}})},M:(a,b,c)=>{c=L(c);if(2===b){var e=tc;var f=uc;var k=vc;var n=l=>Fa[l>>1]}else 4===b&&(e=wc,f=xc,k=yc,n=l=>H[l>>2]);kb(a,{name:c,fromWireType:l=>{for(var p=H[l>>2],v,w=l+4,A=0;A<=p;++A){var D=l+4+A*b;if(A==p||0==n(D))w=e(w,D-w),void 0===v?v=w:(v+=String.fromCharCode(0),v+=w),w=D+b}cc(l);return v},toWireType:(l,p)=>{if("string"!=typeof p)throw new M(`Cannot pass non-string to C++ string type ${c}`);var v=k(p),w=pd(4+v+b);
 | 
			
		||||
H[w>>2]=v/b;f(p,w+4,v+b);null!==l&&l.push(cc,w);return w},je:8,readValueFromPointer:fb,ke(l){cc(l)}})},A:(a,b,c,e,f,k)=>{db[a]={name:L(b),Le:Tb(c,e),ne:Tb(f,k),Qe:[]}},d:(a,b,c,e,f,k,n,l,p,v)=>{db[a].Qe.push({ef:L(b),kf:c,hf:Tb(e,f),jf:k,sf:n,rf:Tb(l,p),tf:v})},kd:(a,b)=>{b=L(b);kb(a,{yf:!0,name:b,je:0,fromWireType:()=>{},toWireType:()=>{}})},jd:()=>1,id:()=>{throw Infinity;},E:(a,b,c)=>{a=mc(a);b=pc(b,"emval::as");return zc(b,c,a)},L:(a,b,c,e)=>{a=Ac[a];b=mc(b);return a(null,b,c,e)},t:(a,b,c,e,f)=>
 | 
			
		||||
{a=Ac[a];b=mc(b);c=Cc(c);return a(b,b[c],e,f)},c:lc,K:a=>{if(0===a)return Nb(Dc());a=Cc(a);return Nb(Dc()[a])},n:(a,b,c)=>{var e=Fc(a,b),f=e.shift();a--;var k=Array(a);b=`methodCaller<(${e.map(n=>n.name).join(", ")}) => ${f.name}>`;return Ec(Eb(b,(n,l,p,v)=>{for(var w=0,A=0;A<a;++A)k[A]=e[A].readValueFromPointer(v+w),w+=e[A].je;n=1===c?Gc(l,k):l.apply(n,k);return zc(f,p,n)}))},y:(a,b)=>{a=mc(a);b=mc(b);return Nb(a[b])},H:a=>{9<a&&(kc[a+1]+=1)},G:()=>Nb([]),f:a=>Nb(Cc(a)),D:()=>Nb({}),hd:a=>{a=mc(a);
 | 
			
		||||
return!a},m:a=>{var b=mc(a);eb(b);lc(a)},h:(a,b,c)=>{a=mc(a);b=mc(b);c=mc(c);a[b]=c},g:(a,b)=>{a=pc(a,"_emval_take_value");a=a.readValueFromPointer(b);return Nb(a)},X:function(){return-52},W:function(){},gd:(a,b,c,e)=>{var f=(new Date).getFullYear(),k=(new Date(f,0,1)).getTimezoneOffset();f=(new Date(f,6,1)).getTimezoneOffset();H[a>>2]=60*Math.max(k,f);E[b>>2]=Number(k!=f);b=n=>{var l=Math.abs(n);return`UTC${0<=n?"-":"+"}${String(Math.floor(l/60)).padStart(2,"0")}${String(l%60).padStart(2,"0")}`};
 | 
			
		||||
a=b(k);b=b(f);f<k?(ra(a,c,17),ra(b,e,17)):(ra(a,e,17),ra(b,c,17))},fd:()=>performance.now(),ed:a=>Q.activeTexture(a),dd:(a,b)=>{Q.attachShader(Nc[a],Qc[b])},cd:(a,b)=>{Q.beginQuery(a,Sc[b])},bd:(a,b)=>{Q.me.beginQueryEXT(a,Sc[b])},ad:(a,b,c)=>{Q.bindAttribLocation(Nc[a],b,c?cb(B,c):"")},$c:(a,b)=>{35051==a?Q.Ie=b:35052==a&&(Q.re=b);Q.bindBuffer(a,Mc[b])},_c:cd,Zc:(a,b)=>{Q.bindRenderbuffer(a,Pc[b])},Yc:(a,b)=>{Q.bindSampler(a,Tc[b])},Xc:(a,b)=>{Q.bindTexture(a,ka[b])},Wc:dd,Vc:dd,Uc:(a,b,c,e)=>Q.blendColor(a,
 | 
			
		||||
b,c,e),Tc:a=>Q.blendEquation(a),Sc:(a,b)=>Q.blendFunc(a,b),Rc:(a,b,c,e,f,k,n,l,p,v)=>Q.blitFramebuffer(a,b,c,e,f,k,n,l,p,v),Qc:(a,b,c,e)=>{2<=z.version?c&&b?Q.bufferData(a,B,e,c,b):Q.bufferData(a,b,e):Q.bufferData(a,c?B.subarray(c,c+b):b,e)},Pc:(a,b,c,e)=>{2<=z.version?c&&Q.bufferSubData(a,b,B,e,c):Q.bufferSubData(a,b,B.subarray(e,e+c))},Oc:a=>Q.checkFramebufferStatus(a),Nc:ed,Mc:fd,Lc:gd,Kc:(a,b,c,e)=>Q.clientWaitSync(Uc[a],b,(c>>>0)+4294967296*e),Jc:(a,b,c,e)=>{Q.colorMask(!!a,!!b,!!c,!!e)},Ic:a=>
 | 
			
		||||
{Q.compileShader(Qc[a])},Hc:(a,b,c,e,f,k,n,l)=>{2<=z.version?Q.re||!n?Q.compressedTexImage2D(a,b,c,e,f,k,n,l):Q.compressedTexImage2D(a,b,c,e,f,k,B,l,n):Q.compressedTexImage2D(a,b,c,e,f,k,B.subarray(l,l+n))},Gc:(a,b,c,e,f,k,n,l,p)=>{2<=z.version?Q.re||!l?Q.compressedTexSubImage2D(a,b,c,e,f,k,n,l,p):Q.compressedTexSubImage2D(a,b,c,e,f,k,n,B,p,l):Q.compressedTexSubImage2D(a,b,c,e,f,k,n,B.subarray(p,p+l))},Fc:(a,b,c,e,f)=>Q.copyBufferSubData(a,b,c,e,f),Ec:(a,b,c,e,f,k,n,l)=>Q.copyTexSubImage2D(a,b,c,
 | 
			
		||||
e,f,k,n,l),Dc:()=>{var a=ja(Nc),b=Q.createProgram();b.name=a;b.Ge=b.Ee=b.Fe=0;b.Me=1;Nc[a]=b;return a},Cc:a=>{var b=ja(Qc);Qc[b]=Q.createShader(a);return b},Bc:a=>Q.cullFace(a),Ac:(a,b)=>{for(var c=0;c<a;c++){var e=E[b+4*c>>2],f=Mc[e];f&&(Q.deleteBuffer(f),f.name=0,Mc[e]=null,e==Q.Ie&&(Q.Ie=0),e==Q.re&&(Q.re=0))}},zc:(a,b)=>{for(var c=0;c<a;++c){var e=E[b+4*c>>2],f=Oc[e];f&&(Q.deleteFramebuffer(f),f.name=0,Oc[e]=null)}},yc:a=>{if(a){var b=Nc[a];b?(Q.deleteProgram(b),b.name=0,Nc[a]=null):S||=1281}},
 | 
			
		||||
xc:(a,b)=>{for(var c=0;c<a;c++){var e=E[b+4*c>>2],f=Sc[e];f&&(Q.deleteQuery(f),Sc[e]=null)}},wc:(a,b)=>{for(var c=0;c<a;c++){var e=E[b+4*c>>2],f=Sc[e];f&&(Q.me.deleteQueryEXT(f),Sc[e]=null)}},vc:(a,b)=>{for(var c=0;c<a;c++){var e=E[b+4*c>>2],f=Pc[e];f&&(Q.deleteRenderbuffer(f),f.name=0,Pc[e]=null)}},uc:(a,b)=>{for(var c=0;c<a;c++){var e=E[b+4*c>>2],f=Tc[e];f&&(Q.deleteSampler(f),f.name=0,Tc[e]=null)}},tc:a=>{if(a){var b=Qc[a];b?(Q.deleteShader(b),Qc[a]=null):S||=1281}},sc:a=>{if(a){var b=Uc[a];b?
 | 
			
		||||
(Q.deleteSync(b),b.name=0,Uc[a]=null):S||=1281}},rc:(a,b)=>{for(var c=0;c<a;c++){var e=E[b+4*c>>2],f=ka[e];f&&(Q.deleteTexture(f),f.name=0,ka[e]=null)}},qc:hd,pc:hd,oc:a=>{Q.depthMask(!!a)},nc:a=>Q.disable(a),mc:a=>{Q.disableVertexAttribArray(a)},lc:(a,b,c)=>{Q.drawArrays(a,b,c)},kc:(a,b,c,e)=>{Q.drawArraysInstanced(a,b,c,e)},jc:(a,b,c,e,f)=>{Q.Pe.drawArraysInstancedBaseInstanceWEBGL(a,b,c,e,f)},ic:(a,b)=>{for(var c=jd[a],e=0;e<a;e++)c[e]=E[b+4*e>>2];Q.drawBuffers(c)},hc:(a,b,c,e)=>{Q.drawElements(a,
 | 
			
		||||
b,c,e)},gc:(a,b,c,e,f)=>{Q.drawElementsInstanced(a,b,c,e,f)},fc:(a,b,c,e,f,k,n)=>{Q.Pe.drawElementsInstancedBaseVertexBaseInstanceWEBGL(a,b,c,e,f,k,n)},ec:(a,b,c,e,f,k)=>{Q.drawElements(a,e,f,k)},dc:a=>Q.enable(a),cc:a=>{Q.enableVertexAttribArray(a)},bc:a=>Q.endQuery(a),ac:a=>{Q.me.endQueryEXT(a)},$b:(a,b)=>(a=Q.fenceSync(a,b))?(b=ja(Uc),a.name=b,Uc[b]=a,b):0,_b:()=>Q.finish(),Zb:()=>Q.flush(),Yb:(a,b,c,e)=>{Q.framebufferRenderbuffer(a,b,c,Pc[e])},Xb:(a,b,c,e,f)=>{Q.framebufferTexture2D(a,b,c,ka[e],
 | 
			
		||||
f)},Wb:a=>Q.frontFace(a),Vb:(a,b)=>{$c(a,b,"createBuffer",Mc)},Ub:(a,b)=>{$c(a,b,"createFramebuffer",Oc)},Tb:(a,b)=>{$c(a,b,"createQuery",Sc)},Sb:(a,b)=>{for(var c=0;c<a;c++){var e=Q.me.createQueryEXT();if(!e){for(S||=1282;c<a;)E[b+4*c++>>2]=0;break}var f=ja(Sc);e.name=f;Sc[f]=e;E[b+4*c>>2]=f}},Rb:(a,b)=>{$c(a,b,"createRenderbuffer",Pc)},Qb:(a,b)=>{$c(a,b,"createSampler",Tc)},Pb:(a,b)=>{$c(a,b,"createTexture",ka)},Ob:kd,Nb:kd,Mb:a=>Q.generateMipmap(a),Lb:(a,b,c)=>{c?E[c>>2]=Q.getBufferParameter(a,
 | 
			
		||||
b):S||=1281},Kb:()=>{var a=Q.getError()||S;S=0;return a},Jb:(a,b)=>md(a,b,2),Ib:(a,b,c,e)=>{a=Q.getFramebufferAttachmentParameter(a,b,c);if(a instanceof WebGLRenderbuffer||a instanceof WebGLTexture)a=a.name|0;E[e>>2]=a},Hb:nd,Gb:(a,b,c,e)=>{a=Q.getProgramInfoLog(Nc[a]);null===a&&(a="(unknown error)");b=0<b&&e?ra(a,e,b):0;c&&(E[c>>2]=b)},Fb:(a,b,c)=>{if(c)if(a>=Lc)S||=1281;else if(a=Nc[a],35716==b)a=Q.getProgramInfoLog(a),null===a&&(a="(unknown error)"),E[c>>2]=a.length+1;else if(35719==b){if(!a.Ge){var e=
 | 
			
		||||
Q.getProgramParameter(a,35718);for(b=0;b<e;++b)a.Ge=Math.max(a.Ge,Q.getActiveUniform(a,b).name.length+1)}E[c>>2]=a.Ge}else if(35722==b){if(!a.Ee)for(e=Q.getProgramParameter(a,35721),b=0;b<e;++b)a.Ee=Math.max(a.Ee,Q.getActiveAttrib(a,b).name.length+1);E[c>>2]=a.Ee}else if(35381==b){if(!a.Fe)for(e=Q.getProgramParameter(a,35382),b=0;b<e;++b)a.Fe=Math.max(a.Fe,Q.getActiveUniformBlockName(a,b).length+1);E[c>>2]=a.Fe}else E[c>>2]=Q.getProgramParameter(a,b);else S||=1281},Eb:od,Db:od,Cb:(a,b,c)=>{if(c){a=
 | 
			
		||||
Q.getQueryParameter(Sc[a],b);var e;"boolean"==typeof a?e=a?1:0:e=a;E[c>>2]=e}else S||=1281},Bb:(a,b,c)=>{if(c){a=Q.me.getQueryObjectEXT(Sc[a],b);var e;"boolean"==typeof a?e=a?1:0:e=a;E[c>>2]=e}else S||=1281},Ab:(a,b,c)=>{c?E[c>>2]=Q.getQuery(a,b):S||=1281},zb:(a,b,c)=>{c?E[c>>2]=Q.me.getQueryEXT(a,b):S||=1281},yb:(a,b,c)=>{c?E[c>>2]=Q.getRenderbufferParameter(a,b):S||=1281},xb:(a,b,c,e)=>{a=Q.getShaderInfoLog(Qc[a]);null===a&&(a="(unknown error)");b=0<b&&e?ra(a,e,b):0;c&&(E[c>>2]=b)},wb:(a,b,c,e)=>
 | 
			
		||||
{a=Q.getShaderPrecisionFormat(a,b);E[c>>2]=a.rangeMin;E[c+4>>2]=a.rangeMax;E[e>>2]=a.precision},vb:(a,b,c)=>{c?35716==b?(a=Q.getShaderInfoLog(Qc[a]),null===a&&(a="(unknown error)"),E[c>>2]=a?a.length+1:0):35720==b?(a=Q.getShaderSource(Qc[a]),E[c>>2]=a?a.length+1:0):E[c>>2]=Q.getShaderParameter(Qc[a],b):S||=1281},ub:rd,tb:sd,sb:(a,b)=>{b=b?cb(B,b):"";if(a=Nc[a]){var c=a,e=c.xe,f=c.Ue,k;if(!e){c.xe=e={};c.Te={};var n=Q.getProgramParameter(c,35718);for(k=0;k<n;++k){var l=Q.getActiveUniform(c,k);var p=
 | 
			
		||||
l.name;l=l.size;var v=td(p);v=0<v?p.slice(0,v):p;var w=c.Me;c.Me+=l;f[v]=[l,w];for(p=0;p<l;++p)e[w]=p,c.Te[w++]=v}}c=a.xe;e=0;f=b;k=td(b);0<k&&(e=parseInt(b.slice(k+1))>>>0,f=b.slice(0,k));if((f=a.Ue[f])&&e<f[0]&&(e+=f[1],c[e]=c[e]||Q.getUniformLocation(a,b)))return e}else S||=1281;return-1},rb:(a,b,c)=>{for(var e=jd[b],f=0;f<b;f++)e[f]=E[c+4*f>>2];Q.invalidateFramebuffer(a,e)},qb:(a,b,c,e,f,k,n)=>{for(var l=jd[b],p=0;p<b;p++)l[p]=E[c+4*p>>2];Q.invalidateSubFramebuffer(a,l,e,f,k,n)},pb:a=>Q.isSync(Uc[a]),
 | 
			
		||||
ob:a=>(a=ka[a])?Q.isTexture(a):0,nb:a=>Q.lineWidth(a),mb:a=>{a=Nc[a];Q.linkProgram(a);a.xe=0;a.Ue={}},lb:(a,b,c,e,f,k)=>{Q.Re.multiDrawArraysInstancedBaseInstanceWEBGL(a,E,b>>2,E,c>>2,E,e>>2,H,f>>2,k)},kb:(a,b,c,e,f,k,n,l)=>{Q.Re.multiDrawElementsInstancedBaseVertexBaseInstanceWEBGL(a,E,b>>2,c,E,e>>2,E,f>>2,E,k>>2,H,n>>2,l)},jb:(a,b)=>{3317==a?Yc=b:3314==a&&(Zc=b);Q.pixelStorei(a,b)},ib:(a,b)=>{Q.me.queryCounterEXT(Sc[a],b)},hb:a=>Q.readBuffer(a),gb:(a,b,c,e,f,k,n)=>{if(2<=z.version)if(Q.Ie)Q.readPixels(a,
 | 
			
		||||
b,c,e,f,k,n);else{var l=ud(k);n>>>=31-Math.clz32(l.BYTES_PER_ELEMENT);Q.readPixels(a,b,c,e,f,k,l,n)}else(l=vd(k,f,c,e,n))?Q.readPixels(a,b,c,e,f,k,l):S||=1280},fb:(a,b,c,e)=>Q.renderbufferStorage(a,b,c,e),eb:(a,b,c,e,f)=>Q.renderbufferStorageMultisample(a,b,c,e,f),db:(a,b,c)=>{Q.samplerParameterf(Tc[a],b,c)},cb:(a,b,c)=>{Q.samplerParameteri(Tc[a],b,c)},bb:(a,b,c)=>{Q.samplerParameteri(Tc[a],b,E[c>>2])},ab:(a,b,c,e)=>Q.scissor(a,b,c,e),$a:(a,b,c,e)=>{for(var f="",k=0;k<b;++k){var n=(n=H[c+4*k>>2])?
 | 
			
		||||
cb(B,n,e?H[e+4*k>>2]:void 0):"";f+=n}Q.shaderSource(Qc[a],f)},_a:(a,b,c)=>Q.stencilFunc(a,b,c),Za:(a,b,c,e)=>Q.stencilFuncSeparate(a,b,c,e),Ya:a=>Q.stencilMask(a),Xa:(a,b)=>Q.stencilMaskSeparate(a,b),Wa:(a,b,c)=>Q.stencilOp(a,b,c),Va:(a,b,c,e)=>Q.stencilOpSeparate(a,b,c,e),Ua:(a,b,c,e,f,k,n,l,p)=>{if(2<=z.version){if(Q.re){Q.texImage2D(a,b,c,e,f,k,n,l,p);return}if(p){var v=ud(l);p>>>=31-Math.clz32(v.BYTES_PER_ELEMENT);Q.texImage2D(a,b,c,e,f,k,n,l,v,p);return}}v=p?vd(l,n,e,f,p):null;Q.texImage2D(a,
 | 
			
		||||
b,c,e,f,k,n,l,v)},Ta:(a,b,c)=>Q.texParameterf(a,b,c),Sa:(a,b,c)=>{Q.texParameterf(a,b,J[c>>2])},Ra:(a,b,c)=>Q.texParameteri(a,b,c),Qa:(a,b,c)=>{Q.texParameteri(a,b,E[c>>2])},Pa:(a,b,c,e,f)=>Q.texStorage2D(a,b,c,e,f),Oa:(a,b,c,e,f,k,n,l,p)=>{if(2<=z.version){if(Q.re){Q.texSubImage2D(a,b,c,e,f,k,n,l,p);return}if(p){var v=ud(l);Q.texSubImage2D(a,b,c,e,f,k,n,l,v,p>>>31-Math.clz32(v.BYTES_PER_ELEMENT));return}}p=p?vd(l,n,f,k,p):null;Q.texSubImage2D(a,b,c,e,f,k,n,l,p)},Na:(a,b)=>{Q.uniform1f(V(a),b)},Ma:(a,
 | 
			
		||||
b,c)=>{if(2<=z.version)b&&Q.uniform1fv(V(a),J,c>>2,b);else{if(288>=b)for(var e=wd[b],f=0;f<b;++f)e[f]=J[c+4*f>>2];else e=J.subarray(c>>2,c+4*b>>2);Q.uniform1fv(V(a),e)}},La:(a,b)=>{Q.uniform1i(V(a),b)},Ka:(a,b,c)=>{if(2<=z.version)b&&Q.uniform1iv(V(a),E,c>>2,b);else{if(288>=b)for(var e=xd[b],f=0;f<b;++f)e[f]=E[c+4*f>>2];else e=E.subarray(c>>2,c+4*b>>2);Q.uniform1iv(V(a),e)}},Ja:(a,b,c)=>{Q.uniform2f(V(a),b,c)},Ia:(a,b,c)=>{if(2<=z.version)b&&Q.uniform2fv(V(a),J,c>>2,2*b);else{if(144>=b){b*=2;for(var e=
 | 
			
		||||
wd[b],f=0;f<b;f+=2)e[f]=J[c+4*f>>2],e[f+1]=J[c+(4*f+4)>>2]}else e=J.subarray(c>>2,c+8*b>>2);Q.uniform2fv(V(a),e)}},Ha:(a,b,c)=>{Q.uniform2i(V(a),b,c)},Ga:(a,b,c)=>{if(2<=z.version)b&&Q.uniform2iv(V(a),E,c>>2,2*b);else{if(144>=b){b*=2;for(var e=xd[b],f=0;f<b;f+=2)e[f]=E[c+4*f>>2],e[f+1]=E[c+(4*f+4)>>2]}else e=E.subarray(c>>2,c+8*b>>2);Q.uniform2iv(V(a),e)}},Fa:(a,b,c,e)=>{Q.uniform3f(V(a),b,c,e)},Ea:(a,b,c)=>{if(2<=z.version)b&&Q.uniform3fv(V(a),J,c>>2,3*b);else{if(96>=b){b*=3;for(var e=wd[b],f=0;f<
 | 
			
		||||
b;f+=3)e[f]=J[c+4*f>>2],e[f+1]=J[c+(4*f+4)>>2],e[f+2]=J[c+(4*f+8)>>2]}else e=J.subarray(c>>2,c+12*b>>2);Q.uniform3fv(V(a),e)}},Da:(a,b,c,e)=>{Q.uniform3i(V(a),b,c,e)},Ca:(a,b,c)=>{if(2<=z.version)b&&Q.uniform3iv(V(a),E,c>>2,3*b);else{if(96>=b){b*=3;for(var e=xd[b],f=0;f<b;f+=3)e[f]=E[c+4*f>>2],e[f+1]=E[c+(4*f+4)>>2],e[f+2]=E[c+(4*f+8)>>2]}else e=E.subarray(c>>2,c+12*b>>2);Q.uniform3iv(V(a),e)}},Ba:(a,b,c,e,f)=>{Q.uniform4f(V(a),b,c,e,f)},Aa:(a,b,c)=>{if(2<=z.version)b&&Q.uniform4fv(V(a),J,c>>2,4*
 | 
			
		||||
b);else{if(72>=b){var e=wd[4*b],f=J;c>>=2;b*=4;for(var k=0;k<b;k+=4){var n=c+k;e[k]=f[n];e[k+1]=f[n+1];e[k+2]=f[n+2];e[k+3]=f[n+3]}}else e=J.subarray(c>>2,c+16*b>>2);Q.uniform4fv(V(a),e)}},za:(a,b,c,e,f)=>{Q.uniform4i(V(a),b,c,e,f)},ya:(a,b,c)=>{if(2<=z.version)b&&Q.uniform4iv(V(a),E,c>>2,4*b);else{if(72>=b){b*=4;for(var e=xd[b],f=0;f<b;f+=4)e[f]=E[c+4*f>>2],e[f+1]=E[c+(4*f+4)>>2],e[f+2]=E[c+(4*f+8)>>2],e[f+3]=E[c+(4*f+12)>>2]}else e=E.subarray(c>>2,c+16*b>>2);Q.uniform4iv(V(a),e)}},xa:(a,b,c,e)=>
 | 
			
		||||
{if(2<=z.version)b&&Q.uniformMatrix2fv(V(a),!!c,J,e>>2,4*b);else{if(72>=b){b*=4;for(var f=wd[b],k=0;k<b;k+=4)f[k]=J[e+4*k>>2],f[k+1]=J[e+(4*k+4)>>2],f[k+2]=J[e+(4*k+8)>>2],f[k+3]=J[e+(4*k+12)>>2]}else f=J.subarray(e>>2,e+16*b>>2);Q.uniformMatrix2fv(V(a),!!c,f)}},wa:(a,b,c,e)=>{if(2<=z.version)b&&Q.uniformMatrix3fv(V(a),!!c,J,e>>2,9*b);else{if(32>=b){b*=9;for(var f=wd[b],k=0;k<b;k+=9)f[k]=J[e+4*k>>2],f[k+1]=J[e+(4*k+4)>>2],f[k+2]=J[e+(4*k+8)>>2],f[k+3]=J[e+(4*k+12)>>2],f[k+4]=J[e+(4*k+16)>>2],f[k+
 | 
			
		||||
5]=J[e+(4*k+20)>>2],f[k+6]=J[e+(4*k+24)>>2],f[k+7]=J[e+(4*k+28)>>2],f[k+8]=J[e+(4*k+32)>>2]}else f=J.subarray(e>>2,e+36*b>>2);Q.uniformMatrix3fv(V(a),!!c,f)}},va:(a,b,c,e)=>{if(2<=z.version)b&&Q.uniformMatrix4fv(V(a),!!c,J,e>>2,16*b);else{if(18>=b){var f=wd[16*b],k=J;e>>=2;b*=16;for(var n=0;n<b;n+=16){var l=e+n;f[n]=k[l];f[n+1]=k[l+1];f[n+2]=k[l+2];f[n+3]=k[l+3];f[n+4]=k[l+4];f[n+5]=k[l+5];f[n+6]=k[l+6];f[n+7]=k[l+7];f[n+8]=k[l+8];f[n+9]=k[l+9];f[n+10]=k[l+10];f[n+11]=k[l+11];f[n+12]=k[l+12];f[n+
 | 
			
		||||
13]=k[l+13];f[n+14]=k[l+14];f[n+15]=k[l+15]}}else f=J.subarray(e>>2,e+64*b>>2);Q.uniformMatrix4fv(V(a),!!c,f)}},ua:a=>{a=Nc[a];Q.useProgram(a);Q.bf=a},ta:(a,b)=>Q.vertexAttrib1f(a,b),sa:(a,b)=>{Q.vertexAttrib2f(a,J[b>>2],J[b+4>>2])},ra:(a,b)=>{Q.vertexAttrib3f(a,J[b>>2],J[b+4>>2],J[b+8>>2])},qa:(a,b)=>{Q.vertexAttrib4f(a,J[b>>2],J[b+4>>2],J[b+8>>2],J[b+12>>2])},pa:(a,b)=>{Q.vertexAttribDivisor(a,b)},oa:(a,b,c,e,f)=>{Q.vertexAttribIPointer(a,b,c,e,f)},na:(a,b,c,e,f,k)=>{Q.vertexAttribPointer(a,b,c,
 | 
			
		||||
!!e,f,k)},ma:(a,b,c,e)=>Q.viewport(a,b,c,e),la:(a,b,c,e)=>{Q.waitSync(Uc[a],b,(c>>>0)+4294967296*e)},ka:a=>{var b=B.length;a>>>=0;if(2147483648<a)return!1;for(var c=1;4>=c;c*=2){var e=b*(1+1/c);e=Math.min(e,a+100663296);a:{e=(Math.min(2147483648,65536*Math.ceil(Math.max(a,e)/65536))-za.buffer.byteLength+65535)/65536|0;try{za.grow(e);Ha();var f=1;break a}catch(k){}f=void 0}if(f)return!0}return!1},ja:()=>z?z.handle:0,qd:(a,b)=>{var c=0;Ad().forEach((e,f)=>{var k=b+c;f=H[a+4*f>>2]=k;for(k=0;k<e.length;++k)Ca[f++]=
 | 
			
		||||
e.charCodeAt(k);Ca[f]=0;c+=e.length+1});return 0},pd:(a,b)=>{var c=Ad();H[a>>2]=c.length;var e=0;c.forEach(f=>e+=f.length+1);H[b>>2]=e;return 0},ia:a=>{throw new Va(a);},N:()=>52,_:function(){return 52},od:()=>52,Z:function(){return 70},T:(a,b,c,e)=>{for(var f=0,k=0;k<c;k++){var n=H[b>>2],l=H[b+4>>2];b+=8;for(var p=0;p<l;p++){var v=B[n+p],w=Bd[a];0===v||10===v?((1===a?wa:ya)(cb(w)),w.length=0):w.push(v)}f+=l}H[e>>2]=f;return 0},ha:cd,ga:ed,fa:fd,ea:gd,J:nd,Q:rd,da:sd,k:Hd,u:Id,i:Jd,I:Kd,ca:Ld,P:Md,
 | 
			
		||||
O:Nd,s:Od,x:Pd,r:Qd,v:Rd,ba:Sd,aa:Td,$:Ud},Z=function(){var a={a:Vd};La++;Ra??=r.locateFile?Qa("canvaskit.wasm")?"canvaskit.wasm":ta+"canvaskit.wasm":(new URL("canvaskit.wasm",import.meta.url)).href;Ua(a,function(b){Z=b.instance.exports;za=Z.wd;Ha();O=Z.zd;Ja.unshift(Z.xd);La--;0==La&&(null!==Ma&&(clearInterval(Ma),Ma=null),Oa&&(b=Oa,Oa=null,b()))}).catch(ca);return{}}(),bc=a=>(bc=Z.yd)(a),pd=r._malloc=a=>(pd=r._malloc=Z.Ad)(a),cc=r._free=a=>(cc=r._free=Z.Bd)(a),Wd=(a,b)=>(Wd=Z.Cd)(a,b),
 | 
			
		||||
Xd=a=>(Xd=Z.Dd)(a),Yd=()=>(Yd=Z.Ed)();r.dynCall_viji=(a,b,c,e,f)=>(r.dynCall_viji=Z.Fd)(a,b,c,e,f);r.dynCall_vijiii=(a,b,c,e,f,k,n)=>(r.dynCall_vijiii=Z.Gd)(a,b,c,e,f,k,n);r.dynCall_viiiiij=(a,b,c,e,f,k,n,l)=>(r.dynCall_viiiiij=Z.Hd)(a,b,c,e,f,k,n,l);r.dynCall_iiiji=(a,b,c,e,f,k)=>(r.dynCall_iiiji=Z.Id)(a,b,c,e,f,k);r.dynCall_jii=(a,b,c)=>(r.dynCall_jii=Z.Jd)(a,b,c);r.dynCall_vij=(a,b,c,e)=>(r.dynCall_vij=Z.Kd)(a,b,c,e);r.dynCall_jiiiiii=(a,b,c,e,f,k,n)=>(r.dynCall_jiiiiii=Z.Ld)(a,b,c,e,f,k,n);
 | 
			
		||||
r.dynCall_jiiiiji=(a,b,c,e,f,k,n,l)=>(r.dynCall_jiiiiji=Z.Md)(a,b,c,e,f,k,n,l);r.dynCall_ji=(a,b)=>(r.dynCall_ji=Z.Nd)(a,b);r.dynCall_iijj=(a,b,c,e,f,k)=>(r.dynCall_iijj=Z.Od)(a,b,c,e,f,k);r.dynCall_iiji=(a,b,c,e,f)=>(r.dynCall_iiji=Z.Pd)(a,b,c,e,f);r.dynCall_iijjiii=(a,b,c,e,f,k,n,l,p)=>(r.dynCall_iijjiii=Z.Qd)(a,b,c,e,f,k,n,l,p);r.dynCall_iij=(a,b,c,e)=>(r.dynCall_iij=Z.Rd)(a,b,c,e);r.dynCall_vijjjii=(a,b,c,e,f,k,n,l,p,v)=>(r.dynCall_vijjjii=Z.Sd)(a,b,c,e,f,k,n,l,p,v);
 | 
			
		||||
r.dynCall_jiji=(a,b,c,e,f)=>(r.dynCall_jiji=Z.Td)(a,b,c,e,f);r.dynCall_viijii=(a,b,c,e,f,k,n)=>(r.dynCall_viijii=Z.Ud)(a,b,c,e,f,k,n);r.dynCall_iiiiij=(a,b,c,e,f,k,n)=>(r.dynCall_iiiiij=Z.Vd)(a,b,c,e,f,k,n);r.dynCall_iiiiijj=(a,b,c,e,f,k,n,l,p)=>(r.dynCall_iiiiijj=Z.Wd)(a,b,c,e,f,k,n,l,p);r.dynCall_iiiiiijj=(a,b,c,e,f,k,n,l,p,v)=>(r.dynCall_iiiiiijj=Z.Xd)(a,b,c,e,f,k,n,l,p,v);function Rd(a,b,c,e,f){var k=Yd();try{O.get(a)(b,c,e,f)}catch(n){Xd(k);if(n!==n+0)throw n;Wd(1,0)}}
 | 
			
		||||
function Id(a,b,c){var e=Yd();try{return O.get(a)(b,c)}catch(f){Xd(e);if(f!==f+0)throw f;Wd(1,0)}}function Pd(a,b,c){var e=Yd();try{O.get(a)(b,c)}catch(f){Xd(e);if(f!==f+0)throw f;Wd(1,0)}}function Hd(a,b){var c=Yd();try{return O.get(a)(b)}catch(e){Xd(c);if(e!==e+0)throw e;Wd(1,0)}}function Od(a,b){var c=Yd();try{O.get(a)(b)}catch(e){Xd(c);if(e!==e+0)throw e;Wd(1,0)}}function Jd(a,b,c,e){var f=Yd();try{return O.get(a)(b,c,e)}catch(k){Xd(f);if(k!==k+0)throw k;Wd(1,0)}}
 | 
			
		||||
function Ud(a,b,c,e,f,k,n,l,p,v){var w=Yd();try{O.get(a)(b,c,e,f,k,n,l,p,v)}catch(A){Xd(w);if(A!==A+0)throw A;Wd(1,0)}}function Qd(a,b,c,e){var f=Yd();try{O.get(a)(b,c,e)}catch(k){Xd(f);if(k!==k+0)throw k;Wd(1,0)}}function Td(a,b,c,e,f,k,n){var l=Yd();try{O.get(a)(b,c,e,f,k,n)}catch(p){Xd(l);if(p!==p+0)throw p;Wd(1,0)}}function Md(a,b,c,e,f,k,n,l){var p=Yd();try{return O.get(a)(b,c,e,f,k,n,l)}catch(v){Xd(p);if(v!==v+0)throw v;Wd(1,0)}}
 | 
			
		||||
function Sd(a,b,c,e,f,k){var n=Yd();try{O.get(a)(b,c,e,f,k)}catch(l){Xd(n);if(l!==l+0)throw l;Wd(1,0)}}function Kd(a,b,c,e,f){var k=Yd();try{return O.get(a)(b,c,e,f)}catch(n){Xd(k);if(n!==n+0)throw n;Wd(1,0)}}function Nd(a,b,c,e,f,k,n,l,p,v){var w=Yd();try{return O.get(a)(b,c,e,f,k,n,l,p,v)}catch(A){Xd(w);if(A!==A+0)throw A;Wd(1,0)}}function Ld(a,b,c,e,f,k,n){var l=Yd();try{return O.get(a)(b,c,e,f,k,n)}catch(p){Xd(l);if(p!==p+0)throw p;Wd(1,0)}}var Zd,$d;Oa=function ae(){Zd||be();Zd||(Oa=ae)};
 | 
			
		||||
function be(){if(!(0<La)){if(!$d&&($d=1,Wa(Ia),0<La))return;Zd||(Zd=1,r.calledRun=1,Aa||(Wa(Ja),aa(r),r.onRuntimeInitialized?.(),Wa(Ka)))}}be();moduleRtn=da;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  return moduleRtn;
 | 
			
		||||
}
 | 
			
		||||
);
 | 
			
		||||
})();
 | 
			
		||||
export default CanvasKitInit;
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							@@ -127,9 +127,29 @@
 | 
			
		||||
    <img class="center" aria-hidden="true" src="splash/img/light-1x.png" alt="">
 | 
			
		||||
</picture>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
{{flutter_js}}
 | 
			
		||||
{{flutter_build_config}}
 | 
			
		||||
 | 
			
		||||
<script src="flutter_bootstrap.js" async=""></script>
 | 
			
		||||
const searchParams = new URLSearchParams(window.location.search);
 | 
			
		||||
const renderer = searchParams.get('renderer');
 | 
			
		||||
let cdn = searchParams.get('cdn');
 | 
			
		||||
 | 
			
		||||
if (cdn) {
 | 
			
		||||
  localStorage.setItem('sn-web-canvaskit-cdn', cdn);
 | 
			
		||||
} else {
 | 
			
		||||
  const storagedCdn = localStorage.getItem('sn-web-canvaskit-cdn');
 | 
			
		||||
  cdn = storagedCdn ?? 'com';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
_flutter.loader.load({
 | 
			
		||||
  config: {
 | 
			
		||||
    renderer: renderer ?? 'canvaskit',
 | 
			
		||||
    canvasKitVariant: 'full',
 | 
			
		||||
    canvasKitBaseUrl: `https://www.gstatic.${cdn}/flutter-canvaskit/f73bfc4522dd0bc87bbcdb4bb3088082755c5e87`,
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
		Reference in New Issue
	
	Block a user