Compare commits
	
		
			4 Commits
		
	
	
		
			54c098c274
			...
			92f7e92018
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 92f7e92018 | |||
| 5c483bd3b8 | |||
| 1c510d63fe | |||
| 115cb4adc1 | 
@@ -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",
 | 
			
		||||
 
 | 
			
		||||
@@ -137,6 +137,11 @@
 | 
			
		||||
  "publisherRunBy": "由 {} 管理",
 | 
			
		||||
  "fieldPublisherBelongToRealm": "所属领域",
 | 
			
		||||
  "fieldPublisherBelongToRealmUnset": "未设置发布者所属领域",
 | 
			
		||||
  "writePost": "撰写",
 | 
			
		||||
  "postTypeStory": "动态",
 | 
			
		||||
  "postTypeArticle": "文章",
 | 
			
		||||
  "postTypeQuestion": "问题",
 | 
			
		||||
  "postTypeVideo": "视频",
 | 
			
		||||
  "writePostTypeStory": "发动态",
 | 
			
		||||
  "writePostTypeArticle": "写文章",
 | 
			
		||||
  "writePostTypeQuestion": "提问题",
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
@@ -237,14 +244,18 @@ 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)) {
 | 
			
		||||
        if (post.preload?.thumbnail != null &&
 | 
			
		||||
            (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
 | 
			
		||||
          thumbnail = PostWriteMedia(post.preload!.thumbnail);
 | 
			
		||||
        }
 | 
			
		||||
        if (post.preload?.realm != null) {
 | 
			
		||||
@@ -272,7 +283,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 +293,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 +311,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 +325,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 +352,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 +389,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 +417,12 @@ class PostWriteController extends ChangeNotifier {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool get isNotEmpty =>
 | 
			
		||||
      title.isNotEmpty ||
 | 
			
		||||
      description.isNotEmpty ||
 | 
			
		||||
      contentController.text.isNotEmpty ||
 | 
			
		||||
      attachments.isNotEmpty;
 | 
			
		||||
 | 
			
		||||
  bool temporaryRestored = false;
 | 
			
		||||
 | 
			
		||||
  void _temporaryLoad() {
 | 
			
		||||
@@ -403,18 +435,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;
 | 
			
		||||
@@ -463,7 +501,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 +512,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) {
 | 
			
		||||
@@ -518,16 +562,23 @@ 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,
 | 
			
		||||
@@ -536,11 +587,14 @@ class PostWriteController extends ChangeNotifier {
 | 
			
		||||
          if (realm != null) 'realm': realm!.id,
 | 
			
		||||
        },
 | 
			
		||||
        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(
 | 
			
		||||
@@ -683,7 +737,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();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -66,10 +66,10 @@ final _appRoutes = [
 | 
			
		||||
    builder: (context, state) => const ExploreScreen(),
 | 
			
		||||
    routes: [
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/write/:mode',
 | 
			
		||||
        path: '/write',
 | 
			
		||||
        name: 'postEditor',
 | 
			
		||||
        builder: (context, state) => PostEditorScreen(
 | 
			
		||||
          mode: state.pathParameters['mode']!,
 | 
			
		||||
          mode: state.uri.queryParameters['mode'],
 | 
			
		||||
          postEditId: int.tryParse(
 | 
			
		||||
            state.uri.queryParameters['editing'] ?? '',
 | 
			
		||||
          ),
 | 
			
		||||
 
 | 
			
		||||
@@ -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(
 | 
			
		||||
 
 | 
			
		||||
@@ -111,7 +111,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 +119,24 @@ 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),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
          Row(
 | 
			
		||||
            children: [
 | 
			
		||||
              Text('writePostTypeArticle').tr(),
 | 
			
		||||
              const Gap(20),
 | 
			
		||||
              FloatingActionButton(
 | 
			
		||||
                heroTag: null,
 | 
			
		||||
                tooltip: 'writePostTypeArticle'.tr(),
 | 
			
		||||
                onPressed: () {
 | 
			
		||||
                  GoRouter.of(context).pushNamed('postEditor', pathParameters: {
 | 
			
		||||
                    'mode': 'articles',
 | 
			
		||||
                  }).then((value) {
 | 
			
		||||
                    if (value == true) {
 | 
			
		||||
                      refreshPosts();
 | 
			
		||||
                    }
 | 
			
		||||
                  });
 | 
			
		||||
                  _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.edit),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
  );
 | 
			
		||||
@@ -209,6 +214,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void dispose() {
 | 
			
		||||
    _tabController.dispose();
 | 
			
		||||
    _writeController.dispose();
 | 
			
		||||
    if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) {
 | 
			
		||||
      hotKeyManager.unregister(_pasteHotKey);
 | 
			
		||||
@@ -220,14 +226,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,26 +263,10 @@ 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(
 | 
			
		||||
@@ -283,6 +275,24 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
 | 
			
		||||
              ),
 | 
			
		||||
              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: [
 | 
			
		||||
@@ -374,7 +384,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 +406,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
 | 
			
		||||
                            onTapRealm: _showRealmPopup,
 | 
			
		||||
                          ),
 | 
			
		||||
                        _ => const Placeholder(),
 | 
			
		||||
                      })
 | 
			
		||||
                          .padding(top: 8),
 | 
			
		||||
                      },
 | 
			
		||||
                    ),
 | 
			
		||||
                    if (_writeController.attachments.isNotEmpty ||
 | 
			
		||||
                        _writeController.thumbnail != null)
 | 
			
		||||
@@ -720,7 +729,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 +978,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 +1062,7 @@ class _PostQuestionEditor extends StatelessWidget {
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ).padding(top: 8),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1154,7 +1163,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,
 | 
			
		||||
 
 | 
			
		||||
@@ -61,7 +61,7 @@ class _AppSharingListenerState extends State<AppSharingListener> {
 | 
			
		||||
                      onTap: () {
 | 
			
		||||
                        GoRouter.of(context).pushNamed(
 | 
			
		||||
                          'postEditor',
 | 
			
		||||
                          pathParameters: {
 | 
			
		||||
                          queryParameters: {
 | 
			
		||||
                            'mode': 'stories',
 | 
			
		||||
                          },
 | 
			
		||||
                          extra: PostEditorExtra(
 | 
			
		||||
 
 | 
			
		||||
@@ -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,36 @@ 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 +176,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(),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
@@ -189,16 +213,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 +242,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 +282,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;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -149,7 +149,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 +159,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,
 | 
			
		||||
@@ -507,6 +506,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,
 | 
			
		||||
@@ -1037,8 +1038,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 +1068,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 +1086,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((_) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user