Compare commits
	
		
			6 Commits
		
	
	
		
			46919dec31
			...
			3.0.0+111
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 552b4b2572 | |||
| 594ac39e3d | |||
| 23321171f3 | |||
| ee72d79c93 | |||
| a20c2598fc | |||
| 2eba871a6d | 
@@ -375,7 +375,9 @@
 | 
				
			|||||||
  "postContent": "Content",
 | 
					  "postContent": "Content",
 | 
				
			||||||
  "postSettings": "Settings",
 | 
					  "postSettings": "Settings",
 | 
				
			||||||
  "postPublisherUnselected": "Publisher Unspecified",
 | 
					  "postPublisherUnselected": "Publisher Unspecified",
 | 
				
			||||||
  "postVisibility": "Visibility",
 | 
					  "postType": "Post Type",
 | 
				
			||||||
 | 
					  "articleAttachmentHint": "Attachments must be uploaded and inserted into the article body to be visible.",
 | 
				
			||||||
 | 
					  "postVisibility": "Post Visibility",
 | 
				
			||||||
  "postVisibilityPublic": "Public",
 | 
					  "postVisibilityPublic": "Public",
 | 
				
			||||||
  "postVisibilityFriends": "Friends Only",
 | 
					  "postVisibilityFriends": "Friends Only",
 | 
				
			||||||
  "postVisibilityUnlisted": "Unlisted",
 | 
					  "postVisibilityUnlisted": "Unlisted",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -532,7 +532,7 @@
 | 
				
			|||||||
  "aboutScreenContactUsTitle": "联系我们",
 | 
					  "aboutScreenContactUsTitle": "联系我们",
 | 
				
			||||||
  "aboutScreenLicenseTitle": "许可证",
 | 
					  "aboutScreenLicenseTitle": "许可证",
 | 
				
			||||||
  "aboutScreenLicenseContent": "GNU Affero General Public License v3.0",
 | 
					  "aboutScreenLicenseContent": "GNU Affero General Public License v3.0",
 | 
				
			||||||
  "aboutScreenCopyright": "版权所有 © Solsynth {}",
 | 
					  "aboutScreenCopyright": "版权所有 © 索尔辛茨 {}",
 | 
				
			||||||
  "aboutScreenMadeWith": "由 Solar Network Team 用 ❤︎️ 制作",
 | 
					  "aboutScreenMadeWith": "由 Solar Network Team 用 ❤︎️ 制作",
 | 
				
			||||||
  "aboutScreenFailedToLoadPackageInfo": "加载包信息失败:{error}",
 | 
					  "aboutScreenFailedToLoadPackageInfo": "加载包信息失败:{error}",
 | 
				
			||||||
  "copiedToClipboard": "已复制到剪贴板",
 | 
					  "copiedToClipboard": "已复制到剪贴板",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -221,7 +221,7 @@ class IslandApp extends HookConsumerWidget {
 | 
				
			|||||||
      Future(() {
 | 
					      Future(() {
 | 
				
			||||||
        userNotifier.fetchUser().then((_) {
 | 
					        userNotifier.fetchUser().then((_) {
 | 
				
			||||||
          final user = ref.watch(userInfoProvider);
 | 
					          final user = ref.watch(userInfoProvider);
 | 
				
			||||||
          if (user.hasValue) {
 | 
					          if (user.value != null) {
 | 
				
			||||||
            final apiClient = ref.read(apiClientProvider);
 | 
					            final apiClient = ref.read(apiClientProvider);
 | 
				
			||||||
            subscribePushNotification(apiClient);
 | 
					            subscribePushNotification(apiClient);
 | 
				
			||||||
            final wsNotifier = ref.read(websocketStateProvider.notifier);
 | 
					            final wsNotifier = ref.read(websocketStateProvider.notifier);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,8 +18,13 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
 | 
				
			|||||||
      final user = SnAccount.fromJson(response.data);
 | 
					      final user = SnAccount.fromJson(response.data);
 | 
				
			||||||
      state = AsyncValue.data(user);
 | 
					      state = AsyncValue.data(user);
 | 
				
			||||||
    } catch (error, stackTrace) {
 | 
					    } catch (error, stackTrace) {
 | 
				
			||||||
      log("[UserInfo] Failed to fetch user info: $error");
 | 
					      log(
 | 
				
			||||||
      state = AsyncValue.error(error, stackTrace);
 | 
					        "[UserInfo] Failed to fetch user info...",
 | 
				
			||||||
 | 
					        name: 'UserInfoNotifier',
 | 
				
			||||||
 | 
					        error: error,
 | 
				
			||||||
 | 
					        stackTrace: stackTrace,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      state = AsyncValue.data(null);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -59,7 +59,7 @@ class AccountScreen extends HookConsumerWidget {
 | 
				
			|||||||
      notificationUnreadCountNotifierProvider,
 | 
					      notificationUnreadCountNotifierProvider,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!user.hasValue || user.value == null) {
 | 
					    if (user.value == null || user.value == null) {
 | 
				
			||||||
      return _UnauthorizedAccountScreen();
 | 
					      return _UnauthorizedAccountScreen();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -367,12 +367,23 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
 | 
				
			|||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                const Gap(8),
 | 
					                const Gap(8),
 | 
				
			||||||
 | 
					                Row(
 | 
				
			||||||
 | 
					                  mainAxisAlignment: MainAxisAlignment.center,
 | 
				
			||||||
 | 
					                  children: [
 | 
				
			||||||
 | 
					                    TextButton(
 | 
				
			||||||
 | 
					                      onPressed: () {
 | 
				
			||||||
 | 
					                        context.push('/about');
 | 
				
			||||||
 | 
					                      },
 | 
				
			||||||
 | 
					                      child: Text('about').tr(),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
                    TextButton(
 | 
					                    TextButton(
 | 
				
			||||||
                      onPressed: () {
 | 
					                      onPressed: () {
 | 
				
			||||||
                        context.push('/settings');
 | 
					                        context.push('/settings');
 | 
				
			||||||
                      },
 | 
					                      },
 | 
				
			||||||
                      child: Text('appSettings').tr(),
 | 
					                      child: Text('appSettings').tr(),
 | 
				
			||||||
                ).center(),
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ],
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
              ],
 | 
					              ],
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ).center(),
 | 
					          ).center(),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -82,7 +82,7 @@ class EventCalanderScreen extends HookConsumerWidget {
 | 
				
			|||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                      // Show user profile if viewing someone else's calendar
 | 
					                      // Show user profile if viewing someone else's calendar
 | 
				
			||||||
                      if (name != 'me' && user.hasValue)
 | 
					                      if (name != 'me' && user.value != null)
 | 
				
			||||||
                        AccountNameplate(name: name),
 | 
					                        AccountNameplate(name: name),
 | 
				
			||||||
                    ],
 | 
					                    ],
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
@@ -106,7 +106,7 @@ class EventCalanderScreen extends HookConsumerWidget {
 | 
				
			|||||||
                    ).padding(horizontal: 8, vertical: 4),
 | 
					                    ).padding(horizontal: 8, vertical: 4),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    // Show user profile if viewing someone else's calendar
 | 
					                    // Show user profile if viewing someone else's calendar
 | 
				
			||||||
                    if (name != 'me' && user.hasValue)
 | 
					                    if (name != 'me' && user.value != null)
 | 
				
			||||||
                      AccountNameplate(name: name),
 | 
					                      AccountNameplate(name: name),
 | 
				
			||||||
                    Gap(MediaQuery.of(context).padding.bottom + 16),
 | 
					                    Gap(MediaQuery.of(context).padding.bottom + 16),
 | 
				
			||||||
                  ],
 | 
					                  ],
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -72,6 +72,8 @@ Future<Color?> accountAppbarForcegroundColor(Ref ref, String uname) async {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@riverpod
 | 
					@riverpod
 | 
				
			||||||
Future<SnChatRoom?> accountDirectChat(Ref ref, String uname) async {
 | 
					Future<SnChatRoom?> accountDirectChat(Ref ref, String uname) async {
 | 
				
			||||||
 | 
					  final userInfo = ref.watch(userInfoProvider);
 | 
				
			||||||
 | 
					  if (userInfo.value == null) return null;
 | 
				
			||||||
  final account = await ref.watch(accountProvider(uname).future);
 | 
					  final account = await ref.watch(accountProvider(uname).future);
 | 
				
			||||||
  final apiClient = ref.watch(apiClientProvider);
 | 
					  final apiClient = ref.watch(apiClientProvider);
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
@@ -87,6 +89,8 @@ Future<SnChatRoom?> accountDirectChat(Ref ref, String uname) async {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@riverpod
 | 
					@riverpod
 | 
				
			||||||
Future<SnRelationship?> accountRelationship(Ref ref, String uname) async {
 | 
					Future<SnRelationship?> accountRelationship(Ref ref, String uname) async {
 | 
				
			||||||
 | 
					  final userInfo = ref.watch(userInfoProvider);
 | 
				
			||||||
 | 
					  if (userInfo.value == null) return null;
 | 
				
			||||||
  final account = await ref.watch(accountProvider(uname).future);
 | 
					  final account = await ref.watch(accountProvider(uname).future);
 | 
				
			||||||
  final apiClient = ref.watch(apiClientProvider);
 | 
					  final apiClient = ref.watch(apiClientProvider);
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
@@ -219,6 +223,8 @@ class AccountProfileScreen extends HookConsumerWidget {
 | 
				
			|||||||
      ];
 | 
					      ];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final user = ref.watch(userInfoProvider);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return account.when(
 | 
					    return account.when(
 | 
				
			||||||
      data:
 | 
					      data:
 | 
				
			||||||
          (data) => AppScaffold(
 | 
					          (data) => AppScaffold(
 | 
				
			||||||
@@ -379,9 +385,13 @@ class AccountProfileScreen extends HookConsumerWidget {
 | 
				
			|||||||
                  ).padding(horizontal: 24),
 | 
					                  ).padding(horizontal: 24),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (user.value != null)
 | 
				
			||||||
                  SliverToBoxAdapter(
 | 
					                  SliverToBoxAdapter(
 | 
				
			||||||
                  child: const Divider(height: 1).padding(top: 24, bottom: 12),
 | 
					                    child: const Divider(
 | 
				
			||||||
 | 
					                      height: 1,
 | 
				
			||||||
 | 
					                    ).padding(top: 24, bottom: 12),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
 | 
					                if (user.value != null)
 | 
				
			||||||
                  SliverToBoxAdapter(
 | 
					                  SliverToBoxAdapter(
 | 
				
			||||||
                    child: Row(
 | 
					                    child: Row(
 | 
				
			||||||
                      spacing: 8,
 | 
					                      spacing: 8,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -51,6 +51,9 @@ class _ArticleDetailContent extends HookConsumerWidget {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return SingleChildScrollView(
 | 
					    return SingleChildScrollView(
 | 
				
			||||||
 | 
					      child: Center(
 | 
				
			||||||
 | 
					        child: ConstrainedBox(
 | 
				
			||||||
 | 
					          constraints: const BoxConstraints(maxWidth: 560),
 | 
				
			||||||
          child: Column(
 | 
					          child: Column(
 | 
				
			||||||
            crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
					            crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
				
			||||||
            children: [
 | 
					            children: [
 | 
				
			||||||
@@ -100,6 +103,8 @@ class _ArticleDetailContent extends HookConsumerWidget {
 | 
				
			|||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -126,7 +126,10 @@ class ArticlesScreen extends ConsumerWidget {
 | 
				
			|||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    return Scaffold(
 | 
					    return Scaffold(
 | 
				
			||||||
      appBar: AppBar(title: Text(title ?? 'Articles')),
 | 
					      appBar: AppBar(title: Text(title ?? 'Articles')),
 | 
				
			||||||
      body: CustomScrollView(
 | 
					      body: Center(
 | 
				
			||||||
 | 
					        child: ConstrainedBox(
 | 
				
			||||||
 | 
					          constraints: const BoxConstraints(maxWidth: 560),
 | 
				
			||||||
 | 
					          child: CustomScrollView(
 | 
				
			||||||
            slivers: [
 | 
					            slivers: [
 | 
				
			||||||
              SliverPadding(
 | 
					              SliverPadding(
 | 
				
			||||||
                padding: const EdgeInsets.only(top: 8, left: 8, right: 8),
 | 
					                padding: const EdgeInsets.only(top: 8, left: 8, right: 8),
 | 
				
			||||||
@@ -137,6 +140,8 @@ class ArticlesScreen extends ConsumerWidget {
 | 
				
			|||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -307,7 +307,7 @@ class _ActivityListView extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return CustomScrollView(
 | 
					    return CustomScrollView(
 | 
				
			||||||
      slivers: [
 | 
					      slivers: [
 | 
				
			||||||
        if (user.hasValue && !contentOnly)
 | 
					        if (user.value != null && !contentOnly)
 | 
				
			||||||
          SliverToBoxAdapter(child: CheckInWidget()),
 | 
					          SliverToBoxAdapter(child: CheckInWidget()),
 | 
				
			||||||
        SliverList.builder(
 | 
					        SliverList.builder(
 | 
				
			||||||
          itemCount: widgetCount,
 | 
					          itemCount: widgetCount,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -321,8 +321,15 @@ class ArticleComposeScreen extends HookConsumerWidget {
 | 
				
			|||||||
            builder: (context, attachments, _) {
 | 
					            builder: (context, attachments, _) {
 | 
				
			||||||
              if (attachments.isEmpty) return const SizedBox.shrink();
 | 
					              if (attachments.isEmpty) return const SizedBox.shrink();
 | 
				
			||||||
              return Column(
 | 
					              return Column(
 | 
				
			||||||
 | 
					                crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
                children: [
 | 
					                children: [
 | 
				
			||||||
                  const Gap(16),
 | 
					                  const Gap(16),
 | 
				
			||||||
 | 
					                  Text(
 | 
				
			||||||
 | 
					                    'articleAttachmentHint'.tr(),
 | 
				
			||||||
 | 
					                    style: Theme.of(context).textTheme.bodySmall?.copyWith(
 | 
				
			||||||
 | 
					                      color: Theme.of(context).colorScheme.onSurfaceVariant,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ).padding(bottom: 8),
 | 
				
			||||||
                  ValueListenableBuilder<Map<int, double>>(
 | 
					                  ValueListenableBuilder<Map<int, double>>(
 | 
				
			||||||
                    valueListenable: state.attachmentProgress,
 | 
					                    valueListenable: state.attachmentProgress,
 | 
				
			||||||
                    builder: (context, progressMap, _) {
 | 
					                    builder: (context, progressMap, _) {
 | 
				
			||||||
@@ -332,8 +339,8 @@ class ArticleComposeScreen extends HookConsumerWidget {
 | 
				
			|||||||
                        children: [
 | 
					                        children: [
 | 
				
			||||||
                          for (var idx = 0; idx < attachments.length; idx++)
 | 
					                          for (var idx = 0; idx < attachments.length; idx++)
 | 
				
			||||||
                            SizedBox(
 | 
					                            SizedBox(
 | 
				
			||||||
                              width: 120,
 | 
					                              width: 280,
 | 
				
			||||||
                              height: 120,
 | 
					                              height: 280,
 | 
				
			||||||
                              child: AttachmentPreview(
 | 
					                              child: AttachmentPreview(
 | 
				
			||||||
                                item: attachments[idx],
 | 
					                                item: attachments[idx],
 | 
				
			||||||
                                progress: progressMap[idx],
 | 
					                                progress: progressMap[idx],
 | 
				
			||||||
@@ -358,6 +365,12 @@ class ArticleComposeScreen extends HookConsumerWidget {
 | 
				
			|||||||
                                    delta,
 | 
					                                    delta,
 | 
				
			||||||
                                  );
 | 
					                                  );
 | 
				
			||||||
                                },
 | 
					                                },
 | 
				
			||||||
 | 
					                                onInsert:
 | 
				
			||||||
 | 
					                                    () => ComposeLogic.insertAttachment(
 | 
				
			||||||
 | 
					                                      ref,
 | 
				
			||||||
 | 
					                                      state,
 | 
				
			||||||
 | 
					                                      idx,
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
                              ),
 | 
					                              ),
 | 
				
			||||||
                            ),
 | 
					                            ),
 | 
				
			||||||
                        ],
 | 
					                        ],
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -59,7 +59,7 @@ class AccountStatusCreationSheet extends HookConsumerWidget {
 | 
				
			|||||||
          },
 | 
					          },
 | 
				
			||||||
          options: Options(method: initialStatus == null ? 'POST' : 'PATCH'),
 | 
					          options: Options(method: initialStatus == null ? 'POST' : 'PATCH'),
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        if (user.hasValue) {
 | 
					        if (user.value != null) {
 | 
				
			||||||
          ref.invalidate(accountStatusProvider(user.value!.name));
 | 
					          ref.invalidate(accountStatusProvider(user.value!.name));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (!context.mounted) return;
 | 
					        if (!context.mounted) return;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -350,7 +350,7 @@ class _WebSocketIndicator extends HookConsumerWidget {
 | 
				
			|||||||
    return AnimatedPositioned(
 | 
					    return AnimatedPositioned(
 | 
				
			||||||
      duration: Duration(milliseconds: 1850),
 | 
					      duration: Duration(milliseconds: 1850),
 | 
				
			||||||
      top:
 | 
					      top:
 | 
				
			||||||
          !user.hasValue ||
 | 
					          user.value == null ||
 | 
				
			||||||
                  user.value == null ||
 | 
					                  user.value == null ||
 | 
				
			||||||
                  websocketState == WebSocketState.connected()
 | 
					                  websocketState == WebSocketState.connected()
 | 
				
			||||||
              ? -indicatorHeight
 | 
					              ? -indicatorHeight
 | 
				
			||||||
@@ -362,7 +362,7 @@ class _WebSocketIndicator extends HookConsumerWidget {
 | 
				
			|||||||
      child: IgnorePointer(
 | 
					      child: IgnorePointer(
 | 
				
			||||||
        child: Material(
 | 
					        child: Material(
 | 
				
			||||||
          elevation:
 | 
					          elevation:
 | 
				
			||||||
              !user.hasValue || websocketState == WebSocketState.connected()
 | 
					              user.value == null || websocketState == WebSocketState.connected()
 | 
				
			||||||
                  ? 0
 | 
					                  ? 0
 | 
				
			||||||
                  : 4,
 | 
					                  : 4,
 | 
				
			||||||
          child: AnimatedContainer(
 | 
					          child: AnimatedContainer(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,6 +15,7 @@ class AttachmentPreview extends StatelessWidget {
 | 
				
			|||||||
  final double? progress;
 | 
					  final double? progress;
 | 
				
			||||||
  final Function(int)? onMove;
 | 
					  final Function(int)? onMove;
 | 
				
			||||||
  final Function? onDelete;
 | 
					  final Function? onDelete;
 | 
				
			||||||
 | 
					  final Function? onInsert;
 | 
				
			||||||
  final Function? onRequestUpload;
 | 
					  final Function? onRequestUpload;
 | 
				
			||||||
  const AttachmentPreview({
 | 
					  const AttachmentPreview({
 | 
				
			||||||
    super.key,
 | 
					    super.key,
 | 
				
			||||||
@@ -23,6 +24,7 @@ class AttachmentPreview extends StatelessWidget {
 | 
				
			|||||||
    this.onRequestUpload,
 | 
					    this.onRequestUpload,
 | 
				
			||||||
    this.onMove,
 | 
					    this.onMove,
 | 
				
			||||||
    this.onDelete,
 | 
					    this.onDelete,
 | 
				
			||||||
 | 
					    this.onInsert,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@@ -104,7 +106,11 @@ class AttachmentPreview extends StatelessWidget {
 | 
				
			|||||||
                          style: TextStyle(color: Colors.white),
 | 
					                          style: TextStyle(color: Colors.white),
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                      Gap(6),
 | 
					                      Gap(6),
 | 
				
			||||||
                      Center(child: LinearProgressIndicator(value: progress)),
 | 
					                      Center(
 | 
				
			||||||
 | 
					                        child: LinearProgressIndicator(
 | 
				
			||||||
 | 
					                          value: progress != null ? progress! / 100.0 : null,
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
                    ],
 | 
					                    ],
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
@@ -166,6 +172,18 @@ class AttachmentPreview extends StatelessWidget {
 | 
				
			|||||||
                              onMove?.call(1);
 | 
					                              onMove?.call(1);
 | 
				
			||||||
                            },
 | 
					                            },
 | 
				
			||||||
                          ),
 | 
					                          ),
 | 
				
			||||||
 | 
					                        if (onInsert != null)
 | 
				
			||||||
 | 
					                          InkWell(
 | 
				
			||||||
 | 
					                            borderRadius: BorderRadius.circular(8),
 | 
				
			||||||
 | 
					                            child: const Icon(
 | 
				
			||||||
 | 
					                              Symbols.add,
 | 
				
			||||||
 | 
					                              size: 14,
 | 
				
			||||||
 | 
					                              color: Colors.white,
 | 
				
			||||||
 | 
					                            ).padding(horizontal: 8, vertical: 6),
 | 
				
			||||||
 | 
					                            onTap: () {
 | 
				
			||||||
 | 
					                              onInsert?.call();
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
                      ],
 | 
					                      ],
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					import 'package:collection/collection.dart';
 | 
				
			||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:go_router/go_router.dart';
 | 
					import 'package:go_router/go_router.dart';
 | 
				
			||||||
@@ -6,11 +7,14 @@ import 'package:flutter_highlight/themes/a11y-dark.dart';
 | 
				
			|||||||
import 'package:flutter_highlight/themes/a11y-light.dart';
 | 
					import 'package:flutter_highlight/themes/a11y-light.dart';
 | 
				
			||||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
					import 'package:flutter_hooks/flutter_hooks.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:island/models/file.dart';
 | 
				
			||||||
import 'package:island/pods/config.dart';
 | 
					import 'package:island/pods/config.dart';
 | 
				
			||||||
import 'package:island/widgets/alert.dart';
 | 
					import 'package:island/widgets/alert.dart';
 | 
				
			||||||
 | 
					import 'package:island/widgets/content/cloud_files.dart';
 | 
				
			||||||
import 'package:island/widgets/content/markdown_latex.dart';
 | 
					import 'package:island/widgets/content/markdown_latex.dart';
 | 
				
			||||||
import 'package:markdown/markdown.dart' as markdown;
 | 
					import 'package:markdown/markdown.dart' as markdown;
 | 
				
			||||||
import 'package:markdown_widget/markdown_widget.dart';
 | 
					import 'package:markdown_widget/markdown_widget.dart';
 | 
				
			||||||
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
import 'package:url_launcher/url_launcher.dart';
 | 
					import 'package:url_launcher/url_launcher.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'image.dart';
 | 
					import 'image.dart';
 | 
				
			||||||
@@ -23,6 +27,7 @@ class MarkdownTextContent extends HookConsumerWidget {
 | 
				
			|||||||
  final TextStyle? linkStyle;
 | 
					  final TextStyle? linkStyle;
 | 
				
			||||||
  final EdgeInsets? linesMargin;
 | 
					  final EdgeInsets? linesMargin;
 | 
				
			||||||
  final bool isSelectable;
 | 
					  final bool isSelectable;
 | 
				
			||||||
 | 
					  final List<SnCloudFile>? attachments;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const MarkdownTextContent({
 | 
					  const MarkdownTextContent({
 | 
				
			||||||
    super.key,
 | 
					    super.key,
 | 
				
			||||||
@@ -33,6 +38,7 @@ class MarkdownTextContent extends HookConsumerWidget {
 | 
				
			|||||||
    this.linkStyle,
 | 
					    this.linkStyle,
 | 
				
			||||||
    this.isSelectable = false,
 | 
					    this.isSelectable = false,
 | 
				
			||||||
    this.linesMargin,
 | 
					    this.linesMargin,
 | 
				
			||||||
 | 
					    this.attachments,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@@ -109,6 +115,29 @@ class MarkdownTextContent extends HookConsumerWidget {
 | 
				
			|||||||
              final uri = Uri.parse(url);
 | 
					              final uri = Uri.parse(url);
 | 
				
			||||||
              if (uri.scheme == 'solian') {
 | 
					              if (uri.scheme == 'solian') {
 | 
				
			||||||
                switch (uri.host) {
 | 
					                switch (uri.host) {
 | 
				
			||||||
 | 
					                  case 'files':
 | 
				
			||||||
 | 
					                    final file = attachments?.firstWhereOrNull(
 | 
				
			||||||
 | 
					                      (file) => file.id == uri.pathSegments[0],
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                    if (file == null) {
 | 
				
			||||||
 | 
					                      return const SizedBox.shrink();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    return ClipRRect(
 | 
				
			||||||
 | 
					                      borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
 | 
					                      child: Container(
 | 
				
			||||||
 | 
					                        decoration: BoxDecoration(
 | 
				
			||||||
 | 
					                          color: Theme.of(context).colorScheme.surfaceContainer,
 | 
				
			||||||
 | 
					                          borderRadius: const BorderRadius.all(
 | 
				
			||||||
 | 
					                            Radius.circular(8),
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                        child: CloudFileWidget(
 | 
				
			||||||
 | 
					                          item: file,
 | 
				
			||||||
 | 
					                          fit: BoxFit.cover,
 | 
				
			||||||
 | 
					                        ).clipRRect(all: 8),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
                  case 'stickers':
 | 
					                  case 'stickers':
 | 
				
			||||||
                    final size = doesEnlargeSticker ? 96.0 : 24.0;
 | 
					                    final size = doesEnlargeSticker ? 96.0 : 24.0;
 | 
				
			||||||
                    return ClipRRect(
 | 
					                    return ClipRRect(
 | 
				
			||||||
@@ -132,9 +161,9 @@ class MarkdownTextContent extends HookConsumerWidget {
 | 
				
			|||||||
                    );
 | 
					                    );
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
              final content = UniversalImage(
 | 
					              final content = ConstrainedBox(
 | 
				
			||||||
                uri: uri.toString(),
 | 
					                constraints: BoxConstraints(maxHeight: 360),
 | 
				
			||||||
                fit: BoxFit.cover,
 | 
					                child: UniversalImage(uri: uri.toString(), fit: BoxFit.contain),
 | 
				
			||||||
              );
 | 
					              );
 | 
				
			||||||
              return content;
 | 
					              return content;
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -474,6 +474,23 @@ class ComposeLogic {
 | 
				
			|||||||
    state.attachments.value = clone;
 | 
					    state.attachments.value = clone;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static void insertAttachment(WidgetRef ref, ComposeState state, int index) {
 | 
				
			||||||
 | 
					    final attachment = state.attachments.value[index];
 | 
				
			||||||
 | 
					    if (!attachment.isOnCloud) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    final cloudFile = attachment.data as SnCloudFile;
 | 
				
			||||||
 | 
					    final markdown = '';
 | 
				
			||||||
 | 
					    final controller = state.contentController;
 | 
				
			||||||
 | 
					    final text = controller.text;
 | 
				
			||||||
 | 
					    final selection = controller.selection;
 | 
				
			||||||
 | 
					    final newText = text.replaceRange(selection.start, selection.end, markdown);
 | 
				
			||||||
 | 
					    controller.text = newText;
 | 
				
			||||||
 | 
					    controller.selection = TextSelection.fromPosition(
 | 
				
			||||||
 | 
					      TextPosition(offset: selection.start + markdown.length),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static Future<void> performAction(
 | 
					  static Future<void> performAction(
 | 
				
			||||||
    WidgetRef ref,
 | 
					    WidgetRef ref,
 | 
				
			||||||
    ComposeState state,
 | 
					    ComposeState state,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -56,7 +56,7 @@ class PostItem extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    final user = ref.watch(userInfoProvider);
 | 
					    final user = ref.watch(userInfoProvider);
 | 
				
			||||||
    final isAuthor = useMemoized(
 | 
					    final isAuthor = useMemoized(
 | 
				
			||||||
      () => user.hasValue && user.value?.id == item.publisher.accountId,
 | 
					      () => user.value != null && user.value?.id == item.publisher.accountId,
 | 
				
			||||||
      [user],
 | 
					      [user],
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -163,7 +163,7 @@ class PostItem extends HookConsumerWidget {
 | 
				
			|||||||
            if ((item.repliedPost != null || item.forwardedPost != null) &&
 | 
					            if ((item.repliedPost != null || item.forwardedPost != null) &&
 | 
				
			||||||
                showReferencePost)
 | 
					                showReferencePost)
 | 
				
			||||||
              _buildReferencePost(context, item),
 | 
					              _buildReferencePost(context, item),
 | 
				
			||||||
            if (item.attachments.isNotEmpty)
 | 
					            if (item.attachments.isNotEmpty && item.type != 1)
 | 
				
			||||||
              CloudFileList(
 | 
					              CloudFileList(
 | 
				
			||||||
                files: item.attachments,
 | 
					                files: item.attachments,
 | 
				
			||||||
                maxWidth: math.min(
 | 
					                maxWidth: math.min(
 | 
				
			||||||
@@ -331,6 +331,7 @@ class PostItem extends HookConsumerWidget {
 | 
				
			|||||||
                                  item.type == 0
 | 
					                                  item.type == 0
 | 
				
			||||||
                                      ? EdgeInsets.only(bottom: 8)
 | 
					                                      ? EdgeInsets.only(bottom: 8)
 | 
				
			||||||
                                      : null,
 | 
					                                      : null,
 | 
				
			||||||
 | 
					                              attachments: item.attachments,
 | 
				
			||||||
                            ),
 | 
					                            ),
 | 
				
			||||||
                        ],
 | 
					                        ],
 | 
				
			||||||
                        // Render tags and categories if they exist
 | 
					                        // Render tags and categories if they exist
 | 
				
			||||||
@@ -389,7 +390,7 @@ class PostItem extends HookConsumerWidget {
 | 
				
			|||||||
                                item.forwardedPost != null) &&
 | 
					                                item.forwardedPost != null) &&
 | 
				
			||||||
                            showReferencePost)
 | 
					                            showReferencePost)
 | 
				
			||||||
                          _buildReferencePost(context, item),
 | 
					                          _buildReferencePost(context, item),
 | 
				
			||||||
                        if (item.attachments.isNotEmpty)
 | 
					                        if (item.attachments.isNotEmpty && item.type != 1)
 | 
				
			||||||
                          CloudFileList(
 | 
					                          CloudFileList(
 | 
				
			||||||
                            files: item.attachments,
 | 
					                            files: item.attachments,
 | 
				
			||||||
                            maxWidth: math.min(
 | 
					                            maxWidth: math.min(
 | 
				
			||||||
@@ -689,6 +690,7 @@ Widget _buildReferencePost(BuildContext context, SnPost item) {
 | 
				
			|||||||
                          referencePost.type == 0
 | 
					                          referencePost.type == 0
 | 
				
			||||||
                              ? EdgeInsets.only(bottom: 4)
 | 
					                              ? EdgeInsets.only(bottom: 4)
 | 
				
			||||||
                              : null,
 | 
					                              : null,
 | 
				
			||||||
 | 
					                      attachments: item.attachments,
 | 
				
			||||||
                    ).padding(bottom: 4),
 | 
					                    ).padding(bottom: 4),
 | 
				
			||||||
                  // Truncation hint for referenced post
 | 
					                  // Truncation hint for referenced post
 | 
				
			||||||
                  if (referencePost.isTruncated)
 | 
					                  if (referencePost.isTruncated)
 | 
				
			||||||
@@ -696,7 +698,8 @@ Widget _buildReferencePost(BuildContext context, SnPost item) {
 | 
				
			|||||||
                      isCompact: true,
 | 
					                      isCompact: true,
 | 
				
			||||||
                      margin: const EdgeInsets.only(top: 4, bottom: 8),
 | 
					                      margin: const EdgeInsets.only(top: 4, bottom: 8),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                  if (referencePost.attachments.isNotEmpty)
 | 
					                  if (referencePost.attachments.isNotEmpty &&
 | 
				
			||||||
 | 
					                      referencePost.type != 1)
 | 
				
			||||||
                    Row(
 | 
					                    Row(
 | 
				
			||||||
                      mainAxisSize: MainAxisSize.min,
 | 
					                      mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
                      children: [
 | 
					                      children: [
 | 
				
			||||||
@@ -1030,6 +1033,7 @@ class _ArticlePostDisplay extends StatelessWidget {
 | 
				
			|||||||
            MarkdownTextContent(
 | 
					            MarkdownTextContent(
 | 
				
			||||||
              content: item.content!,
 | 
					              content: item.content!,
 | 
				
			||||||
              textStyle: Theme.of(context).textTheme.bodyLarge,
 | 
					              textStyle: Theme.of(context).textTheme.bodyLarge,
 | 
				
			||||||
 | 
					              attachments: item.attachments,
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:island/models/post.dart';
 | 
					import 'package:island/models/post.dart';
 | 
				
			||||||
 | 
					import 'package:island/pods/userinfo.dart';
 | 
				
			||||||
import 'package:island/widgets/content/sheet.dart';
 | 
					import 'package:island/widgets/content/sheet.dart';
 | 
				
			||||||
import 'package:island/widgets/post/post_replies.dart';
 | 
					import 'package:island/widgets/post/post_replies.dart';
 | 
				
			||||||
import 'package:island/widgets/post/post_quick_reply.dart';
 | 
					import 'package:island/widgets/post/post_quick_reply.dart';
 | 
				
			||||||
@@ -14,6 +15,8 @@ class PostRepliesSheet extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
 | 
					    final user = ref.watch(userInfoProvider);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return SheetScaffold(
 | 
					    return SheetScaffold(
 | 
				
			||||||
      titleText: 'repliesCount'.plural(post.repliesCount),
 | 
					      titleText: 'repliesCount'.plural(post.repliesCount),
 | 
				
			||||||
      child: Column(
 | 
					      child: Column(
 | 
				
			||||||
@@ -21,13 +24,16 @@ class PostRepliesSheet extends HookConsumerWidget {
 | 
				
			|||||||
          // Replies list
 | 
					          // Replies list
 | 
				
			||||||
          Expanded(
 | 
					          Expanded(
 | 
				
			||||||
            child: CustomScrollView(
 | 
					            child: CustomScrollView(
 | 
				
			||||||
              slivers: [PostRepliesList(
 | 
					              slivers: [
 | 
				
			||||||
 | 
					                PostRepliesList(
 | 
				
			||||||
                  postId: post.id.toString(),
 | 
					                  postId: post.id.toString(),
 | 
				
			||||||
                  backgroundColor: Colors.transparent,
 | 
					                  backgroundColor: Colors.transparent,
 | 
				
			||||||
              )],
 | 
					                ),
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          // Quick reply section
 | 
					          // Quick reply section
 | 
				
			||||||
 | 
					          if (user.value != null)
 | 
				
			||||||
            Material(
 | 
					            Material(
 | 
				
			||||||
              elevation: 2,
 | 
					              elevation: 2,
 | 
				
			||||||
              child: PostQuickReply(
 | 
					              child: PostQuickReply(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
 | 
				
			|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
					# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
				
			||||||
# In Windows, build-name is used as the major, minor, and patch parts
 | 
					# In Windows, build-name is used as the major, minor, and patch parts
 | 
				
			||||||
# of the product and file versions while build-number is used as the build suffix.
 | 
					# of the product and file versions while build-number is used as the build suffix.
 | 
				
			||||||
version: 3.0.0+110
 | 
					version: 3.0.0+111
 | 
				
			||||||
 | 
					
 | 
				
			||||||
environment:
 | 
					environment:
 | 
				
			||||||
  sdk: ^3.7.2
 | 
					  sdk: ^3.7.2
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user