Compare commits
	
		
			5 Commits
		
	
	
		
			2.3.2+64
			...
			f0a3bbe023
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| f0a3bbe023 | |||
| df81c84438 | |||
| 8b12395fca | |||
| cb2b71d194 | |||
| 7ed508e2bb | 
@@ -12,9 +12,9 @@ post {
 | 
			
		||||
 | 
			
		||||
body:json {
 | 
			
		||||
  {
 | 
			
		||||
    "alias": "Meltdown",
 | 
			
		||||
    "name": "Meltdown",
 | 
			
		||||
    "attachment_id": "IpDPHEbWDDCbBofX",
 | 
			
		||||
    "pack_id": 4
 | 
			
		||||
    "alias": "BaLoading",
 | 
			
		||||
    "name": "BaLoading",
 | 
			
		||||
    "attachment_id": "2JCI2uh21mKkfk9P",
 | 
			
		||||
    "pack_id": 3
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -155,6 +155,7 @@
 | 
			
		||||
  "writePostTypeStory": "Post a story",
 | 
			
		||||
  "writePostTypeArticle": "Write an article",
 | 
			
		||||
  "writePostTypeQuestion": "Ask a question",
 | 
			
		||||
  "writePostTypeVideo": "Post a video",
 | 
			
		||||
  "fieldPostPublisher": "Post publisher",
 | 
			
		||||
  "fieldPostContent": "What happened?!",
 | 
			
		||||
  "fieldPostTitle": "Title",
 | 
			
		||||
@@ -617,5 +618,6 @@
 | 
			
		||||
  "postQuestionUnansweredWithReward": "Unanswered Question, reward source points {}",
 | 
			
		||||
  "postQuestionAnswered": "Answered Question",
 | 
			
		||||
  "postQuestionAnswerSelect": "Select as Answer",
 | 
			
		||||
  "postQuestionAnswerSelected": "Answer has been selected, reward has been applied."
 | 
			
		||||
  "postQuestionAnswerSelected": "Answer has been selected, reward has been applied.",
 | 
			
		||||
  "postVideoUpload": "Upload Video"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -139,6 +139,7 @@
 | 
			
		||||
  "writePostTypeStory": "发动态",
 | 
			
		||||
  "writePostTypeArticle": "写文章",
 | 
			
		||||
  "writePostTypeQuestion": "提问题",
 | 
			
		||||
  "writePostTypeVideo": "发视频",
 | 
			
		||||
  "fieldPostPublisher": "帖子发布者",
 | 
			
		||||
  "fieldPostContent": "发生什么事了?!",
 | 
			
		||||
  "fieldPostTitle": "标题",
 | 
			
		||||
@@ -616,5 +617,6 @@
 | 
			
		||||
  "postQuestionAnswered": "已解答的问题",
 | 
			
		||||
  "postQuestionAnswerTitle": "精选解答",
 | 
			
		||||
  "postQuestionAnswerSelect": "选择解答",
 | 
			
		||||
  "postQuestionAnswerSelected": "解答已选择,奖励已发放。"
 | 
			
		||||
  "postQuestionAnswerSelected": "解答已选择,奖励已发放。",
 | 
			
		||||
  "postVideoUpload": "上传视频"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,6 @@ PODS:
 | 
			
		||||
  - Alamofire (5.10.2)
 | 
			
		||||
  - connectivity_plus (0.0.1):
 | 
			
		||||
    - Flutter
 | 
			
		||||
    - FlutterMacOS
 | 
			
		||||
  - croppy (0.0.1):
 | 
			
		||||
    - Flutter
 | 
			
		||||
  - device_info_plus (0.0.1):
 | 
			
		||||
@@ -180,7 +179,7 @@ PODS:
 | 
			
		||||
  - in_app_review (2.0.0):
 | 
			
		||||
    - Flutter
 | 
			
		||||
  - Kingfisher (8.2.0)
 | 
			
		||||
  - livekit_client (2.3.5):
 | 
			
		||||
  - livekit_client (2.3.6):
 | 
			
		||||
    - Flutter
 | 
			
		||||
    - flutter_webrtc
 | 
			
		||||
    - WebRTC-SDK (= 125.6422.06)
 | 
			
		||||
@@ -237,7 +236,7 @@ PODS:
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES:
 | 
			
		||||
  - Alamofire
 | 
			
		||||
  - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
 | 
			
		||||
  - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
 | 
			
		||||
  - croppy (from `.symlinks/plugins/croppy/ios`)
 | 
			
		||||
  - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
 | 
			
		||||
  - file_picker (from `.symlinks/plugins/file_picker/ios`)
 | 
			
		||||
@@ -300,7 +299,7 @@ SPEC REPOS:
 | 
			
		||||
 | 
			
		||||
EXTERNAL SOURCES:
 | 
			
		||||
  connectivity_plus:
 | 
			
		||||
    :path: ".symlinks/plugins/connectivity_plus/darwin"
 | 
			
		||||
    :path: ".symlinks/plugins/connectivity_plus/ios"
 | 
			
		||||
  croppy:
 | 
			
		||||
    :path: ".symlinks/plugins/croppy/ios"
 | 
			
		||||
  device_info_plus:
 | 
			
		||||
@@ -374,7 +373,7 @@ EXTERNAL SOURCES:
 | 
			
		||||
 | 
			
		||||
SPEC CHECKSUMS:
 | 
			
		||||
  Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
 | 
			
		||||
  connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
 | 
			
		||||
  connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
 | 
			
		||||
  croppy: b6199bc8d56bd2e03cc11609d1c47ad9875c1321
 | 
			
		||||
  device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
 | 
			
		||||
  DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
 | 
			
		||||
@@ -404,7 +403,7 @@ SPEC CHECKSUMS:
 | 
			
		||||
  image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
 | 
			
		||||
  in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
 | 
			
		||||
  Kingfisher: 323e5c4ec7983aaace12af655a7b51a7f88a599d
 | 
			
		||||
  livekit_client: dcc5fd47ba69c98fc6baeb12e862c9d43807d976
 | 
			
		||||
  livekit_client: 148b2cf67a09aaf475ba8e5bf1667fe10dc35f81
 | 
			
		||||
  media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
 | 
			
		||||
  media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
 | 
			
		||||
  media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
 | 
			
		||||
 
 | 
			
		||||
@@ -145,6 +145,7 @@ class PostWriteController extends ChangeNotifier {
 | 
			
		||||
    'stories': 'writePostTypeStory',
 | 
			
		||||
    'articles': 'writePostTypeArticle',
 | 
			
		||||
    'questions': 'writePostTypeQuestion',
 | 
			
		||||
    'videos': 'writePostTypeVideo',
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static const kAttachmentProgressWeight = 0.9;
 | 
			
		||||
@@ -197,6 +198,7 @@ class PostWriteController extends ChangeNotifier {
 | 
			
		||||
  PostWriteMedia? thumbnail;
 | 
			
		||||
  List<PostWriteMedia> attachments = List.empty(growable: true);
 | 
			
		||||
  DateTime? publishedAt, publishedUntil;
 | 
			
		||||
  SnAttachment? videoAttachment;
 | 
			
		||||
 | 
			
		||||
  Future<void> fetchRelatedPost(
 | 
			
		||||
    BuildContext context, {
 | 
			
		||||
@@ -218,6 +220,7 @@ class PostWriteController extends ChangeNotifier {
 | 
			
		||||
        contentController.text = post.body['content'] ?? '';
 | 
			
		||||
        aliasController.text = post.alias ?? '';
 | 
			
		||||
        rewardController.text = post.body['reward']?.toString() ?? '';
 | 
			
		||||
        videoAttachment = post.preload?.video;
 | 
			
		||||
        publishedAt = post.publishedAt;
 | 
			
		||||
        publishedUntil = post.publishedUntil;
 | 
			
		||||
        visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
 | 
			
		||||
@@ -507,6 +510,7 @@ class PostWriteController extends ChangeNotifier {
 | 
			
		||||
          if (replyingPost != null) 'reply_to': replyingPost!.id,
 | 
			
		||||
          if (repostingPost != null) 'repost_to': repostingPost!.id,
 | 
			
		||||
          if (reward != null) 'reward': reward,
 | 
			
		||||
          if (videoAttachment != null) 'video': videoAttachment!.rid,
 | 
			
		||||
        },
 | 
			
		||||
        onSendProgress: (count, total) {
 | 
			
		||||
          progress = baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
 | 
			
		||||
@@ -633,6 +637,11 @@ class PostWriteController extends ChangeNotifier {
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void setVideoAttachment(SnAttachment? value) {
 | 
			
		||||
    videoAttachment = value;
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void reset() {
 | 
			
		||||
    publishedAt = null;
 | 
			
		||||
    publishedUntil = null;
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,9 @@ class SnPostContentProvider {
 | 
			
		||||
      if (out[i].body['thumbnail'] != null) {
 | 
			
		||||
        rids.add(out[i].body['thumbnail']);
 | 
			
		||||
      }
 | 
			
		||||
      if (out[i].body['video'] != null) {
 | 
			
		||||
        rids.add(out[i].body['video']);
 | 
			
		||||
      }
 | 
			
		||||
      if (out[i].repostTo != null) {
 | 
			
		||||
        out[i] = out[i].copyWith(
 | 
			
		||||
          repostTo: await _preloadRelatedDataSingle(out[i].repostTo!),
 | 
			
		||||
@@ -36,6 +39,7 @@ class SnPostContentProvider {
 | 
			
		||||
        preload: SnPostPreload(
 | 
			
		||||
          thumbnail: attachments.where((ele) => ele?.rid == out[i].body['thumbnail']).firstOrNull,
 | 
			
		||||
          attachments: attachments.where((ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false).toList(),
 | 
			
		||||
          video: attachments.where((ele) => ele?.rid == out[i].body['video']).firstOrNull,
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
@@ -53,6 +57,9 @@ class SnPostContentProvider {
 | 
			
		||||
    if (out.body['thumbnail'] != null) {
 | 
			
		||||
      rids.add(out.body['thumbnail']);
 | 
			
		||||
    }
 | 
			
		||||
    if (out.body['video'] != null) {
 | 
			
		||||
      rids.add(out.body['video']);
 | 
			
		||||
    }
 | 
			
		||||
    if (out.repostTo != null) {
 | 
			
		||||
      out = out.copyWith(
 | 
			
		||||
        repostTo: await _preloadRelatedDataSingle(out.repostTo!),
 | 
			
		||||
@@ -64,6 +71,7 @@ class SnPostContentProvider {
 | 
			
		||||
      preload: SnPostPreload(
 | 
			
		||||
        thumbnail: attachments.where((ele) => ele?.rid == out.body['thumbnail']).firstOrNull,
 | 
			
		||||
        attachments: attachments.where((ele) => out.body['attachments']?.contains(ele?.rid) ?? false).toList(),
 | 
			
		||||
        video: attachments.where((ele) => ele?.rid == out.body['video']).firstOrNull,
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -192,11 +192,6 @@ final _appRoutes = [
 | 
			
		||||
      child: const RealmScreen(),
 | 
			
		||||
    ),
 | 
			
		||||
    routes: [
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/:alias',
 | 
			
		||||
        name: 'realmDetail',
 | 
			
		||||
        builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!),
 | 
			
		||||
      ),
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/manage',
 | 
			
		||||
        name: 'realmManage',
 | 
			
		||||
@@ -204,6 +199,11 @@ final _appRoutes = [
 | 
			
		||||
          editingRealmAlias: state.uri.queryParameters['editing'],
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/:alias',
 | 
			
		||||
        name: 'realmDetail',
 | 
			
		||||
        builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!),
 | 
			
		||||
      ),
 | 
			
		||||
    ],
 | 
			
		||||
  ),
 | 
			
		||||
  GoRoute(path: '/news', name: 'news', builder: (context, state) => const NewsScreen(), routes: [
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
import 'package:collection/collection.dart';
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:dropdown_button2/dropdown_button2.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
@@ -17,6 +18,7 @@ import 'package:uuid/uuid.dart';
 | 
			
		||||
 | 
			
		||||
class ChatManageScreen extends StatefulWidget {
 | 
			
		||||
  final String? editingChannelAlias;
 | 
			
		||||
 | 
			
		||||
  const ChatManageScreen({super.key, this.editingChannelAlias});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@@ -33,6 +35,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
			
		||||
  List<SnRealm>? _realms;
 | 
			
		||||
  SnRealm? _belongToRealm;
 | 
			
		||||
 | 
			
		||||
  SnChannel? _editingChannel;
 | 
			
		||||
 | 
			
		||||
  Future<void> _fetchRealms() async {
 | 
			
		||||
    setState(() => _isBusy = true);
 | 
			
		||||
    try {
 | 
			
		||||
@@ -41,6 +45,9 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
			
		||||
      _realms = List<SnRealm>.from(
 | 
			
		||||
        resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
 | 
			
		||||
      );
 | 
			
		||||
      if (_editingChannel != null) {
 | 
			
		||||
        _belongToRealm = _realms?.firstWhereOrNull((e) => e.id == _editingChannel!.realmId);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (mounted) context.showErrorDialog(err);
 | 
			
		||||
    } finally {
 | 
			
		||||
@@ -48,8 +55,6 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  SnChannel? _editingChannel;
 | 
			
		||||
 | 
			
		||||
  Future<void> _fetchChannel() async {
 | 
			
		||||
    setState(() => _isBusy = true);
 | 
			
		||||
 | 
			
		||||
@@ -124,9 +129,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: widget.editingChannelAlias != null
 | 
			
		||||
            ? Text('screenChatManage').tr()
 | 
			
		||||
            : Text('screenChatNew').tr(),
 | 
			
		||||
        title: widget.editingChannelAlias != null ? Text('screenChatManage').tr() : Text('screenChatNew').tr(),
 | 
			
		||||
      ),
 | 
			
		||||
      body: SingleChildScrollView(
 | 
			
		||||
        child: Column(
 | 
			
		||||
@@ -138,8 +141,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
			
		||||
                leadingPadding: const EdgeInsets.only(left: 10, right: 20),
 | 
			
		||||
                dividerColor: Colors.transparent,
 | 
			
		||||
                content: Text(
 | 
			
		||||
                  'channelEditingNotice'
 | 
			
		||||
                      .tr(args: ['#${_editingChannel!.alias}']),
 | 
			
		||||
                  'channelEditingNotice'.tr(args: ['#${_editingChannel!.alias}']),
 | 
			
		||||
                ),
 | 
			
		||||
                actions: [
 | 
			
		||||
                  TextButton(
 | 
			
		||||
@@ -162,6 +164,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
			
		||||
                items: [
 | 
			
		||||
                  ...(_realms?.map(
 | 
			
		||||
                        (SnRealm item) => DropdownMenuItem<SnRealm>(
 | 
			
		||||
                          enabled: _editingChannel == null || _editingChannel?.realmId == item.id,
 | 
			
		||||
                          value: item,
 | 
			
		||||
                          child: Row(
 | 
			
		||||
                            children: [
 | 
			
		||||
@@ -179,15 +182,12 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
			
		||||
                                  mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                                  crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                                  children: [
 | 
			
		||||
                                    Text(item.name).textStyle(Theme.of(context)
 | 
			
		||||
                                        .textTheme
 | 
			
		||||
                                        .bodyMedium!),
 | 
			
		||||
                                    Text(item.name).textStyle(Theme.of(context).textTheme.bodyMedium!),
 | 
			
		||||
                                    Text(
 | 
			
		||||
                                      item.description,
 | 
			
		||||
                                      maxLines: 1,
 | 
			
		||||
                                      overflow: TextOverflow.ellipsis,
 | 
			
		||||
                                    ).textStyle(
 | 
			
		||||
                                        Theme.of(context).textTheme.bodySmall!),
 | 
			
		||||
                                    ).textStyle(Theme.of(context).textTheme.bodySmall!),
 | 
			
		||||
                                  ],
 | 
			
		||||
                                ),
 | 
			
		||||
                              ),
 | 
			
		||||
@@ -197,14 +197,14 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
			
		||||
                      ) ??
 | 
			
		||||
                      []),
 | 
			
		||||
                  DropdownMenuItem<SnRealm>(
 | 
			
		||||
                    enabled: _editingChannel == null,
 | 
			
		||||
                    value: null,
 | 
			
		||||
                    child: Row(
 | 
			
		||||
                      children: [
 | 
			
		||||
                        CircleAvatar(
 | 
			
		||||
                          radius: 16,
 | 
			
		||||
                          backgroundColor: Colors.transparent,
 | 
			
		||||
                          foregroundColor:
 | 
			
		||||
                              Theme.of(context).colorScheme.onSurface,
 | 
			
		||||
                          foregroundColor: Theme.of(context).colorScheme.onSurface,
 | 
			
		||||
                          child: const Icon(Symbols.clear),
 | 
			
		||||
                        ),
 | 
			
		||||
                        const Gap(12),
 | 
			
		||||
@@ -213,9 +213,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
			
		||||
                            mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                            crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                            children: [
 | 
			
		||||
                              Text('fieldChatBelongToRealmUnset')
 | 
			
		||||
                                  .tr()
 | 
			
		||||
                                  .textStyle(
 | 
			
		||||
                              Text('fieldChatBelongToRealmUnset').tr().textStyle(
 | 
			
		||||
                                    Theme.of(context).textTheme.bodyMedium!,
 | 
			
		||||
                                  ),
 | 
			
		||||
                            ],
 | 
			
		||||
@@ -231,10 +229,10 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
			
		||||
                },
 | 
			
		||||
                buttonStyleData: const ButtonStyleData(
 | 
			
		||||
                  padding: EdgeInsets.only(right: 16),
 | 
			
		||||
                  height: 60,
 | 
			
		||||
                  height: 48,
 | 
			
		||||
                ),
 | 
			
		||||
                menuItemStyleData: const MenuItemStyleData(
 | 
			
		||||
                  height: 60,
 | 
			
		||||
                  height: 48,
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
@@ -250,8 +248,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
			
		||||
                    helperText: 'fieldChatAliasHint'.tr(),
 | 
			
		||||
                    helperMaxLines: 2,
 | 
			
		||||
                  ),
 | 
			
		||||
                  onTapOutside: (_) =>
 | 
			
		||||
                      FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                ),
 | 
			
		||||
                const Gap(4),
 | 
			
		||||
                TextField(
 | 
			
		||||
@@ -260,8 +257,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
			
		||||
                    border: const UnderlineInputBorder(),
 | 
			
		||||
                    labelText: 'fieldChatName'.tr(),
 | 
			
		||||
                  ),
 | 
			
		||||
                  onTapOutside: (_) =>
 | 
			
		||||
                      FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                ),
 | 
			
		||||
                const Gap(4),
 | 
			
		||||
                TextField(
 | 
			
		||||
@@ -272,8 +268,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
			
		||||
                    border: const UnderlineInputBorder(),
 | 
			
		||||
                    labelText: 'fieldChatDescription'.tr(),
 | 
			
		||||
                  ),
 | 
			
		||||
                  onTapOutside: (_) =>
 | 
			
		||||
                      FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                ),
 | 
			
		||||
                const Gap(12),
 | 
			
		||||
                Row(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,3 @@
 | 
			
		||||
import 'package:animations/animations.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
 | 
			
		||||
@@ -7,10 +6,8 @@ import 'package:go_router/go_router.dart';
 | 
			
		||||
import 'package:material_symbols_icons/symbols.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/config.dart';
 | 
			
		||||
import 'package:surface/providers/post.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/screens/post/post_detail.dart';
 | 
			
		||||
import 'package:surface/types/post.dart';
 | 
			
		||||
import 'package:surface/widgets/app_bar_leading.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
@@ -97,8 +94,6 @@ class _ExploreScreenState extends State<ExploreScreen> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final cfg = context.read<ConfigProvider>();
 | 
			
		||||
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      floatingActionButtonLocation: ExpandableFab.location,
 | 
			
		||||
      floatingActionButton: ExpandableFab(
 | 
			
		||||
@@ -187,6 +182,27 @@ class _ExploreScreenState extends State<ExploreScreen> {
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
          Row(
 | 
			
		||||
            children: [
 | 
			
		||||
              Text('writePostTypeVideo').tr(),
 | 
			
		||||
              const Gap(20),
 | 
			
		||||
              FloatingActionButton(
 | 
			
		||||
                heroTag: null,
 | 
			
		||||
                tooltip: 'writePostTypeVideo'.tr(),
 | 
			
		||||
                onPressed: () {
 | 
			
		||||
                  GoRouter.of(context).pushNamed('postEditor', pathParameters: {
 | 
			
		||||
                    'mode': 'videos',
 | 
			
		||||
                  }).then((value) {
 | 
			
		||||
                    if (value == true) {
 | 
			
		||||
                      _refreshPosts();
 | 
			
		||||
                    }
 | 
			
		||||
                  });
 | 
			
		||||
                  _fabKey.currentState!.toggle();
 | 
			
		||||
                },
 | 
			
		||||
                child: const Icon(Symbols.video_call),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
      body: RefreshIndicator(
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:flutter/gestures.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter/services.dart';
 | 
			
		||||
import 'package:flutter_context_menu/flutter_context_menu.dart';
 | 
			
		||||
import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:hotkey_manager/hotkey_manager.dart';
 | 
			
		||||
import 'package:material_symbols_icons/symbols.dart';
 | 
			
		||||
@@ -14,10 +15,14 @@ import 'package:responsive_framework/responsive_framework.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
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/types/attachment.dart';
 | 
			
		||||
import 'package:surface/types/post.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
import 'package:surface/widgets/attachment/attachment_item.dart';
 | 
			
		||||
import 'package:surface/widgets/attachment/pending_attachment_alt.dart';
 | 
			
		||||
import 'package:surface/widgets/attachment/pending_attachment_boost.dart';
 | 
			
		||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
			
		||||
import 'package:surface/widgets/markdown_content.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
@@ -25,6 +30,9 @@ import 'package:surface/widgets/post/post_media_pending_list.dart';
 | 
			
		||||
import 'package:surface/widgets/post/post_meta_editor.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:uuid/uuid.dart';
 | 
			
		||||
 | 
			
		||||
import '../../widgets/attachment/attachment_input.dart';
 | 
			
		||||
 | 
			
		||||
class PostEditorExtra {
 | 
			
		||||
  final String? text;
 | 
			
		||||
@@ -243,6 +251,10 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
 | 
			
		||||
                            controller: _writeController,
 | 
			
		||||
                            onTapPublisher: _showPublisherPopup,
 | 
			
		||||
                          ),
 | 
			
		||||
                        'videos' => _PostVideoEditor(
 | 
			
		||||
                            controller: _writeController,
 | 
			
		||||
                            onTapPublisher: _showPublisherPopup,
 | 
			
		||||
                          ),
 | 
			
		||||
                        _ => const Placeholder(),
 | 
			
		||||
                      },
 | 
			
		||||
                    ),
 | 
			
		||||
@@ -687,3 +699,226 @@ class _PostQuestionEditor extends StatelessWidget {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _PostVideoEditor extends StatelessWidget {
 | 
			
		||||
  final PostWriteController controller;
 | 
			
		||||
  final Function? onTapPublisher;
 | 
			
		||||
 | 
			
		||||
  const _PostVideoEditor({required this.controller, this.onTapPublisher});
 | 
			
		||||
 | 
			
		||||
  void _selectVideo(BuildContext context) async {
 | 
			
		||||
    final video = await showDialog<SnAttachment?>(
 | 
			
		||||
      context: context,
 | 
			
		||||
      builder: (context) => AttachmentInputDialog(
 | 
			
		||||
        title: 'postVideoUpload'.tr(),
 | 
			
		||||
        pool: 'interactive',
 | 
			
		||||
        mediaType: SnMediaType.video,
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
    if (!context.mounted) return;
 | 
			
		||||
    if (video == null) return;
 | 
			
		||||
    controller.setVideoAttachment(video);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _setAlt(BuildContext context) async {
 | 
			
		||||
    if (controller.videoAttachment == null) return;
 | 
			
		||||
 | 
			
		||||
    final result = await showDialog<SnAttachment?>(
 | 
			
		||||
      context: context,
 | 
			
		||||
      builder: (context) => PendingAttachmentAltDialog(media: PostWriteMedia(controller.videoAttachment)),
 | 
			
		||||
    );
 | 
			
		||||
    if (result == null) return;
 | 
			
		||||
 | 
			
		||||
    controller.setVideoAttachment(result);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _createBoost(BuildContext context) async {
 | 
			
		||||
    if (controller.videoAttachment == null) return;
 | 
			
		||||
 | 
			
		||||
    final result = await showDialog<SnAttachmentBoost?>(
 | 
			
		||||
      context: context,
 | 
			
		||||
      builder: (context) => PendingAttachmentBoostDialog(media: PostWriteMedia(controller.videoAttachment)),
 | 
			
		||||
    );
 | 
			
		||||
    if (result == null) return;
 | 
			
		||||
 | 
			
		||||
    final newAttach = controller.videoAttachment!.copyWith(
 | 
			
		||||
      boosts: [...controller.videoAttachment!.boosts, result],
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    controller.setVideoAttachment(newAttach);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _setThumbnail(BuildContext context) async {
 | 
			
		||||
    if (controller.videoAttachment == null) return;
 | 
			
		||||
 | 
			
		||||
    final thumbnail = await showDialog<SnAttachment?>(
 | 
			
		||||
      context: context,
 | 
			
		||||
      builder: (context) => AttachmentInputDialog(
 | 
			
		||||
        title: 'attachmentSetThumbnail'.tr(),
 | 
			
		||||
        pool: 'interactive',
 | 
			
		||||
        analyzeNow: true,
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
    if (thumbnail == null) return;
 | 
			
		||||
    if (!context.mounted) return;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      final attach = context.read<SnAttachmentProvider>();
 | 
			
		||||
      final newAttach = await attach.updateOne(
 | 
			
		||||
        controller.videoAttachment!,
 | 
			
		||||
        thumbnailId: thumbnail.id,
 | 
			
		||||
      );
 | 
			
		||||
      controller.setVideoAttachment(newAttach);
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!context.mounted) return;
 | 
			
		||||
      context.showErrorDialog(err);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _deleteAttachment(BuildContext context) async {
 | 
			
		||||
    if (controller.videoAttachment == null) return;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
      await sn.client.delete('/cgi/uc/attachments/${controller.videoAttachment!.id}');
 | 
			
		||||
      controller.setVideoAttachment(null);
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!context.mounted) return;
 | 
			
		||||
      context.showErrorDialog(err);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Column(
 | 
			
		||||
      children: [
 | 
			
		||||
        Material(
 | 
			
		||||
          color: Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
			
		||||
          child: InkWell(
 | 
			
		||||
            child: Row(
 | 
			
		||||
              children: [
 | 
			
		||||
                AccountImage(content: controller.publisher?.avatar, radius: 20),
 | 
			
		||||
                const Gap(8),
 | 
			
		||||
                Expanded(
 | 
			
		||||
                  child: Column(
 | 
			
		||||
                    crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                    children: [
 | 
			
		||||
                      Text(controller.publisher?.nick ?? 'loading'.tr()).bold(),
 | 
			
		||||
                      Text('@${controller.publisher?.name}'),
 | 
			
		||||
                    ],
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ).padding(horizontal: 12, vertical: 8),
 | 
			
		||||
            onTap: () {
 | 
			
		||||
              onTapPublisher?.call();
 | 
			
		||||
            },
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        const Gap(16),
 | 
			
		||||
        TextField(
 | 
			
		||||
          controller: controller.titleController,
 | 
			
		||||
          decoration: InputDecoration.collapsed(
 | 
			
		||||
            hintText: 'fieldPostTitle'.tr(),
 | 
			
		||||
            border: InputBorder.none,
 | 
			
		||||
          ),
 | 
			
		||||
          style: Theme.of(context).textTheme.titleLarge,
 | 
			
		||||
          onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
        ).padding(horizontal: 16),
 | 
			
		||||
        const Gap(8),
 | 
			
		||||
        TextField(
 | 
			
		||||
          controller: controller.descriptionController,
 | 
			
		||||
          decoration: InputDecoration.collapsed(
 | 
			
		||||
            hintText: 'fieldPostDescription'.tr(),
 | 
			
		||||
            border: InputBorder.none,
 | 
			
		||||
          ),
 | 
			
		||||
          maxLines: null,
 | 
			
		||||
          keyboardType: TextInputType.multiline,
 | 
			
		||||
          style: Theme.of(context).textTheme.bodyLarge,
 | 
			
		||||
          onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
        ).padding(horizontal: 16),
 | 
			
		||||
        const Gap(12),
 | 
			
		||||
        Container(
 | 
			
		||||
          margin: const EdgeInsets.only(left: 16, right: 16),
 | 
			
		||||
          decoration: BoxDecoration(
 | 
			
		||||
            borderRadius: BorderRadius.circular(16),
 | 
			
		||||
            border: Border.all(color: Theme.of(context).dividerColor),
 | 
			
		||||
          ),
 | 
			
		||||
          child: ContextMenuRegion(
 | 
			
		||||
            contextMenu: ContextMenu(
 | 
			
		||||
              entries: [
 | 
			
		||||
                MenuItem(
 | 
			
		||||
                  label: 'attachmentSetAlt'.tr(),
 | 
			
		||||
                  icon: Symbols.description,
 | 
			
		||||
                  onSelected: () {
 | 
			
		||||
                    _setAlt(context);
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                MenuItem(
 | 
			
		||||
                  label: 'attachmentBoost'.tr(),
 | 
			
		||||
                  icon: Symbols.bolt,
 | 
			
		||||
                  onSelected: () {
 | 
			
		||||
                    _createBoost(context);
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                MenuItem(
 | 
			
		||||
                  label: 'attachmentSetThumbnail'.tr(),
 | 
			
		||||
                  icon: Symbols.image,
 | 
			
		||||
                  onSelected: () {
 | 
			
		||||
                    _setThumbnail(context);
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                MenuItem(
 | 
			
		||||
                  label: 'attachmentCopyRandomId'.tr(),
 | 
			
		||||
                  icon: Symbols.content_copy,
 | 
			
		||||
                  onSelected: () {
 | 
			
		||||
                    Clipboard.setData(ClipboardData(text: controller.videoAttachment!.rid));
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                MenuItem(
 | 
			
		||||
                  label: 'delete'.tr(),
 | 
			
		||||
                  icon: Symbols.delete,
 | 
			
		||||
                  onSelected: () => _deleteAttachment(context),
 | 
			
		||||
                ),
 | 
			
		||||
                MenuItem(
 | 
			
		||||
                  label: 'unlink'.tr(),
 | 
			
		||||
                  icon: Symbols.link_off,
 | 
			
		||||
                  onSelected: () {
 | 
			
		||||
                    controller.setVideoAttachment(null);
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
            child: InkWell(
 | 
			
		||||
              borderRadius: BorderRadius.circular(16),
 | 
			
		||||
              onTap: controller.videoAttachment != null ? () => _selectVideo(context) : null,
 | 
			
		||||
              child: AspectRatio(
 | 
			
		||||
                aspectRatio: 16 / 9,
 | 
			
		||||
                child: controller.videoAttachment == null
 | 
			
		||||
                    ? Center(
 | 
			
		||||
                        child: Row(
 | 
			
		||||
                          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                          crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
                          mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
                          children: [
 | 
			
		||||
                            const Icon(Icons.add),
 | 
			
		||||
                            const Gap(4),
 | 
			
		||||
                            Text('postVideoUpload'.tr()),
 | 
			
		||||
                          ],
 | 
			
		||||
                        ),
 | 
			
		||||
                      )
 | 
			
		||||
                    : ClipRRect(
 | 
			
		||||
                        borderRadius: BorderRadius.circular(16),
 | 
			
		||||
                        child: AttachmentItem(
 | 
			
		||||
                          data: controller.videoAttachment!,
 | 
			
		||||
                          heroTag: const Uuid().v4(),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:go_router/go_router.dart';
 | 
			
		||||
import 'package:material_symbols_icons/symbols.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
 
 | 
			
		||||
@@ -89,6 +89,7 @@ class SnPostPreload with _$SnPostPreload {
 | 
			
		||||
  const factory SnPostPreload({
 | 
			
		||||
    required SnAttachment? thumbnail,
 | 
			
		||||
    required List<SnAttachment?>? attachments,
 | 
			
		||||
    required SnAttachment? video,
 | 
			
		||||
  }) = _SnPostPreload;
 | 
			
		||||
 | 
			
		||||
  factory SnPostPreload.fromJson(Map<String, Object?> json) =>
 | 
			
		||||
 
 | 
			
		||||
@@ -1567,6 +1567,7 @@ SnPostPreload _$SnPostPreloadFromJson(Map<String, dynamic> json) {
 | 
			
		||||
mixin _$SnPostPreload {
 | 
			
		||||
  SnAttachment? get thumbnail => throw _privateConstructorUsedError;
 | 
			
		||||
  List<SnAttachment?>? get attachments => throw _privateConstructorUsedError;
 | 
			
		||||
  SnAttachment? get video => throw _privateConstructorUsedError;
 | 
			
		||||
 | 
			
		||||
  /// Serializes this SnPostPreload to a JSON map.
 | 
			
		||||
  Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
 | 
			
		||||
@@ -1584,9 +1585,13 @@ abstract class $SnPostPreloadCopyWith<$Res> {
 | 
			
		||||
          SnPostPreload value, $Res Function(SnPostPreload) then) =
 | 
			
		||||
      _$SnPostPreloadCopyWithImpl<$Res, SnPostPreload>;
 | 
			
		||||
  @useResult
 | 
			
		||||
  $Res call({SnAttachment? thumbnail, List<SnAttachment?>? attachments});
 | 
			
		||||
  $Res call(
 | 
			
		||||
      {SnAttachment? thumbnail,
 | 
			
		||||
      List<SnAttachment?>? attachments,
 | 
			
		||||
      SnAttachment? video});
 | 
			
		||||
 | 
			
		||||
  $SnAttachmentCopyWith<$Res>? get thumbnail;
 | 
			
		||||
  $SnAttachmentCopyWith<$Res>? get video;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
@@ -1606,6 +1611,7 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload>
 | 
			
		||||
  $Res call({
 | 
			
		||||
    Object? thumbnail = freezed,
 | 
			
		||||
    Object? attachments = freezed,
 | 
			
		||||
    Object? video = freezed,
 | 
			
		||||
  }) {
 | 
			
		||||
    return _then(_value.copyWith(
 | 
			
		||||
      thumbnail: freezed == thumbnail
 | 
			
		||||
@@ -1616,6 +1622,10 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload>
 | 
			
		||||
          ? _value.attachments
 | 
			
		||||
          : attachments // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as List<SnAttachment?>?,
 | 
			
		||||
      video: freezed == video
 | 
			
		||||
          ? _value.video
 | 
			
		||||
          : video // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as SnAttachment?,
 | 
			
		||||
    ) as $Val);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -1632,6 +1642,20 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload>
 | 
			
		||||
      return _then(_value.copyWith(thumbnail: value) as $Val);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Create a copy of SnPostPreload
 | 
			
		||||
  /// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
  @override
 | 
			
		||||
  @pragma('vm:prefer-inline')
 | 
			
		||||
  $SnAttachmentCopyWith<$Res>? get video {
 | 
			
		||||
    if (_value.video == null) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return $SnAttachmentCopyWith<$Res>(_value.video!, (value) {
 | 
			
		||||
      return _then(_value.copyWith(video: value) as $Val);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
@@ -1642,10 +1666,15 @@ abstract class _$$SnPostPreloadImplCopyWith<$Res>
 | 
			
		||||
      __$$SnPostPreloadImplCopyWithImpl<$Res>;
 | 
			
		||||
  @override
 | 
			
		||||
  @useResult
 | 
			
		||||
  $Res call({SnAttachment? thumbnail, List<SnAttachment?>? attachments});
 | 
			
		||||
  $Res call(
 | 
			
		||||
      {SnAttachment? thumbnail,
 | 
			
		||||
      List<SnAttachment?>? attachments,
 | 
			
		||||
      SnAttachment? video});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  $SnAttachmentCopyWith<$Res>? get thumbnail;
 | 
			
		||||
  @override
 | 
			
		||||
  $SnAttachmentCopyWith<$Res>? get video;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
@@ -1663,6 +1692,7 @@ class __$$SnPostPreloadImplCopyWithImpl<$Res>
 | 
			
		||||
  $Res call({
 | 
			
		||||
    Object? thumbnail = freezed,
 | 
			
		||||
    Object? attachments = freezed,
 | 
			
		||||
    Object? video = freezed,
 | 
			
		||||
  }) {
 | 
			
		||||
    return _then(_$SnPostPreloadImpl(
 | 
			
		||||
      thumbnail: freezed == thumbnail
 | 
			
		||||
@@ -1673,6 +1703,10 @@ class __$$SnPostPreloadImplCopyWithImpl<$Res>
 | 
			
		||||
          ? _value._attachments
 | 
			
		||||
          : attachments // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as List<SnAttachment?>?,
 | 
			
		||||
      video: freezed == video
 | 
			
		||||
          ? _value.video
 | 
			
		||||
          : video // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as SnAttachment?,
 | 
			
		||||
    ));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1682,7 +1716,8 @@ class __$$SnPostPreloadImplCopyWithImpl<$Res>
 | 
			
		||||
class _$SnPostPreloadImpl implements _SnPostPreload {
 | 
			
		||||
  const _$SnPostPreloadImpl(
 | 
			
		||||
      {required this.thumbnail,
 | 
			
		||||
      required final List<SnAttachment?>? attachments})
 | 
			
		||||
      required final List<SnAttachment?>? attachments,
 | 
			
		||||
      required this.video})
 | 
			
		||||
      : _attachments = attachments;
 | 
			
		||||
 | 
			
		||||
  factory _$SnPostPreloadImpl.fromJson(Map<String, dynamic> json) =>
 | 
			
		||||
@@ -1700,9 +1735,12 @@ class _$SnPostPreloadImpl implements _SnPostPreload {
 | 
			
		||||
    return EqualUnmodifiableListView(value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  final SnAttachment? video;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() {
 | 
			
		||||
    return 'SnPostPreload(thumbnail: $thumbnail, attachments: $attachments)';
 | 
			
		||||
    return 'SnPostPreload(thumbnail: $thumbnail, attachments: $attachments, video: $video)';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@@ -1713,13 +1751,14 @@ class _$SnPostPreloadImpl implements _SnPostPreload {
 | 
			
		||||
            (identical(other.thumbnail, thumbnail) ||
 | 
			
		||||
                other.thumbnail == thumbnail) &&
 | 
			
		||||
            const DeepCollectionEquality()
 | 
			
		||||
                .equals(other._attachments, _attachments));
 | 
			
		||||
                .equals(other._attachments, _attachments) &&
 | 
			
		||||
            (identical(other.video, video) || other.video == video));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
  @override
 | 
			
		||||
  int get hashCode => Object.hash(runtimeType, thumbnail,
 | 
			
		||||
      const DeepCollectionEquality().hash(_attachments));
 | 
			
		||||
      const DeepCollectionEquality().hash(_attachments), video);
 | 
			
		||||
 | 
			
		||||
  /// Create a copy of SnPostPreload
 | 
			
		||||
  /// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
@@ -1740,7 +1779,8 @@ class _$SnPostPreloadImpl implements _SnPostPreload {
 | 
			
		||||
abstract class _SnPostPreload implements SnPostPreload {
 | 
			
		||||
  const factory _SnPostPreload(
 | 
			
		||||
      {required final SnAttachment? thumbnail,
 | 
			
		||||
      required final List<SnAttachment?>? attachments}) = _$SnPostPreloadImpl;
 | 
			
		||||
      required final List<SnAttachment?>? attachments,
 | 
			
		||||
      required final SnAttachment? video}) = _$SnPostPreloadImpl;
 | 
			
		||||
 | 
			
		||||
  factory _SnPostPreload.fromJson(Map<String, dynamic> json) =
 | 
			
		||||
      _$SnPostPreloadImpl.fromJson;
 | 
			
		||||
@@ -1749,6 +1789,8 @@ abstract class _SnPostPreload implements SnPostPreload {
 | 
			
		||||
  SnAttachment? get thumbnail;
 | 
			
		||||
  @override
 | 
			
		||||
  List<SnAttachment?>? get attachments;
 | 
			
		||||
  @override
 | 
			
		||||
  SnAttachment? get video;
 | 
			
		||||
 | 
			
		||||
  /// Create a copy of SnPostPreload
 | 
			
		||||
  /// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
 
 | 
			
		||||
@@ -165,12 +165,16 @@ _$SnPostPreloadImpl _$$SnPostPreloadImplFromJson(Map<String, dynamic> json) =>
 | 
			
		||||
              ? null
 | 
			
		||||
              : SnAttachment.fromJson(e as Map<String, dynamic>))
 | 
			
		||||
          .toList(),
 | 
			
		||||
      video: json['video'] == null
 | 
			
		||||
          ? null
 | 
			
		||||
          : SnAttachment.fromJson(json['video'] as Map<String, dynamic>),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
Map<String, dynamic> _$$SnPostPreloadImplToJson(_$SnPostPreloadImpl instance) =>
 | 
			
		||||
    <String, dynamic>{
 | 
			
		||||
      'thumbnail': instance.thumbnail?.toJson(),
 | 
			
		||||
      'attachments': instance.attachments?.map((e) => e?.toJson()).toList(),
 | 
			
		||||
      'video': instance.video?.toJson(),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
_$SnBodyImpl _$$SnBodyImplFromJson(Map<String, dynamic> json) => _$SnBodyImpl(
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/providers/user_directory.dart';
 | 
			
		||||
import 'package:surface/providers/userinfo.dart';
 | 
			
		||||
import 'package:surface/types/account.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
 | 
			
		||||
@@ -49,10 +50,19 @@ class _AccountSelectState extends State<AccountSelect> {
 | 
			
		||||
  Future<void> _getFriends() async {
 | 
			
		||||
    final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
    final resp = await sn.client.get('/cgi/id/users/me/relations?status=1');
 | 
			
		||||
    if (!mounted) return;
 | 
			
		||||
    final ua = context.read<UserProvider>();
 | 
			
		||||
 | 
			
		||||
    setState(() {
 | 
			
		||||
      _relativeUsers.addAll(
 | 
			
		||||
        resp.data?.map((e) => SnRelationship.fromJson(e)) ?? [],
 | 
			
		||||
        resp.data?.map((e) {
 | 
			
		||||
          final rel = SnRelationship.fromJson(e);
 | 
			
		||||
          if (rel.relatedId == ua.user?.id) {
 | 
			
		||||
            return rel.account!;
 | 
			
		||||
          } else {
 | 
			
		||||
            return rel.related!;
 | 
			
		||||
          }
 | 
			
		||||
        }).cast<SnAccount>(),
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
@@ -123,13 +133,9 @@ class _AccountSelectState extends State<AccountSelect> {
 | 
			
		||||
          ),
 | 
			
		||||
          Expanded(
 | 
			
		||||
            child: ListView.builder(
 | 
			
		||||
              itemCount: _pendingUsers.isEmpty
 | 
			
		||||
                  ? _relativeUsers.length
 | 
			
		||||
                  : _pendingUsers.length,
 | 
			
		||||
              itemCount: _pendingUsers.isEmpty ? _relativeUsers.length : _pendingUsers.length,
 | 
			
		||||
              itemBuilder: (context, index) {
 | 
			
		||||
                var user = _pendingUsers.isEmpty
 | 
			
		||||
                    ? _relativeUsers[index]
 | 
			
		||||
                    : _pendingUsers[index];
 | 
			
		||||
                var user = _pendingUsers.isEmpty ? _relativeUsers[index] : _pendingUsers[index];
 | 
			
		||||
                return ListTile(
 | 
			
		||||
                  title: Text(user.nick),
 | 
			
		||||
                  subtitle: Text(user.name),
 | 
			
		||||
@@ -148,8 +154,7 @@ class _AccountSelectState extends State<AccountSelect> {
 | 
			
		||||
                          }
 | 
			
		||||
 | 
			
		||||
                          setState(() {
 | 
			
		||||
                            final idx = _selectedUsers
 | 
			
		||||
                                .indexWhere((x) => x.id == user.id);
 | 
			
		||||
                            final idx = _selectedUsers.indexWhere((x) => x.id == user.id);
 | 
			
		||||
                            if (idx != -1) {
 | 
			
		||||
                              _selectedUsers.removeAt(idx);
 | 
			
		||||
                            } else {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,12 +6,22 @@ import 'package:material_symbols_icons/symbols.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/sn_attachment.dart';
 | 
			
		||||
import 'package:surface/types/attachment.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
 | 
			
		||||
class AttachmentInputDialog extends StatefulWidget {
 | 
			
		||||
  final String? title;
 | 
			
		||||
final bool? analyzeNow;
 | 
			
		||||
  const AttachmentInputDialog({super.key, required this.title, this.analyzeNow = false});
 | 
			
		||||
  final bool? analyzeNow;
 | 
			
		||||
  final SnMediaType? mediaType;
 | 
			
		||||
  final String pool;
 | 
			
		||||
 | 
			
		||||
  const AttachmentInputDialog({
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.title,
 | 
			
		||||
    required this.pool,
 | 
			
		||||
    this.analyzeNow = false,
 | 
			
		||||
    this.mediaType = SnMediaType.image,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<AttachmentInputDialog> createState() => _AttachmentInputDialogState();
 | 
			
		||||
@@ -20,13 +30,18 @@ final bool? analyzeNow;
 | 
			
		||||
class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
 | 
			
		||||
  final _randomIdController = TextEditingController();
 | 
			
		||||
 | 
			
		||||
  XFile? _thumbnailFile;
 | 
			
		||||
  XFile? _file;
 | 
			
		||||
  double? _progress;
 | 
			
		||||
 | 
			
		||||
  void _pickImage() async {
 | 
			
		||||
  void _pickMedia() async {
 | 
			
		||||
    final picker = ImagePicker();
 | 
			
		||||
    final result = await picker.pickImage(source: ImageSource.gallery);
 | 
			
		||||
    final result = switch (widget.mediaType) {
 | 
			
		||||
      SnMediaType.image => await picker.pickImage(source: ImageSource.gallery),
 | 
			
		||||
      SnMediaType.video => await picker.pickVideo(source: ImageSource.gallery),
 | 
			
		||||
      _ => await picker.pickMedia(),
 | 
			
		||||
    };
 | 
			
		||||
    if (result == null) return;
 | 
			
		||||
    setState(() => _thumbnailFile = result);
 | 
			
		||||
    setState(() => _file = result);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool _isBusy = false;
 | 
			
		||||
@@ -46,15 +61,20 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
 | 
			
		||||
        if (!mounted) return;
 | 
			
		||||
        context.showErrorDialog(err);
 | 
			
		||||
      }
 | 
			
		||||
    } else if (_thumbnailFile != null) {
 | 
			
		||||
    } else if (_file != null) {
 | 
			
		||||
      try {
 | 
			
		||||
        final attachment = await attach.directUploadOne(
 | 
			
		||||
          (await _thumbnailFile!.readAsBytes()).buffer.asUint8List(),
 | 
			
		||||
          _thumbnailFile!.path,
 | 
			
		||||
          'interactive',
 | 
			
		||||
          null,
 | 
			
		||||
        final place = await attach.chunkedUploadInitialize(await _file!.length(), _file!.name, widget.pool, null);
 | 
			
		||||
 | 
			
		||||
        final attachment = await attach.chunkedUploadParts(
 | 
			
		||||
          _file!,
 | 
			
		||||
          place.$1,
 | 
			
		||||
          place.$2,
 | 
			
		||||
          analyzeNow: widget.analyzeNow ?? false,
 | 
			
		||||
          onProgress: (value) {
 | 
			
		||||
            setState(() => _progress = value);
 | 
			
		||||
          },
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (!mounted) return;
 | 
			
		||||
        Navigator.pop(context, attachment);
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
@@ -67,7 +87,7 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AlertDialog(
 | 
			
		||||
      title: Text(widget.title ?? 'attachmentInputDialog').tr(),
 | 
			
		||||
      title: Text(widget.title ?? 'attachmentInputDialog'.tr()),
 | 
			
		||||
      content: Column(
 | 
			
		||||
        crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
        mainAxisSize: MainAxisSize.min,
 | 
			
		||||
@@ -86,22 +106,33 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
 | 
			
		||||
          const Gap(24),
 | 
			
		||||
          Text('attachmentInputNew').tr().fontSize(14),
 | 
			
		||||
          Card(
 | 
			
		||||
            child: ListTile(
 | 
			
		||||
            child: Column(
 | 
			
		||||
              children: [
 | 
			
		||||
                ListTile(
 | 
			
		||||
                  contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
 | 
			
		||||
                  leading: const Icon(Symbols.add_photo_alternate),
 | 
			
		||||
                  trailing: const Icon(Symbols.chevron_right),
 | 
			
		||||
                  title: Text('addAttachmentFromAlbum').tr(),
 | 
			
		||||
              subtitle: _thumbnailFile == null ? Text('unset').tr() : Text('waitingForUpload').tr(),
 | 
			
		||||
                  subtitle: _file == null ? Text('unset').tr() : Text('waitingForUpload').tr(),
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                _pickImage();
 | 
			
		||||
                    _pickMedia();
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          if (_isBusy)
 | 
			
		||||
            LinearProgressIndicator(
 | 
			
		||||
              value: _progress,
 | 
			
		||||
              borderRadius: BorderRadius.all(Radius.circular(8)),
 | 
			
		||||
            ).padding(top: 16),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
      actions: [
 | 
			
		||||
        TextButton(
 | 
			
		||||
          onPressed: _isBusy ? null : () {
 | 
			
		||||
          onPressed: _isBusy
 | 
			
		||||
              ? null
 | 
			
		||||
              : () {
 | 
			
		||||
                  Navigator.pop(context);
 | 
			
		||||
                },
 | 
			
		||||
          child: Text('dialogDismiss').tr(),
 | 
			
		||||
 
 | 
			
		||||
@@ -203,6 +203,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
 | 
			
		||||
  void dispose() {
 | 
			
		||||
    _contentController.dispose();
 | 
			
		||||
    _focusNode.dispose();
 | 
			
		||||
    _dismissEmojiPicker();
 | 
			
		||||
    if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) hotKeyManager.unregister(_pasteHotKey);
 | 
			
		||||
    super.dispose();
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -28,6 +28,7 @@ import 'package:surface/types/attachment.dart';
 | 
			
		||||
import 'package:surface/types/post.dart';
 | 
			
		||||
import 'package:surface/types/reaction.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
import 'package:surface/widgets/attachment/attachment_item.dart';
 | 
			
		||||
import 'package:surface/widgets/attachment/attachment_list.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/link_preview.dart';
 | 
			
		||||
@@ -210,6 +211,7 @@ class PostItem extends StatelessWidget {
 | 
			
		||||
                if (onDeleted != null) {}
 | 
			
		||||
              },
 | 
			
		||||
            ).padding(horizontal: 12, top: 8, bottom: 8),
 | 
			
		||||
            if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
 | 
			
		||||
            Container(
 | 
			
		||||
              width: double.infinity,
 | 
			
		||||
              margin: const EdgeInsets.only(bottom: 4, left: 12, right: 12),
 | 
			
		||||
@@ -293,6 +295,7 @@ class PostItem extends StatelessWidget {
 | 
			
		||||
                  if (onDeleted != null) onDeleted!();
 | 
			
		||||
                },
 | 
			
		||||
              ).padding(horizontal: 12, vertical: 8),
 | 
			
		||||
              if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
 | 
			
		||||
              if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8),
 | 
			
		||||
              if (data.body['title'] != null || data.body['description'] != null)
 | 
			
		||||
                _PostHeadline(
 | 
			
		||||
@@ -1520,3 +1523,29 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _PostVideoPlayer extends StatelessWidget {
 | 
			
		||||
  final SnPost data;
 | 
			
		||||
 | 
			
		||||
  const _PostVideoPlayer({required this.data});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Container(
 | 
			
		||||
      decoration: BoxDecoration(
 | 
			
		||||
        borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
			
		||||
        border: Border.all(
 | 
			
		||||
          color: Theme.of(context).dividerColor,
 | 
			
		||||
          width: 1,
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
      child: AspectRatio(
 | 
			
		||||
        aspectRatio: 16 / 9,
 | 
			
		||||
        child: ClipRRect(
 | 
			
		||||
          borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
			
		||||
          child: AttachmentItem(data: data.preload!.video!, heroTag: 'post-video-${data.id}'),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -95,6 +95,7 @@ class PostMediaPendingList extends StatelessWidget {
 | 
			
		||||
      context: context,
 | 
			
		||||
      builder: (context) => AttachmentInputDialog(
 | 
			
		||||
        title: 'attachmentSetThumbnail'.tr(),
 | 
			
		||||
        pool: 'interactive',
 | 
			
		||||
        analyzeNow: true,
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,6 @@ PODS:
 | 
			
		||||
  - bitsdojo_window_macos (0.0.1):
 | 
			
		||||
    - FlutterMacOS
 | 
			
		||||
  - connectivity_plus (0.0.1):
 | 
			
		||||
    - Flutter
 | 
			
		||||
    - FlutterMacOS
 | 
			
		||||
  - croppy (0.0.1):
 | 
			
		||||
    - FlutterMacOS
 | 
			
		||||
@@ -143,7 +142,7 @@ PODS:
 | 
			
		||||
    - HotKey
 | 
			
		||||
  - in_app_review (2.0.0):
 | 
			
		||||
    - FlutterMacOS
 | 
			
		||||
  - livekit_client (2.3.5):
 | 
			
		||||
  - livekit_client (2.3.6):
 | 
			
		||||
    - flutter_webrtc
 | 
			
		||||
    - FlutterMacOS
 | 
			
		||||
    - WebRTC-SDK (= 125.6422.06)
 | 
			
		||||
@@ -190,7 +189,7 @@ PODS:
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES:
 | 
			
		||||
  - bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`)
 | 
			
		||||
  - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`)
 | 
			
		||||
  - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`)
 | 
			
		||||
  - croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/macos`)
 | 
			
		||||
  - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
 | 
			
		||||
  - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
 | 
			
		||||
@@ -244,7 +243,7 @@ EXTERNAL SOURCES:
 | 
			
		||||
  bitsdojo_window_macos:
 | 
			
		||||
    :path: Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos
 | 
			
		||||
  connectivity_plus:
 | 
			
		||||
    :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin
 | 
			
		||||
    :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos
 | 
			
		||||
  croppy:
 | 
			
		||||
    :path: Flutter/ephemeral/.symlinks/plugins/croppy/macos
 | 
			
		||||
  device_info_plus:
 | 
			
		||||
@@ -308,7 +307,7 @@ EXTERNAL SOURCES:
 | 
			
		||||
 | 
			
		||||
SPEC CHECKSUMS:
 | 
			
		||||
  bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00
 | 
			
		||||
  connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
 | 
			
		||||
  connectivity_plus: 0a976dfd033b59192912fa3c6c7b54aab5093802
 | 
			
		||||
  croppy: 25a638bd7d05411d8c697f481568f261037694fc
 | 
			
		||||
  device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
 | 
			
		||||
  file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af
 | 
			
		||||
@@ -334,7 +333,7 @@ SPEC CHECKSUMS:
 | 
			
		||||
  HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277
 | 
			
		||||
  hotkey_manager_macos: 1e2edb0c7ae4fe67108af44a9d3445de41404160
 | 
			
		||||
  in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93
 | 
			
		||||
  livekit_client: 91c68237edede55f8891a166a28c1daec8a1e4b1
 | 
			
		||||
  livekit_client: 0ad107154753a5a76802d2222c040223ad049499
 | 
			
		||||
  media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
 | 
			
		||||
  media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
 | 
			
		||||
  media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										48
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										48
									
								
								pubspec.lock
									
									
									
									
									
								
							@@ -214,6 +214,14 @@ packages:
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.4.3"
 | 
			
		||||
  chalkdart:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: chalkdart
 | 
			
		||||
      sha256: "08c910ee341fcdd1e46f20ddce59b13c1d85f5d97f2fd2f12014c46ede670e40"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.3.2"
 | 
			
		||||
  characters:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -266,10 +274,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: connectivity_plus
 | 
			
		||||
      sha256: "8a68739d3ee113e51ad35583fdf9ab82c55d09d693d3c39da1aebab87c938412"
 | 
			
		||||
      sha256: "04bf81bb0b77de31557b58d052b24b3eee33f09a6e7a8c68a3e247c7df19ec27"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "6.1.2"
 | 
			
		||||
    version: "6.1.3"
 | 
			
		||||
  connectivity_plus_platform_interface:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -362,10 +370,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: device_info_plus
 | 
			
		||||
      sha256: e3fc9a65820fef83035af8ee8c09004a719d5d1d54e6de978fcb0d84bbeb241a
 | 
			
		||||
      sha256: "72d146c6d7098689ff5c5f66bcf593ac11efc530095385356e131070333e64da"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "11.2.2"
 | 
			
		||||
    version: "11.3.0"
 | 
			
		||||
  device_info_plus_platform_interface:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -490,10 +498,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: file_picker
 | 
			
		||||
      sha256: c9943dd7d702ab4199d199bc151a2d79c86db031a02ad84566dab58c494d2adc
 | 
			
		||||
      sha256: cacfdc5abe93e64d418caa9256eef663499ad791bb688d9fd12c85a311968fba
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "8.3.1"
 | 
			
		||||
    version: "8.3.2"
 | 
			
		||||
  file_saver:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -830,10 +838,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_webrtc
 | 
			
		||||
      sha256: "4c76069f724f79dc6e8739c8f16c2ee00ca30a1f8e3aa75c0e830f8183278f03"
 | 
			
		||||
      sha256: e917067abeef2400e6a7a03db53a6e1418551e54809f18ab80447ac323eb77e4
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.12.7"
 | 
			
		||||
    version: "0.12.8"
 | 
			
		||||
  freezed:
 | 
			
		||||
    dependency: "direct dev"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -886,10 +894,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: go_router
 | 
			
		||||
      sha256: "9b736a9fa879d8ad6df7932cbdcc58237c173ab004ef90d8377923d7ad731eaa"
 | 
			
		||||
      sha256: "04539267a740931c6d4479a10d466717ca5901c6fdfd3fcda09391bbb8ebd651"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "14.7.2"
 | 
			
		||||
    version: "14.8.0"
 | 
			
		||||
  google_fonts:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -934,10 +942,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: home_widget
 | 
			
		||||
      sha256: b313e3304c0429669fddf1286e1fbf61a64b873f38ba30b3eb890ef0d7560b12
 | 
			
		||||
      sha256: "7430f7549d42cef2e729bd3c779de748b93f1eb78b1abfe6bca8fffd1cfce3e9"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.7.0"
 | 
			
		||||
    version: "0.7.0+1"
 | 
			
		||||
  hotkey_manager:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -1190,10 +1198,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: livekit_client
 | 
			
		||||
      sha256: "02b4653d903852d0ae86b15fbe4324747606dae6410fe860d0c07a11c79988de"
 | 
			
		||||
      sha256: "0cfb2f48eff7a93ea8927696dc6f218aebd2fcd1fcc1b1a7b2f53ff3597fdb52"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.3.5"
 | 
			
		||||
    version: "2.3.6"
 | 
			
		||||
  logging:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -1246,10 +1254,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: material_symbols_icons
 | 
			
		||||
      sha256: "89aac72d25dd49303f71b3b1e70f8374791846729365b25bebc2a2531e5b86cd"
 | 
			
		||||
      sha256: "1403944c2a68dbdf934fca2b2115a372e70a4a185c136ab71a9d16e2108d0b14"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "4.2801.1"
 | 
			
		||||
    version: "4.2805.1"
 | 
			
		||||
  media_kit:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -1382,10 +1390,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: package_info_plus
 | 
			
		||||
      sha256: c447a3c3e7be4addf129b8f9ab6a4bd5d166b78918223e223b61fddf4d07e254
 | 
			
		||||
      sha256: "67eae327b1b0faf761964a1d2e5d323c797f3799db0e85aa232db8d9e922bc35"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "8.2.0"
 | 
			
		||||
    version: "8.2.1"
 | 
			
		||||
  package_info_plus_platform_interface:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -2251,10 +2259,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: webrtc_interface
 | 
			
		||||
      sha256: abec3ab7956bd5ac539cf34a42fa0c82ea26675847c0966bb85160400eea9388
 | 
			
		||||
      sha256: "10fc6dc0ac16f909f5e434c18902415211d759313c87261f1e4ec5b4f6a04c26"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.2.0"
 | 
			
		||||
    version: "1.2.1"
 | 
			
		||||
  win32:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
 | 
			
		||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
			
		||||
# In Windows, build-name is used as the major, minor, and patch parts
 | 
			
		||||
# of the product and file versions while build-number is used as the build suffix.
 | 
			
		||||
version: 2.3.2+64
 | 
			
		||||
version: 2.3.2+65
 | 
			
		||||
 | 
			
		||||
environment:
 | 
			
		||||
  sdk: ^3.5.4
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user