Compare commits
	
		
			16 Commits
		
	
	
		
			27bc17079e
			...
			3.0.0+111
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 552b4b2572 | |||
| 594ac39e3d | |||
| 23321171f3 | |||
| ee72d79c93 | |||
| a20c2598fc | |||
| 2eba871a6d | |||
| 46919dec31 | |||
| 9dd6cffe0c | |||
| 2ea9f5e907 | |||
| 050750a808 | |||
| f479b9fc8b | |||
| 13ea182707 | |||
| 14183a7316 | |||
| 9fc9b87608 | |||
| 53c2445ba9 | |||
| d414695eb3 | 
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -9,6 +9,7 @@ import 'package:flutter/foundation.dart';
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter/services.dart';
 | 
					import 'package:flutter/services.dart';
 | 
				
			||||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
					import 'package:flutter_hooks/flutter_hooks.dart';
 | 
				
			||||||
 | 
					import 'package:go_router/go_router.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:image_picker_android/image_picker_android.dart';
 | 
					import 'package:image_picker_android/image_picker_android.dart';
 | 
				
			||||||
import 'package:island/firebase_options.dart';
 | 
					import 'package:island/firebase_options.dart';
 | 
				
			||||||
@@ -45,6 +46,10 @@ void main() async {
 | 
				
			|||||||
    FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
 | 
					    FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (kIsWeb) {
 | 
				
			||||||
 | 
					    GoRouter.optionURLReflectsImperativeAPIs = true;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    await EasyLocalization.ensureInitialized();
 | 
					    await EasyLocalization.ensureInitialized();
 | 
				
			||||||
    await Firebase.initializeApp(
 | 
					    await Firebase.initializeApp(
 | 
				
			||||||
@@ -216,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);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -65,6 +65,9 @@ final routerProvider = Provider<GoRouter>((ref) {
 | 
				
			|||||||
            builder:
 | 
					            builder:
 | 
				
			||||||
                (context, state) => PostComposeScreen(
 | 
					                (context, state) => PostComposeScreen(
 | 
				
			||||||
                  initialState: state.extra as PostComposeInitialState?,
 | 
					                  initialState: state.extra as PostComposeInitialState?,
 | 
				
			||||||
 | 
					                  type:
 | 
				
			||||||
 | 
					                      int.tryParse(state.uri.queryParameters['type'] ?? '0') ??
 | 
				
			||||||
 | 
					                      0,
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          GoRoute(
 | 
					          GoRoute(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,22 +1,34 @@
 | 
				
			|||||||
 | 
					import 'package:device_info_plus/device_info_plus.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter/services.dart';
 | 
					import 'package:flutter/services.dart';
 | 
				
			||||||
 | 
					import 'package:gap/gap.dart';
 | 
				
			||||||
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:island/pods/network.dart';
 | 
				
			||||||
 | 
					import 'package:island/services/notify.dart';
 | 
				
			||||||
 | 
					import 'package:island/services/udid.native.dart';
 | 
				
			||||||
 | 
					import 'package:island/widgets/alert.dart';
 | 
				
			||||||
 | 
					import 'package:material_symbols_icons/symbols.dart';
 | 
				
			||||||
import 'package:package_info_plus/package_info_plus.dart';
 | 
					import 'package:package_info_plus/package_info_plus.dart';
 | 
				
			||||||
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
import 'package:url_launcher/url_launcher.dart';
 | 
					import 'package:url_launcher/url_launcher.dart';
 | 
				
			||||||
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AboutScreen extends StatefulWidget {
 | 
					class AboutScreen extends ConsumerStatefulWidget {
 | 
				
			||||||
  const AboutScreen({super.key});
 | 
					  const AboutScreen({super.key});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  State<AboutScreen> createState() => _AboutScreenState();
 | 
					  ConsumerState<AboutScreen> createState() => _AboutScreenState();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _AboutScreenState extends State<AboutScreen> {
 | 
					class _AboutScreenState extends ConsumerState<AboutScreen> {
 | 
				
			||||||
  PackageInfo _packageInfo = PackageInfo(
 | 
					  PackageInfo _packageInfo = PackageInfo(
 | 
				
			||||||
    appName: 'Island',
 | 
					    appName: 'Solian',
 | 
				
			||||||
    packageName: 'com.example.island',
 | 
					    packageName: 'dev.solsynth.solian',
 | 
				
			||||||
    version: '1.0.0',
 | 
					    version: '1.0.0',
 | 
				
			||||||
    buildNumber: '1',
 | 
					    buildNumber: '1',
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					  BaseDeviceInfo? _deviceInfo;
 | 
				
			||||||
 | 
					  String? _deviceUdid;
 | 
				
			||||||
  bool _isLoading = true;
 | 
					  bool _isLoading = true;
 | 
				
			||||||
  String? _errorMessage;
 | 
					  String? _errorMessage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -24,6 +36,7 @@ class _AboutScreenState extends State<AboutScreen> {
 | 
				
			|||||||
  void initState() {
 | 
					  void initState() {
 | 
				
			||||||
    super.initState();
 | 
					    super.initState();
 | 
				
			||||||
    _initPackageInfo();
 | 
					    _initPackageInfo();
 | 
				
			||||||
 | 
					    _initDeviceInfo();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> _initPackageInfo() async {
 | 
					  Future<void> _initPackageInfo() async {
 | 
				
			||||||
@@ -38,13 +51,34 @@ class _AboutScreenState extends State<AboutScreen> {
 | 
				
			|||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      if (mounted) {
 | 
					      if (mounted) {
 | 
				
			||||||
        setState(() {
 | 
					        setState(() {
 | 
				
			||||||
          _errorMessage = 'Failed to load package info: $e';
 | 
					          _errorMessage = 'aboutScreenFailedToLoadPackageInfo'.tr(
 | 
				
			||||||
 | 
					            args: [e.toString()],
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
          _isLoading = false;
 | 
					          _isLoading = false;
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _initDeviceInfo() async {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final deviceInfoPlugin = DeviceInfoPlugin();
 | 
				
			||||||
 | 
					      _deviceInfo = await deviceInfoPlugin.deviceInfo;
 | 
				
			||||||
 | 
					      _deviceUdid = await getUdid();
 | 
				
			||||||
 | 
					      if (mounted) {
 | 
				
			||||||
 | 
					        setState(() {});
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      if (mounted) {
 | 
				
			||||||
 | 
					        setState(() {
 | 
				
			||||||
 | 
					          _errorMessage = 'aboutScreenFailedToLoadDeviceInfo'.tr(
 | 
				
			||||||
 | 
					            args: [e.toString()],
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> _launchURL(String url) async {
 | 
					  Future<void> _launchURL(String url) async {
 | 
				
			||||||
    final uri = Uri.parse(url);
 | 
					    final uri = Uri.parse(url);
 | 
				
			||||||
    if (await canLaunchUrl(uri)) {
 | 
					    if (await canLaunchUrl(uri)) {
 | 
				
			||||||
@@ -57,7 +91,7 @@ class _AboutScreenState extends State<AboutScreen> {
 | 
				
			|||||||
    final theme = Theme.of(context);
 | 
					    final theme = Theme.of(context);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return Scaffold(
 | 
				
			||||||
      appBar: AppBar(title: const Text('About'), elevation: 0),
 | 
					      appBar: AppBar(title: Text('about'.tr()), elevation: 0),
 | 
				
			||||||
      body:
 | 
					      body:
 | 
				
			||||||
          _isLoading
 | 
					          _isLoading
 | 
				
			||||||
              ? const Center(child: CircularProgressIndicator())
 | 
					              ? const Center(child: CircularProgressIndicator())
 | 
				
			||||||
@@ -88,7 +122,9 @@ class _AboutScreenState extends State<AboutScreen> {
 | 
				
			|||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                    Text(
 | 
					                    Text(
 | 
				
			||||||
                      'Version ${_packageInfo.version} (${_packageInfo.buildNumber})',
 | 
					                      'aboutScreenVersionInfo'.tr(
 | 
				
			||||||
 | 
					                        args: [_packageInfo.version, _packageInfo.buildNumber],
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
                      style: theme.textTheme.bodyMedium?.copyWith(
 | 
					                      style: theme.textTheme.bodyMedium?.copyWith(
 | 
				
			||||||
                        color: theme.textTheme.bodySmall?.color,
 | 
					                        color: theme.textTheme.bodySmall?.color,
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
@@ -98,40 +134,81 @@ class _AboutScreenState extends State<AboutScreen> {
 | 
				
			|||||||
                    // App Info Card
 | 
					                    // App Info Card
 | 
				
			||||||
                    _buildSection(
 | 
					                    _buildSection(
 | 
				
			||||||
                      context,
 | 
					                      context,
 | 
				
			||||||
                      title: 'App Information',
 | 
					                      title: 'aboutScreenAppInfoSectionTitle'.tr(),
 | 
				
			||||||
                      children: [
 | 
					                      children: [
 | 
				
			||||||
                        _buildInfoItem(
 | 
					                        _buildInfoItem(
 | 
				
			||||||
                          context,
 | 
					                          context,
 | 
				
			||||||
                          icon: Icons.info_outline,
 | 
					                          icon: Symbols.info,
 | 
				
			||||||
                          label: 'Package Name',
 | 
					                          label: 'aboutScreenPackageNameLabel'.tr(),
 | 
				
			||||||
                          value: _packageInfo.packageName,
 | 
					                          value: _packageInfo.packageName,
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                        _buildInfoItem(
 | 
					                        _buildInfoItem(
 | 
				
			||||||
                          context,
 | 
					                          context,
 | 
				
			||||||
                          icon: Icons.update,
 | 
					                          icon: Symbols.update,
 | 
				
			||||||
                          label: 'Version',
 | 
					                          label: 'aboutScreenVersionLabel'.tr(),
 | 
				
			||||||
                          value: _packageInfo.version,
 | 
					                          value: _packageInfo.version,
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                        _buildInfoItem(
 | 
					                        _buildInfoItem(
 | 
				
			||||||
                          context,
 | 
					                          context,
 | 
				
			||||||
                          icon: Icons.build,
 | 
					                          icon: Symbols.build,
 | 
				
			||||||
                          label: 'Build Number',
 | 
					                          label: 'aboutScreenBuildNumberLabel'.tr(),
 | 
				
			||||||
                          value: _packageInfo.buildNumber,
 | 
					                          value: _packageInfo.buildNumber,
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                      ],
 | 
					                      ],
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if (_deviceInfo != null) const SizedBox(height: 16),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if (_deviceInfo != null)
 | 
				
			||||||
 | 
					                      _buildSection(
 | 
				
			||||||
 | 
					                        context,
 | 
				
			||||||
 | 
					                        title: 'Device Information',
 | 
				
			||||||
 | 
					                        children: [
 | 
				
			||||||
 | 
					                          _buildInfoItem(
 | 
				
			||||||
 | 
					                            context,
 | 
				
			||||||
 | 
					                            icon: Symbols.label,
 | 
				
			||||||
 | 
					                            label: 'Device Name',
 | 
				
			||||||
 | 
					                            value: _deviceInfo?.data['name'],
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                          _buildInfoItem(
 | 
				
			||||||
 | 
					                            context,
 | 
				
			||||||
 | 
					                            icon: Symbols.fingerprint,
 | 
				
			||||||
 | 
					                            label: 'Device Identifier',
 | 
				
			||||||
 | 
					                            value: _deviceUdid ?? 'N/A',
 | 
				
			||||||
 | 
					                            copyable: true,
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                          const Divider(height: 1),
 | 
				
			||||||
 | 
					                          _buildListTile(
 | 
				
			||||||
 | 
					                            context,
 | 
				
			||||||
 | 
					                            icon: Symbols.notifications_active,
 | 
				
			||||||
 | 
					                            title: 'Reactivate Push Notifications',
 | 
				
			||||||
 | 
					                            onTap: () async {
 | 
				
			||||||
 | 
					                              showLoadingModal(context);
 | 
				
			||||||
 | 
					                              try {
 | 
				
			||||||
 | 
					                                await subscribePushNotification(
 | 
				
			||||||
 | 
					                                  ref.watch(apiClientProvider),
 | 
				
			||||||
 | 
					                                );
 | 
				
			||||||
 | 
					                              } catch (err) {
 | 
				
			||||||
 | 
					                                showErrorAlert(err);
 | 
				
			||||||
 | 
					                              } finally {
 | 
				
			||||||
 | 
					                                if (context.mounted) hideLoadingModal(context);
 | 
				
			||||||
 | 
					                              }
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    const SizedBox(height: 16),
 | 
					                    const SizedBox(height: 16),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    // Links Card
 | 
					                    // Links Card
 | 
				
			||||||
                    _buildSection(
 | 
					                    _buildSection(
 | 
				
			||||||
                      context,
 | 
					                      context,
 | 
				
			||||||
                      title: 'Links',
 | 
					                      title: 'aboutScreenLinksSectionTitle'.tr(),
 | 
				
			||||||
                      children: [
 | 
					                      children: [
 | 
				
			||||||
                        _buildListTile(
 | 
					                        _buildListTile(
 | 
				
			||||||
                          context,
 | 
					                          context,
 | 
				
			||||||
                          icon: Icons.privacy_tip_outlined,
 | 
					                          icon: Symbols.privacy_tip,
 | 
				
			||||||
                          title: 'Privacy Policy',
 | 
					                          title: 'aboutScreenPrivacyPolicyTitle'.tr(),
 | 
				
			||||||
                          onTap:
 | 
					                          onTap:
 | 
				
			||||||
                              () => _launchURL(
 | 
					                              () => _launchURL(
 | 
				
			||||||
                                'https://solsynth.dev/terms/privacy-policy',
 | 
					                                'https://solsynth.dev/terms/privacy-policy',
 | 
				
			||||||
@@ -139,17 +216,17 @@ class _AboutScreenState extends State<AboutScreen> {
 | 
				
			|||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                        _buildListTile(
 | 
					                        _buildListTile(
 | 
				
			||||||
                          context,
 | 
					                          context,
 | 
				
			||||||
                          icon: Icons.description_outlined,
 | 
					                          icon: Symbols.description,
 | 
				
			||||||
                          title: 'Terms of Service',
 | 
					                          title: 'aboutScreenTermsOfServiceTitle'.tr(),
 | 
				
			||||||
                          onTap:
 | 
					                          onTap:
 | 
				
			||||||
                              () => _launchURL(
 | 
					                              () => _launchURL(
 | 
				
			||||||
                                'https://example.com/terms/basic-law',
 | 
					                                'https://solsynth.dev/terms/basic-law',
 | 
				
			||||||
                              ),
 | 
					                              ),
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                        _buildListTile(
 | 
					                        _buildListTile(
 | 
				
			||||||
                          context,
 | 
					                          context,
 | 
				
			||||||
                          icon: Icons.code,
 | 
					                          icon: Symbols.code,
 | 
				
			||||||
                          title: 'Open Source Licenses',
 | 
					                          title: 'aboutScreenOpenSourceLicensesTitle'.tr(),
 | 
				
			||||||
                          onTap: () {
 | 
					                          onTap: () {
 | 
				
			||||||
                            showLicensePage(
 | 
					                            showLicensePage(
 | 
				
			||||||
                              context: context,
 | 
					                              context: context,
 | 
				
			||||||
@@ -167,21 +244,22 @@ class _AboutScreenState extends State<AboutScreen> {
 | 
				
			|||||||
                    // Developer Info
 | 
					                    // Developer Info
 | 
				
			||||||
                    _buildSection(
 | 
					                    _buildSection(
 | 
				
			||||||
                      context,
 | 
					                      context,
 | 
				
			||||||
                      title: 'Developer',
 | 
					                      title: 'aboutScreenDeveloperSectionTitle'.tr(),
 | 
				
			||||||
                      children: [
 | 
					                      children: [
 | 
				
			||||||
                        _buildListTile(
 | 
					                        _buildListTile(
 | 
				
			||||||
                          context,
 | 
					                          context,
 | 
				
			||||||
                          icon: Icons.email_outlined,
 | 
					                          icon: Symbols.email,
 | 
				
			||||||
                          title: 'Contact Us',
 | 
					                          title: 'aboutScreenContactUsTitle'.tr(),
 | 
				
			||||||
                          subtitle: 'lily@solsynth.dev',
 | 
					                          subtitle: 'lily@solsynth.dev',
 | 
				
			||||||
                          onTap: () => _launchURL('mailto:lily@solsynth.dev'),
 | 
					                          onTap: () => _launchURL('mailto:lily@solsynth.dev'),
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                        _buildListTile(
 | 
					                        _buildListTile(
 | 
				
			||||||
                          context,
 | 
					                          context,
 | 
				
			||||||
                          icon: Icons.copyright,
 | 
					                          icon: Symbols.copyright,
 | 
				
			||||||
                          title: 'License',
 | 
					                          title: 'aboutScreenLicenseTitle'.tr(),
 | 
				
			||||||
                          subtitle:
 | 
					                          subtitle: 'aboutScreenLicenseContent'.tr(
 | 
				
			||||||
                              'Copyright reserved © ${DateTime.now().year} Solsynth\nGNU Affero General Public License v3.0',
 | 
					                            args: [DateTime.now().year.toString()],
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
                          onTap:
 | 
					                          onTap:
 | 
				
			||||||
                              () => _launchURL(
 | 
					                              () => _launchURL(
 | 
				
			||||||
                                'https://github.com/Solsynth/Solian/blob/v3/LICENSE.txt',
 | 
					                                'https://github.com/Solsynth/Solian/blob/v3/LICENSE.txt',
 | 
				
			||||||
@@ -195,12 +273,25 @@ class _AboutScreenState extends State<AboutScreen> {
 | 
				
			|||||||
                    // Copyright
 | 
					                    // Copyright
 | 
				
			||||||
                    Padding(
 | 
					                    Padding(
 | 
				
			||||||
                      padding: const EdgeInsets.all(16.0),
 | 
					                      padding: const EdgeInsets.all(16.0),
 | 
				
			||||||
                      child: Text(
 | 
					                      child: Column(
 | 
				
			||||||
                        '© ${DateTime.now().year} ${_packageInfo.appName}. All rights reserved.',
 | 
					                        children: [
 | 
				
			||||||
                        style: theme.textTheme.bodySmall,
 | 
					                          Text(
 | 
				
			||||||
                        textAlign: TextAlign.center,
 | 
					                            'aboutScreenCopyright'.tr(
 | 
				
			||||||
 | 
					                              args: [DateTime.now().year.toString()],
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            style: theme.textTheme.bodySmall,
 | 
				
			||||||
 | 
					                            textAlign: TextAlign.center,
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                          const Gap(1),
 | 
				
			||||||
 | 
					                          Text(
 | 
				
			||||||
 | 
					                            'aboutScreenMadeWith'.tr(),
 | 
				
			||||||
 | 
					                            textAlign: TextAlign.center,
 | 
				
			||||||
 | 
					                          ).fontSize(10).opacity(0.8),
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    Gap(MediaQuery.of(context).padding.bottom + 16),
 | 
				
			||||||
                  ],
 | 
					                  ],
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
@@ -238,6 +329,7 @@ class _AboutScreenState extends State<AboutScreen> {
 | 
				
			|||||||
    required IconData icon,
 | 
					    required IconData icon,
 | 
				
			||||||
    required String label,
 | 
					    required String label,
 | 
				
			||||||
    required String value,
 | 
					    required String value,
 | 
				
			||||||
 | 
					    bool copyable = false,
 | 
				
			||||||
  }) {
 | 
					  }) {
 | 
				
			||||||
    return Padding(
 | 
					    return Padding(
 | 
				
			||||||
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
 | 
					      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
 | 
				
			||||||
@@ -254,22 +346,23 @@ class _AboutScreenState extends State<AboutScreen> {
 | 
				
			|||||||
                SelectableText(
 | 
					                SelectableText(
 | 
				
			||||||
                  value,
 | 
					                  value,
 | 
				
			||||||
                  style: Theme.of(context).textTheme.bodyMedium,
 | 
					                  style: Theme.of(context).textTheme.bodyMedium,
 | 
				
			||||||
 | 
					                  maxLines: copyable ? 1 : null,
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ],
 | 
					              ],
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          if (value.startsWith('http') || value.contains('@'))
 | 
					          if (value.startsWith('http') || value.contains('@') || copyable)
 | 
				
			||||||
            IconButton(
 | 
					            IconButton(
 | 
				
			||||||
              icon: const Icon(Icons.copy, size: 16),
 | 
					              icon: const Icon(Symbols.content_copy, size: 16),
 | 
				
			||||||
              onPressed: () {
 | 
					              onPressed: () {
 | 
				
			||||||
                Clipboard.setData(ClipboardData(text: value));
 | 
					                Clipboard.setData(ClipboardData(text: value));
 | 
				
			||||||
                ScaffoldMessenger.of(context).showSnackBar(
 | 
					                ScaffoldMessenger.of(context).showSnackBar(
 | 
				
			||||||
                  const SnackBar(content: Text('Copied to clipboard')),
 | 
					                  SnackBar(content: Text('copiedToClipboard'.tr())),
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
              padding: EdgeInsets.zero,
 | 
					              padding: EdgeInsets.zero,
 | 
				
			||||||
              constraints: const BoxConstraints(),
 | 
					              constraints: const BoxConstraints(),
 | 
				
			||||||
              tooltip: 'Copy to clipboard',
 | 
					              tooltip: 'copyToClipboardTooltip'.tr(),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
@@ -283,13 +376,18 @@ class _AboutScreenState extends State<AboutScreen> {
 | 
				
			|||||||
    String? subtitle,
 | 
					    String? subtitle,
 | 
				
			||||||
    required VoidCallback onTap,
 | 
					    required VoidCallback onTap,
 | 
				
			||||||
  }) {
 | 
					  }) {
 | 
				
			||||||
 | 
					    final multipleLines = subtitle?.contains('\n') ?? false;
 | 
				
			||||||
    return Column(
 | 
					    return Column(
 | 
				
			||||||
      children: [
 | 
					      children: [
 | 
				
			||||||
        ListTile(
 | 
					        ListTile(
 | 
				
			||||||
          leading: Icon(icon),
 | 
					          leading: Icon(icon).padding(top: multipleLines ? 8 : 0),
 | 
				
			||||||
          title: Text(title),
 | 
					          title: Text(title),
 | 
				
			||||||
          subtitle: subtitle != null ? Text(subtitle) : null,
 | 
					          subtitle: subtitle != null ? Text(subtitle) : null,
 | 
				
			||||||
          trailing: const Icon(Icons.chevron_right),
 | 
					          isThreeLine: multipleLines,
 | 
				
			||||||
 | 
					          trailing: const Icon(
 | 
				
			||||||
 | 
					            Symbols.chevron_right,
 | 
				
			||||||
 | 
					          ).padding(top: multipleLines ? 8 : 0),
 | 
				
			||||||
 | 
					          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
 | 
				
			||||||
          onTap: onTap,
 | 
					          onTap: onTap,
 | 
				
			||||||
          contentPadding: const EdgeInsets.symmetric(horizontal: 16),
 | 
					          contentPadding: const EdgeInsets.symmetric(horizontal: 16),
 | 
				
			||||||
          minLeadingWidth: 24,
 | 
					          minLeadingWidth: 24,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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),
 | 
				
			||||||
                TextButton(
 | 
					                Row(
 | 
				
			||||||
                  onPressed: () {
 | 
					                  mainAxisAlignment: MainAxisAlignment.center,
 | 
				
			||||||
                    context.push('/settings');
 | 
					                  children: [
 | 
				
			||||||
                  },
 | 
					                    TextButton(
 | 
				
			||||||
                  child: Text('appSettings').tr(),
 | 
					                      onPressed: () {
 | 
				
			||||||
                ).center(),
 | 
					                        context.push('/about');
 | 
				
			||||||
 | 
					                      },
 | 
				
			||||||
 | 
					                      child: Text('about').tr(),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    TextButton(
 | 
				
			||||||
 | 
					                      onPressed: () {
 | 
				
			||||||
 | 
					                        context.push('/settings');
 | 
				
			||||||
 | 
					                      },
 | 
				
			||||||
 | 
					                      child: Text('appSettings').tr(),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ],
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
              ],
 | 
					              ],
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ).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),
 | 
				
			||||||
                  ],
 | 
					                  ],
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,7 @@
 | 
				
			|||||||
 | 
					import 'dart:io';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:dio/dio.dart';
 | 
					import 'package:dio/dio.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/foundation.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:gap/gap.dart';
 | 
					import 'package:gap/gap.dart';
 | 
				
			||||||
import 'package:google_fonts/google_fonts.dart';
 | 
					import 'package:google_fonts/google_fonts.dart';
 | 
				
			||||||
@@ -14,7 +17,9 @@ import 'package:island/widgets/alert.dart';
 | 
				
			|||||||
import 'package:island/widgets/app_scaffold.dart';
 | 
					import 'package:island/widgets/app_scaffold.dart';
 | 
				
			||||||
import 'package:island/widgets/payment/payment_overlay.dart';
 | 
					import 'package:island/widgets/payment/payment_overlay.dart';
 | 
				
			||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
 | 
					import 'package:material_symbols_icons/symbols.dart';
 | 
				
			||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
					import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
				
			||||||
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
part 'leveling.g.dart';
 | 
					part 'leveling.g.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -84,35 +89,6 @@ class LevelingScreen extends HookConsumerWidget {
 | 
				
			|||||||
                // Membership section
 | 
					                // Membership section
 | 
				
			||||||
                _buildMembershipSection(context, ref, stellarSubscription),
 | 
					                _buildMembershipSection(context, ref, stellarSubscription),
 | 
				
			||||||
                const Gap(16),
 | 
					                const Gap(16),
 | 
				
			||||||
 | 
					 | 
				
			||||||
                // Unlocked features section
 | 
					 | 
				
			||||||
                Container(
 | 
					 | 
				
			||||||
                  width: double.infinity,
 | 
					 | 
				
			||||||
                  padding: const EdgeInsets.all(16),
 | 
					 | 
				
			||||||
                  decoration: BoxDecoration(
 | 
					 | 
				
			||||||
                    color: Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
					 | 
				
			||||||
                    borderRadius: BorderRadius.circular(12),
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                  child: Column(
 | 
					 | 
				
			||||||
                    crossAxisAlignment: CrossAxisAlignment.start,
 | 
					 | 
				
			||||||
                    children: [
 | 
					 | 
				
			||||||
                      Text(
 | 
					 | 
				
			||||||
                        'unlockedFeatures'.tr(),
 | 
					 | 
				
			||||||
                        style: TextStyle(
 | 
					 | 
				
			||||||
                          fontSize: 18,
 | 
					 | 
				
			||||||
                          fontWeight: FontWeight.bold,
 | 
					 | 
				
			||||||
                        ),
 | 
					 | 
				
			||||||
                      ),
 | 
					 | 
				
			||||||
                      const Gap(8),
 | 
					 | 
				
			||||||
                      Text(
 | 
					 | 
				
			||||||
                        'unlockedFeaturesDescription'.tr(),
 | 
					 | 
				
			||||||
                        style: Theme.of(context).textTheme.bodyMedium?.copyWith(
 | 
					 | 
				
			||||||
                          color: Theme.of(context).colorScheme.onSurfaceVariant,
 | 
					 | 
				
			||||||
                        ),
 | 
					 | 
				
			||||||
                      ),
 | 
					 | 
				
			||||||
                    ],
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
              ],
 | 
					              ],
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
@@ -292,6 +268,31 @@ class LevelingScreen extends HookConsumerWidget {
 | 
				
			|||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    final isActive = membership?.isActive ?? false;
 | 
					    final isActive = membership?.isActive ?? false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Future<void> membershipCancel() async {
 | 
				
			||||||
 | 
					      if (!isActive || membership == null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      final confirm = await showConfirmAlert(
 | 
				
			||||||
 | 
					        'membershipCancelHint'.tr(),
 | 
				
			||||||
 | 
					        'membershipCancelConfirm'.tr(),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      if (!confirm || !context.mounted) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        showLoadingModal(context);
 | 
				
			||||||
 | 
					        final client = ref.watch(apiClientProvider);
 | 
				
			||||||
 | 
					        await client.post('/subscriptions/${membership.identifier}/cancel');
 | 
				
			||||||
 | 
					        ref.invalidate(accountStellarSubscriptionProvider);
 | 
				
			||||||
 | 
					        ref.read(userInfoProvider.notifier).fetchUser();
 | 
				
			||||||
 | 
					        if (context.mounted) {
 | 
				
			||||||
 | 
					          hideLoadingModal(context);
 | 
				
			||||||
 | 
					          showSnackBar('membershipCancelSuccess'.tr());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (err) {
 | 
				
			||||||
 | 
					        if (context.mounted) hideLoadingModal(context);
 | 
				
			||||||
 | 
					        showErrorAlert(err);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Container(
 | 
					    return Container(
 | 
				
			||||||
      width: double.infinity,
 | 
					      width: double.infinity,
 | 
				
			||||||
      padding: const EdgeInsets.all(16),
 | 
					      padding: const EdgeInsets.all(16),
 | 
				
			||||||
@@ -307,7 +308,7 @@ class LevelingScreen extends HookConsumerWidget {
 | 
				
			|||||||
        borderRadius: BorderRadius.circular(12),
 | 
					        borderRadius: BorderRadius.circular(12),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      child: Column(
 | 
					      child: Column(
 | 
				
			||||||
        crossAxisAlignment: CrossAxisAlignment.start,
 | 
					        crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
				
			||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          Row(
 | 
					          Row(
 | 
				
			||||||
            children: [
 | 
					            children: [
 | 
				
			||||||
@@ -327,27 +328,42 @@ class LevelingScreen extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
          if (isActive) ...[
 | 
					          if (isActive) ...[
 | 
				
			||||||
            _buildCurrentMembershipCard(context, membership!),
 | 
					            _buildCurrentMembershipCard(context, membership!),
 | 
				
			||||||
            const Gap(16),
 | 
					            const Gap(12),
 | 
				
			||||||
 | 
					            FilledButton.icon(
 | 
				
			||||||
 | 
					              style: ButtonStyle(
 | 
				
			||||||
 | 
					                backgroundColor: WidgetStateProperty.all(
 | 
				
			||||||
 | 
					                  Theme.of(context).colorScheme.error,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                foregroundColor: WidgetStateProperty.all(
 | 
				
			||||||
 | 
					                  Theme.of(context).colorScheme.onError,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              onPressed: membershipCancel,
 | 
				
			||||||
 | 
					              icon: const Icon(Symbols.cancel),
 | 
				
			||||||
 | 
					              label: Text('membershipCancel'.tr()),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
          ],
 | 
					          ],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          Text(
 | 
					          if (!isActive) ...[
 | 
				
			||||||
            isActive ? 'upgradeYourPlan'.tr() : 'chooseYourPlan'.tr(),
 | 
					            Text(
 | 
				
			||||||
            style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
 | 
					              'chooseYourPlan'.tr(),
 | 
				
			||||||
          ),
 | 
					              style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
 | 
				
			||||||
          const Gap(12),
 | 
					            ),
 | 
				
			||||||
 | 
					            const Gap(12),
 | 
				
			||||||
          _buildMembershipTiers(context, ref, membership),
 | 
					            _buildMembershipTiers(context, ref, membership),
 | 
				
			||||||
          const Gap(12),
 | 
					          ],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          // Restore Purchase Button
 | 
					          // Restore Purchase Button
 | 
				
			||||||
          OutlinedButton.icon(
 | 
					          // As you know Apple platform need IAP
 | 
				
			||||||
            onPressed: () => _showRestorePurchaseSheet(context, ref),
 | 
					          if (kIsWeb || !(Platform.isIOS || Platform.isMacOS))
 | 
				
			||||||
            icon: const Icon(Icons.restore),
 | 
					            OutlinedButton.icon(
 | 
				
			||||||
            label: Text('restorePurchase'.tr()),
 | 
					              onPressed: () => _showRestorePurchaseSheet(context, ref),
 | 
				
			||||||
            style: OutlinedButton.styleFrom(
 | 
					              icon: const Icon(Icons.restore),
 | 
				
			||||||
              minimumSize: const Size(double.infinity, 48),
 | 
					              label: Text('restorePurchase'.tr()),
 | 
				
			||||||
            ),
 | 
					              style: OutlinedButton.styleFrom(
 | 
				
			||||||
          ),
 | 
					                minimumSize: const Size(double.infinity, 48),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ).padding(top: 12),
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
@@ -410,33 +426,18 @@ class LevelingScreen extends HookConsumerWidget {
 | 
				
			|||||||
        'id': 'solian.stellar.primary',
 | 
					        'id': 'solian.stellar.primary',
 | 
				
			||||||
        'name': 'membershipTierStellar'.tr(),
 | 
					        'name': 'membershipTierStellar'.tr(),
 | 
				
			||||||
        'price': 'membershipPriceStellar'.tr(),
 | 
					        'price': 'membershipPriceStellar'.tr(),
 | 
				
			||||||
        'features': [
 | 
					 | 
				
			||||||
          'membershipFeatureBasic'.tr(),
 | 
					 | 
				
			||||||
          'membershipFeaturePrioritySupport'.tr(),
 | 
					 | 
				
			||||||
          'membershipFeatureAdFree'.tr(),
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
        'color': Colors.blue,
 | 
					        'color': Colors.blue,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        'id': 'solian.stellar.nova',
 | 
					        'id': 'solian.stellar.nova',
 | 
				
			||||||
        'name': 'membershipTierNova'.tr(),
 | 
					        'name': 'membershipTierNova'.tr(),
 | 
				
			||||||
        'price': 'membershipPriceNova'.tr(),
 | 
					        'price': 'membershipPriceNova'.tr(),
 | 
				
			||||||
        'features': [
 | 
					        'color': Colors.indigo,
 | 
				
			||||||
          'membershipFeatureAllPrimary'.tr(),
 | 
					 | 
				
			||||||
          'membershipFeatureAdvancedCustomization'.tr(),
 | 
					 | 
				
			||||||
          'membershipFeatureEarlyAccess'.tr(),
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
        'color': Colors.purple,
 | 
					 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        'id': 'solian.stellar.supernova',
 | 
					        'id': 'solian.stellar.supernova',
 | 
				
			||||||
        'name': 'membershipTierSupernova'.tr(),
 | 
					        'name': 'membershipTierSupernova'.tr(),
 | 
				
			||||||
        'price': 'membershipPriceSupernova'.tr(),
 | 
					        'price': 'membershipPriceSupernova'.tr(),
 | 
				
			||||||
        'features': [
 | 
					 | 
				
			||||||
          'membershipFeatureAllNova'.tr(),
 | 
					 | 
				
			||||||
          'membershipFeatureExclusiveContent'.tr(),
 | 
					 | 
				
			||||||
          'membershipFeatureVipSupport'.tr(),
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
        'color': Colors.orange,
 | 
					        'color': Colors.orange,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,56 +385,60 @@ class AccountProfileScreen extends HookConsumerWidget {
 | 
				
			|||||||
                  ).padding(horizontal: 24),
 | 
					                  ).padding(horizontal: 24),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                SliverToBoxAdapter(
 | 
					                if (user.value != null)
 | 
				
			||||||
                  child: const Divider(height: 1).padding(top: 24, bottom: 12),
 | 
					                  SliverToBoxAdapter(
 | 
				
			||||||
                ),
 | 
					                    child: const Divider(
 | 
				
			||||||
                SliverToBoxAdapter(
 | 
					                      height: 1,
 | 
				
			||||||
                  child: Row(
 | 
					                    ).padding(top: 24, bottom: 12),
 | 
				
			||||||
                    spacing: 8,
 | 
					                  ),
 | 
				
			||||||
                    children: [
 | 
					                if (user.value != null)
 | 
				
			||||||
                      Expanded(
 | 
					                  SliverToBoxAdapter(
 | 
				
			||||||
                        child: FilledButton.icon(
 | 
					                    child: Row(
 | 
				
			||||||
                          style: ButtonStyle(
 | 
					                      spacing: 8,
 | 
				
			||||||
                            backgroundColor: WidgetStatePropertyAll(
 | 
					                      children: [
 | 
				
			||||||
                              accountRelationship.value == null
 | 
					                        Expanded(
 | 
				
			||||||
                                  ? null
 | 
					                          child: FilledButton.icon(
 | 
				
			||||||
                                  : Theme.of(context).colorScheme.secondary,
 | 
					                            style: ButtonStyle(
 | 
				
			||||||
                            ),
 | 
					                              backgroundColor: WidgetStatePropertyAll(
 | 
				
			||||||
                            foregroundColor: WidgetStatePropertyAll(
 | 
					 | 
				
			||||||
                              accountRelationship.value == null
 | 
					 | 
				
			||||||
                                  ? null
 | 
					 | 
				
			||||||
                                  : Theme.of(context).colorScheme.onSecondary,
 | 
					 | 
				
			||||||
                            ),
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                          onPressed: relationshipAction,
 | 
					 | 
				
			||||||
                          label:
 | 
					 | 
				
			||||||
                              Text(
 | 
					 | 
				
			||||||
                                accountRelationship.value == null
 | 
					                                accountRelationship.value == null
 | 
				
			||||||
                                    ? 'addFriendShort'
 | 
					                                    ? null
 | 
				
			||||||
                                    : 'added',
 | 
					                                    : Theme.of(context).colorScheme.secondary,
 | 
				
			||||||
                              ).tr(),
 | 
					                              ),
 | 
				
			||||||
                          icon:
 | 
					                              foregroundColor: WidgetStatePropertyAll(
 | 
				
			||||||
                              accountRelationship.value == null
 | 
					                                accountRelationship.value == null
 | 
				
			||||||
                                  ? const Icon(Symbols.person_add)
 | 
					                                    ? null
 | 
				
			||||||
                                  : const Icon(Symbols.person_check),
 | 
					                                    : Theme.of(context).colorScheme.onSecondary,
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            onPressed: relationshipAction,
 | 
				
			||||||
 | 
					                            label:
 | 
				
			||||||
 | 
					                                Text(
 | 
				
			||||||
 | 
					                                  accountRelationship.value == null
 | 
				
			||||||
 | 
					                                      ? 'addFriendShort'
 | 
				
			||||||
 | 
					                                      : 'added',
 | 
				
			||||||
 | 
					                                ).tr(),
 | 
				
			||||||
 | 
					                            icon:
 | 
				
			||||||
 | 
					                                accountRelationship.value == null
 | 
				
			||||||
 | 
					                                    ? const Icon(Symbols.person_add)
 | 
				
			||||||
 | 
					                                    : const Icon(Symbols.person_check),
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                      ),
 | 
					                        Expanded(
 | 
				
			||||||
                      Expanded(
 | 
					                          child: FilledButton.icon(
 | 
				
			||||||
                        child: FilledButton.icon(
 | 
					                            onPressed: directMessageAction,
 | 
				
			||||||
                          onPressed: directMessageAction,
 | 
					                            icon: const Icon(Symbols.message),
 | 
				
			||||||
                          icon: const Icon(Symbols.message),
 | 
					                            label:
 | 
				
			||||||
                          label:
 | 
					                                Text(
 | 
				
			||||||
                              Text(
 | 
					                                  accountChat.value == null
 | 
				
			||||||
                                accountChat.value == null
 | 
					                                      ? 'createDirectMessage'
 | 
				
			||||||
                                    ? 'createDirectMessage'
 | 
					                                      : 'gotoDirectMessage',
 | 
				
			||||||
                                    : 'gotoDirectMessage',
 | 
					                                  maxLines: 1,
 | 
				
			||||||
                                maxLines: 1,
 | 
					                                ).tr(),
 | 
				
			||||||
                              ).tr(),
 | 
					                          ),
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                      ),
 | 
					                      ],
 | 
				
			||||||
                    ],
 | 
					                    ).padding(horizontal: 16),
 | 
				
			||||||
                  ).padding(horizontal: 16),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
                SliverToBoxAdapter(
 | 
					                SliverToBoxAdapter(
 | 
				
			||||||
                  child: const Divider(height: 1).padding(top: 12),
 | 
					                  child: const Divider(height: 1).padding(top: 12),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -51,54 +51,59 @@ class _ArticleDetailContent extends HookConsumerWidget {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return SingleChildScrollView(
 | 
					    return SingleChildScrollView(
 | 
				
			||||||
      child: Column(
 | 
					      child: Center(
 | 
				
			||||||
        crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
					        child: ConstrainedBox(
 | 
				
			||||||
        children: [
 | 
					          constraints: const BoxConstraints(maxWidth: 560),
 | 
				
			||||||
          if (article.preview?.imageUrl != null)
 | 
					          child: Column(
 | 
				
			||||||
            Image.network(
 | 
					            crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
				
			||||||
              article.preview!.imageUrl!,
 | 
					            children: [
 | 
				
			||||||
              width: double.infinity,
 | 
					              if (article.preview?.imageUrl != null)
 | 
				
			||||||
              height: 200,
 | 
					                Image.network(
 | 
				
			||||||
              fit: BoxFit.cover,
 | 
					                  article.preview!.imageUrl!,
 | 
				
			||||||
            ),
 | 
					                  width: double.infinity,
 | 
				
			||||||
          Padding(
 | 
					                  height: 200,
 | 
				
			||||||
            padding: const EdgeInsets.all(16.0),
 | 
					                  fit: BoxFit.cover,
 | 
				
			||||||
            child: Column(
 | 
					 | 
				
			||||||
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
					 | 
				
			||||||
              children: [
 | 
					 | 
				
			||||||
                Text(
 | 
					 | 
				
			||||||
                  article.title,
 | 
					 | 
				
			||||||
                  style: Theme.of(context).textTheme.headlineSmall,
 | 
					 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                const SizedBox(height: 8),
 | 
					              Padding(
 | 
				
			||||||
                if (article.feed?.title != null)
 | 
					                padding: const EdgeInsets.all(16.0),
 | 
				
			||||||
                  Text(
 | 
					                child: Column(
 | 
				
			||||||
                    article.feed!.title,
 | 
					                  crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
                    style: Theme.of(context).textTheme.bodyMedium?.copyWith(
 | 
					                  children: [
 | 
				
			||||||
                      color: Theme.of(context).colorScheme.onSurfaceVariant,
 | 
					                    Text(
 | 
				
			||||||
 | 
					                      article.title,
 | 
				
			||||||
 | 
					                      style: Theme.of(context).textTheme.headlineSmall,
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                  ),
 | 
					                    const SizedBox(height: 8),
 | 
				
			||||||
                const Divider(height: 32),
 | 
					                    if (article.feed?.title != null)
 | 
				
			||||||
                if (article.content != null)
 | 
					                      Text(
 | 
				
			||||||
                  ...MarkdownTextContent.buildGenerator(
 | 
					                        article.feed!.title,
 | 
				
			||||||
                    isDark: Theme.of(context).brightness == Brightness.dark,
 | 
					                        style: Theme.of(context).textTheme.bodyMedium?.copyWith(
 | 
				
			||||||
                  ).buildWidgets(markdownContent)
 | 
					                          color: Theme.of(context).colorScheme.onSurfaceVariant,
 | 
				
			||||||
                else if (article.preview?.description != null)
 | 
					                        ),
 | 
				
			||||||
                  Text(article.preview!.description!),
 | 
					 | 
				
			||||||
                const Gap(24),
 | 
					 | 
				
			||||||
                FilledButton(
 | 
					 | 
				
			||||||
                  onPressed:
 | 
					 | 
				
			||||||
                      () => launchUrlString(
 | 
					 | 
				
			||||||
                        article.url,
 | 
					 | 
				
			||||||
                        mode: LaunchMode.externalApplication,
 | 
					 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                  child: const Text('Read Full Article'),
 | 
					                    const Divider(height: 32),
 | 
				
			||||||
 | 
					                    if (article.content != null)
 | 
				
			||||||
 | 
					                      ...MarkdownTextContent.buildGenerator(
 | 
				
			||||||
 | 
					                        isDark: Theme.of(context).brightness == Brightness.dark,
 | 
				
			||||||
 | 
					                      ).buildWidgets(markdownContent)
 | 
				
			||||||
 | 
					                    else if (article.preview?.description != null)
 | 
				
			||||||
 | 
					                      Text(article.preview!.description!),
 | 
				
			||||||
 | 
					                    const Gap(24),
 | 
				
			||||||
 | 
					                    FilledButton(
 | 
				
			||||||
 | 
					                      onPressed:
 | 
				
			||||||
 | 
					                          () => launchUrlString(
 | 
				
			||||||
 | 
					                            article.url,
 | 
				
			||||||
 | 
					                            mode: LaunchMode.externalApplication,
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                      child: const Text('Read Full Article'),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    Gap(MediaQuery.of(context).padding.bottom),
 | 
				
			||||||
 | 
					                  ],
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                Gap(MediaQuery.of(context).padding.bottom),
 | 
					              ),
 | 
				
			||||||
              ],
 | 
					            ],
 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
        ],
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -108,15 +108,18 @@ class CreatorHubShellScreen extends StatelessWidget {
 | 
				
			|||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    final isWide = isWideScreen(context);
 | 
					    final isWide = isWideScreen(context);
 | 
				
			||||||
    if (isWide) {
 | 
					    if (isWide) {
 | 
				
			||||||
      return Row(
 | 
					      return AppBackground(
 | 
				
			||||||
        children: [
 | 
					        isRoot: true,
 | 
				
			||||||
          SizedBox(width: 360, child: const CreatorHubScreen(isAside: true)),
 | 
					        child: Row(
 | 
				
			||||||
          const VerticalDivider(width: 1),
 | 
					          children: [
 | 
				
			||||||
          Expanded(child: child),
 | 
					            SizedBox(width: 360, child: const CreatorHubScreen(isAside: true)),
 | 
				
			||||||
        ],
 | 
					            const VerticalDivider(width: 1),
 | 
				
			||||||
 | 
					            Expanded(child: child),
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return child;
 | 
					    return AppBackground(isRoot: true, child: child);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -198,7 +201,6 @@ class CreatorHubScreen extends HookConsumerWidget {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return AppScaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      noBackground: false,
 | 
					 | 
				
			||||||
      appBar: AppBar(
 | 
					      appBar: AppBar(
 | 
				
			||||||
        leading: !isWide ? const PageBackButton() : null,
 | 
					        leading: !isWide ? const PageBackButton() : null,
 | 
				
			||||||
        title: Text('creatorHub').tr(),
 | 
					        title: Text('creatorHub').tr(),
 | 
				
			||||||
@@ -322,9 +324,7 @@ class CreatorHubScreen extends HookConsumerWidget {
 | 
				
			|||||||
                            subtitle: Text('createPublisherHint').tr(),
 | 
					                            subtitle: Text('createPublisherHint').tr(),
 | 
				
			||||||
                            trailing: const Icon(Symbols.chevron_right),
 | 
					                            trailing: const Icon(Symbols.chevron_right),
 | 
				
			||||||
                            onTap: () {
 | 
					                            onTap: () {
 | 
				
			||||||
                              context.push('/creators/publishers/new').then((
 | 
					                              context.push('/creators/new').then((value) {
 | 
				
			||||||
                                value,
 | 
					 | 
				
			||||||
                              ) {
 | 
					 | 
				
			||||||
                                if (value != null) {
 | 
					                                if (value != null) {
 | 
				
			||||||
                                  ref.invalidate(publishersManagedProvider);
 | 
					                                  ref.invalidate(publishersManagedProvider);
 | 
				
			||||||
                                }
 | 
					                                }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,7 +26,7 @@ class CreatorPostListScreen extends HookConsumerWidget {
 | 
				
			|||||||
                children: [
 | 
					                children: [
 | 
				
			||||||
                  ListTile(
 | 
					                  ListTile(
 | 
				
			||||||
                    leading: const Icon(Symbols.edit),
 | 
					                    leading: const Icon(Symbols.edit),
 | 
				
			||||||
                    title: Text('postContent'.tr()),
 | 
					                    title: Text('Post'),
 | 
				
			||||||
                    subtitle: Text('Create a regular post'),
 | 
					                    subtitle: Text('Create a regular post'),
 | 
				
			||||||
                    onTap: () async {
 | 
					                    onTap: () async {
 | 
				
			||||||
                      Navigator.pop(context);
 | 
					                      Navigator.pop(context);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -47,15 +47,21 @@ class DeveloperHubShellScreen extends StatelessWidget {
 | 
				
			|||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    final isWide = isWideScreen(context);
 | 
					    final isWide = isWideScreen(context);
 | 
				
			||||||
    if (isWide) {
 | 
					    if (isWide) {
 | 
				
			||||||
      return Row(
 | 
					      return AppBackground(
 | 
				
			||||||
        children: [
 | 
					        isRoot: true,
 | 
				
			||||||
          SizedBox(width: 360, child: const DeveloperHubScreen(isAside: true)),
 | 
					        child: Row(
 | 
				
			||||||
          const VerticalDivider(width: 1),
 | 
					          children: [
 | 
				
			||||||
          Expanded(child: child),
 | 
					            SizedBox(
 | 
				
			||||||
        ],
 | 
					              width: 360,
 | 
				
			||||||
 | 
					              child: const DeveloperHubScreen(isAside: true),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            const VerticalDivider(width: 1),
 | 
				
			||||||
 | 
					            Expanded(child: child),
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return child;
 | 
					    return AppBackground(isRoot: true, child: child);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -238,8 +244,8 @@ class DeveloperHubScreen extends HookConsumerWidget {
 | 
				
			|||||||
                            ),
 | 
					                            ),
 | 
				
			||||||
                            onTap: () {
 | 
					                            onTap: () {
 | 
				
			||||||
                              context.push(
 | 
					                              context.push(
 | 
				
			||||||
                          '/developers/${currentDeveloper.value!.name}/apps',
 | 
					                                '/developers/${currentDeveloper.value!.name}/apps',
 | 
				
			||||||
                        );
 | 
					                              );
 | 
				
			||||||
                            },
 | 
					                            },
 | 
				
			||||||
                          ),
 | 
					                          ),
 | 
				
			||||||
                        ],
 | 
					                        ],
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -126,16 +126,21 @@ 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(
 | 
				
			||||||
        slivers: [
 | 
					        child: ConstrainedBox(
 | 
				
			||||||
          SliverPadding(
 | 
					          constraints: const BoxConstraints(maxWidth: 560),
 | 
				
			||||||
            padding: const EdgeInsets.only(top: 8, left: 8, right: 8),
 | 
					          child: CustomScrollView(
 | 
				
			||||||
            sliver: SliverArticlesList(
 | 
					            slivers: [
 | 
				
			||||||
              feedId: feedId,
 | 
					              SliverPadding(
 | 
				
			||||||
              publisherId: publisherId,
 | 
					                padding: const EdgeInsets.only(top: 8, left: 8, right: 8),
 | 
				
			||||||
            ),
 | 
					                sliver: SliverArticlesList(
 | 
				
			||||||
 | 
					                  feedId: feedId,
 | 
				
			||||||
 | 
					                  publisherId: publisherId,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
        ],
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -33,6 +33,8 @@ sealed class PostComposeInitialState with _$PostComposeInitialState {
 | 
				
			|||||||
    String? content,
 | 
					    String? content,
 | 
				
			||||||
    @Default([]) List<UniversalFile> attachments,
 | 
					    @Default([]) List<UniversalFile> attachments,
 | 
				
			||||||
    int? visibility,
 | 
					    int? visibility,
 | 
				
			||||||
 | 
					    SnPost? replyingTo,
 | 
				
			||||||
 | 
					    SnPost? forwardingTo,
 | 
				
			||||||
  }) = _PostComposeInitialState;
 | 
					  }) = _PostComposeInitialState;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory PostComposeInitialState.fromJson(Map<String, dynamic> json) =>
 | 
					  factory PostComposeInitialState.fromJson(Map<String, dynamic> json) =>
 | 
				
			||||||
@@ -66,23 +68,22 @@ class PostEditScreen extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class PostComposeScreen extends HookConsumerWidget {
 | 
					class PostComposeScreen extends HookConsumerWidget {
 | 
				
			||||||
  final SnPost? originalPost;
 | 
					  final SnPost? originalPost;
 | 
				
			||||||
  final SnPost? repliedPost;
 | 
					 | 
				
			||||||
  final SnPost? forwardedPost;
 | 
					 | 
				
			||||||
  final int? type;
 | 
					  final int? type;
 | 
				
			||||||
  final PostComposeInitialState? initialState;
 | 
					  final PostComposeInitialState? initialState;
 | 
				
			||||||
  const PostComposeScreen({
 | 
					  const PostComposeScreen({
 | 
				
			||||||
    super.key,
 | 
					    super.key,
 | 
				
			||||||
    this.originalPost,
 | 
					 | 
				
			||||||
    this.repliedPost,
 | 
					 | 
				
			||||||
    this.forwardedPost,
 | 
					 | 
				
			||||||
    this.type,
 | 
					    this.type,
 | 
				
			||||||
    this.initialState,
 | 
					    this.initialState,
 | 
				
			||||||
 | 
					    this.originalPost,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    // Determine the compose type: auto-detect from edited post or use query parameter
 | 
					    // Determine the compose type: auto-detect from edited post or use query parameter
 | 
				
			||||||
    final composeType = originalPost?.type ?? type ?? 0;
 | 
					    final composeType = originalPost?.type ?? type ?? 0;
 | 
				
			||||||
 | 
					    final repliedPost = initialState?.replyingTo ?? originalPost?.repliedPost;
 | 
				
			||||||
 | 
					    final forwardedPost =
 | 
				
			||||||
 | 
					        initialState?.forwardingTo ?? originalPost?.forwardedPost;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // If type is 1 (article), return ArticleComposeScreen
 | 
					    // If type is 1 (article), return ArticleComposeScreen
 | 
				
			||||||
    if (composeType == 1) {
 | 
					    if (composeType == 1) {
 | 
				
			||||||
@@ -136,7 +137,10 @@ class PostComposeScreen extends HookConsumerWidget {
 | 
				
			|||||||
    // Initialize publisher once when data is available
 | 
					    // Initialize publisher once when data is available
 | 
				
			||||||
    useEffect(() {
 | 
					    useEffect(() {
 | 
				
			||||||
      if (publishers.value?.isNotEmpty ?? false) {
 | 
					      if (publishers.value?.isNotEmpty ?? false) {
 | 
				
			||||||
        state.currentPublisher.value = publishers.value!.first;
 | 
					        if (state.currentPublisher.value == null) {
 | 
				
			||||||
 | 
					          // If no publisher is set, use the first available one
 | 
				
			||||||
 | 
					          state.currentPublisher.value = publishers.value!.first;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return null;
 | 
					      return null;
 | 
				
			||||||
    }, [publishers]);
 | 
					    }, [publishers]);
 | 
				
			||||||
@@ -480,8 +484,10 @@ class PostComposeScreen extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  Widget _buildInfoBanner(BuildContext context) {
 | 
					  Widget _buildInfoBanner(BuildContext context) {
 | 
				
			||||||
    // When editing, preserve the original replied/forwarded post references
 | 
					    // When editing, preserve the original replied/forwarded post references
 | 
				
			||||||
    final effectiveRepliedPost = repliedPost ?? originalPost?.repliedPost;
 | 
					    final effectiveRepliedPost =
 | 
				
			||||||
    final effectiveForwardedPost = forwardedPost ?? originalPost?.forwardedPost;
 | 
					        initialState?.replyingTo ?? originalPost?.repliedPost;
 | 
				
			||||||
 | 
					    final effectiveForwardedPost =
 | 
				
			||||||
 | 
					        initialState?.forwardingTo ?? originalPost?.forwardedPost;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Show editing banner when editing a post
 | 
					    // Show editing banner when editing a post
 | 
				
			||||||
    if (originalPost != null) {
 | 
					    if (originalPost != null) {
 | 
				
			||||||
@@ -497,15 +503,15 @@ class PostComposeScreen extends HookConsumerWidget {
 | 
				
			|||||||
                  size: 16,
 | 
					                  size: 16,
 | 
				
			||||||
                  color: Theme.of(context).colorScheme.onPrimaryContainer,
 | 
					                  color: Theme.of(context).colorScheme.onPrimaryContainer,
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                const Gap(4),
 | 
					                const Gap(8),
 | 
				
			||||||
                Text(
 | 
					                Text(
 | 
				
			||||||
                  'edit'.tr(),
 | 
					                  'postEditing'.tr(),
 | 
				
			||||||
                  style: Theme.of(context).textTheme.labelMedium?.copyWith(
 | 
					                  style: Theme.of(context).textTheme.labelMedium?.copyWith(
 | 
				
			||||||
                    color: Theme.of(context).colorScheme.onPrimaryContainer,
 | 
					                    color: Theme.of(context).colorScheme.onPrimaryContainer,
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ],
 | 
					              ],
 | 
				
			||||||
            ).padding(all: 16),
 | 
					            ).padding(horizontal: 16, vertical: 8),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          // Show reply/forward banners below editing banner if they exist
 | 
					          // Show reply/forward banners below editing banner if they exist
 | 
				
			||||||
          if (effectiveRepliedPost != null)
 | 
					          if (effectiveRepliedPost != null)
 | 
				
			||||||
@@ -615,6 +621,7 @@ class PostComposeScreen extends HookConsumerWidget {
 | 
				
			|||||||
        showModalBottomSheet(
 | 
					        showModalBottomSheet(
 | 
				
			||||||
          context: context,
 | 
					          context: context,
 | 
				
			||||||
          isScrollControlled: true,
 | 
					          isScrollControlled: true,
 | 
				
			||||||
 | 
					          backgroundColor: Colors.transparent,
 | 
				
			||||||
          builder:
 | 
					          builder:
 | 
				
			||||||
              (context) => DraggableScrollableSheet(
 | 
					              (context) => DraggableScrollableSheet(
 | 
				
			||||||
                initialChildSize: 0.7,
 | 
					                initialChildSize: 0.7,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,7 +16,7 @@ T _$identity<T>(T value) => value;
 | 
				
			|||||||
/// @nodoc
 | 
					/// @nodoc
 | 
				
			||||||
mixin _$PostComposeInitialState {
 | 
					mixin _$PostComposeInitialState {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 String? get title; String? get description; String? get content; List<UniversalFile> get attachments; int? get visibility;
 | 
					 String? get title; String? get description; String? get content; List<UniversalFile> get attachments; int? get visibility; SnPost? get replyingTo; SnPost? get forwardingTo;
 | 
				
			||||||
/// Create a copy of PostComposeInitialState
 | 
					/// Create a copy of PostComposeInitialState
 | 
				
			||||||
/// with the given fields replaced by the non-null parameter values.
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
@JsonKey(includeFromJson: false, includeToJson: false)
 | 
					@JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
@@ -29,16 +29,16 @@ $PostComposeInitialStateCopyWith<PostComposeInitialState> get copyWith => _$Post
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@override
 | 
					@override
 | 
				
			||||||
bool operator ==(Object other) {
 | 
					bool operator ==(Object other) {
 | 
				
			||||||
  return identical(this, other) || (other.runtimeType == runtimeType&&other is PostComposeInitialState&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.content, content) || other.content == content)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&(identical(other.visibility, visibility) || other.visibility == visibility));
 | 
					  return identical(this, other) || (other.runtimeType == runtimeType&&other is PostComposeInitialState&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.content, content) || other.content == content)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.replyingTo, replyingTo) || other.replyingTo == replyingTo)&&(identical(other.forwardingTo, forwardingTo) || other.forwardingTo == forwardingTo));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@JsonKey(includeFromJson: false, includeToJson: false)
 | 
					@JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
@override
 | 
					@override
 | 
				
			||||||
int get hashCode => Object.hash(runtimeType,title,description,content,const DeepCollectionEquality().hash(attachments),visibility);
 | 
					int get hashCode => Object.hash(runtimeType,title,description,content,const DeepCollectionEquality().hash(attachments),visibility,replyingTo,forwardingTo);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@override
 | 
					@override
 | 
				
			||||||
String toString() {
 | 
					String toString() {
 | 
				
			||||||
  return 'PostComposeInitialState(title: $title, description: $description, content: $content, attachments: $attachments, visibility: $visibility)';
 | 
					  return 'PostComposeInitialState(title: $title, description: $description, content: $content, attachments: $attachments, visibility: $visibility, replyingTo: $replyingTo, forwardingTo: $forwardingTo)';
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -49,11 +49,11 @@ abstract mixin class $PostComposeInitialStateCopyWith<$Res>  {
 | 
				
			|||||||
  factory $PostComposeInitialStateCopyWith(PostComposeInitialState value, $Res Function(PostComposeInitialState) _then) = _$PostComposeInitialStateCopyWithImpl;
 | 
					  factory $PostComposeInitialStateCopyWith(PostComposeInitialState value, $Res Function(PostComposeInitialState) _then) = _$PostComposeInitialStateCopyWithImpl;
 | 
				
			||||||
@useResult
 | 
					@useResult
 | 
				
			||||||
$Res call({
 | 
					$Res call({
 | 
				
			||||||
 String? title, String? description, String? content, List<UniversalFile> attachments, int? visibility
 | 
					 String? title, String? description, String? content, List<UniversalFile> attachments, int? visibility, SnPost? replyingTo, SnPost? forwardingTo
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$SnPostCopyWith<$Res>? get replyingTo;$SnPostCopyWith<$Res>? get forwardingTo;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
/// @nodoc
 | 
					/// @nodoc
 | 
				
			||||||
@@ -66,17 +66,43 @@ class _$PostComposeInitialStateCopyWithImpl<$Res>
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
/// Create a copy of PostComposeInitialState
 | 
					/// Create a copy of PostComposeInitialState
 | 
				
			||||||
/// with the given fields replaced by the non-null parameter values.
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
@pragma('vm:prefer-inline') @override $Res call({Object? title = freezed,Object? description = freezed,Object? content = freezed,Object? attachments = null,Object? visibility = freezed,}) {
 | 
					@pragma('vm:prefer-inline') @override $Res call({Object? title = freezed,Object? description = freezed,Object? content = freezed,Object? attachments = null,Object? visibility = freezed,Object? replyingTo = freezed,Object? forwardingTo = freezed,}) {
 | 
				
			||||||
  return _then(_self.copyWith(
 | 
					  return _then(_self.copyWith(
 | 
				
			||||||
title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
 | 
					title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
 | 
					as String?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String?,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
 | 
					as String?,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String?,attachments: null == attachments ? _self.attachments : attachments // ignore: cast_nullable_to_non_nullable
 | 
					as String?,attachments: null == attachments ? _self.attachments : attachments // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as List<UniversalFile>,visibility: freezed == visibility ? _self.visibility : visibility // ignore: cast_nullable_to_non_nullable
 | 
					as List<UniversalFile>,visibility: freezed == visibility ? _self.visibility : visibility // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as int?,
 | 
					as int?,replyingTo: freezed == replyingTo ? _self.replyingTo : replyingTo // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as SnPost?,forwardingTo: freezed == forwardingTo ? _self.forwardingTo : forwardingTo // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as SnPost?,
 | 
				
			||||||
  ));
 | 
					  ));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					/// Create a copy of PostComposeInitialState
 | 
				
			||||||
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					@pragma('vm:prefer-inline')
 | 
				
			||||||
 | 
					$SnPostCopyWith<$Res>? get replyingTo {
 | 
				
			||||||
 | 
					    if (_self.replyingTo == null) {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return $SnPostCopyWith<$Res>(_self.replyingTo!, (value) {
 | 
				
			||||||
 | 
					    return _then(_self.copyWith(replyingTo: value));
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}/// Create a copy of PostComposeInitialState
 | 
				
			||||||
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					@pragma('vm:prefer-inline')
 | 
				
			||||||
 | 
					$SnPostCopyWith<$Res>? get forwardingTo {
 | 
				
			||||||
 | 
					    if (_self.forwardingTo == null) {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return $SnPostCopyWith<$Res>(_self.forwardingTo!, (value) {
 | 
				
			||||||
 | 
					    return _then(_self.copyWith(forwardingTo: value));
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -84,7 +110,7 @@ as int?,
 | 
				
			|||||||
@JsonSerializable()
 | 
					@JsonSerializable()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _PostComposeInitialState implements PostComposeInitialState {
 | 
					class _PostComposeInitialState implements PostComposeInitialState {
 | 
				
			||||||
  const _PostComposeInitialState({this.title, this.description, this.content, final  List<UniversalFile> attachments = const [], this.visibility}): _attachments = attachments;
 | 
					  const _PostComposeInitialState({this.title, this.description, this.content, final  List<UniversalFile> attachments = const [], this.visibility, this.replyingTo, this.forwardingTo}): _attachments = attachments;
 | 
				
			||||||
  factory _PostComposeInitialState.fromJson(Map<String, dynamic> json) => _$PostComposeInitialStateFromJson(json);
 | 
					  factory _PostComposeInitialState.fromJson(Map<String, dynamic> json) => _$PostComposeInitialStateFromJson(json);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@override final  String? title;
 | 
					@override final  String? title;
 | 
				
			||||||
@@ -98,6 +124,8 @@ class _PostComposeInitialState implements PostComposeInitialState {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@override final  int? visibility;
 | 
					@override final  int? visibility;
 | 
				
			||||||
 | 
					@override final  SnPost? replyingTo;
 | 
				
			||||||
 | 
					@override final  SnPost? forwardingTo;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Create a copy of PostComposeInitialState
 | 
					/// Create a copy of PostComposeInitialState
 | 
				
			||||||
/// with the given fields replaced by the non-null parameter values.
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
@@ -112,16 +140,16 @@ Map<String, dynamic> toJson() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@override
 | 
					@override
 | 
				
			||||||
bool operator ==(Object other) {
 | 
					bool operator ==(Object other) {
 | 
				
			||||||
  return identical(this, other) || (other.runtimeType == runtimeType&&other is _PostComposeInitialState&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.content, content) || other.content == content)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&(identical(other.visibility, visibility) || other.visibility == visibility));
 | 
					  return identical(this, other) || (other.runtimeType == runtimeType&&other is _PostComposeInitialState&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.content, content) || other.content == content)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.replyingTo, replyingTo) || other.replyingTo == replyingTo)&&(identical(other.forwardingTo, forwardingTo) || other.forwardingTo == forwardingTo));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@JsonKey(includeFromJson: false, includeToJson: false)
 | 
					@JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
@override
 | 
					@override
 | 
				
			||||||
int get hashCode => Object.hash(runtimeType,title,description,content,const DeepCollectionEquality().hash(_attachments),visibility);
 | 
					int get hashCode => Object.hash(runtimeType,title,description,content,const DeepCollectionEquality().hash(_attachments),visibility,replyingTo,forwardingTo);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@override
 | 
					@override
 | 
				
			||||||
String toString() {
 | 
					String toString() {
 | 
				
			||||||
  return 'PostComposeInitialState(title: $title, description: $description, content: $content, attachments: $attachments, visibility: $visibility)';
 | 
					  return 'PostComposeInitialState(title: $title, description: $description, content: $content, attachments: $attachments, visibility: $visibility, replyingTo: $replyingTo, forwardingTo: $forwardingTo)';
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -132,11 +160,11 @@ abstract mixin class _$PostComposeInitialStateCopyWith<$Res> implements $PostCom
 | 
				
			|||||||
  factory _$PostComposeInitialStateCopyWith(_PostComposeInitialState value, $Res Function(_PostComposeInitialState) _then) = __$PostComposeInitialStateCopyWithImpl;
 | 
					  factory _$PostComposeInitialStateCopyWith(_PostComposeInitialState value, $Res Function(_PostComposeInitialState) _then) = __$PostComposeInitialStateCopyWithImpl;
 | 
				
			||||||
@override @useResult
 | 
					@override @useResult
 | 
				
			||||||
$Res call({
 | 
					$Res call({
 | 
				
			||||||
 String? title, String? description, String? content, List<UniversalFile> attachments, int? visibility
 | 
					 String? title, String? description, String? content, List<UniversalFile> attachments, int? visibility, SnPost? replyingTo, SnPost? forwardingTo
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override $SnPostCopyWith<$Res>? get replyingTo;@override $SnPostCopyWith<$Res>? get forwardingTo;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
/// @nodoc
 | 
					/// @nodoc
 | 
				
			||||||
@@ -149,18 +177,44 @@ class __$PostComposeInitialStateCopyWithImpl<$Res>
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
/// Create a copy of PostComposeInitialState
 | 
					/// Create a copy of PostComposeInitialState
 | 
				
			||||||
/// with the given fields replaced by the non-null parameter values.
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
@override @pragma('vm:prefer-inline') $Res call({Object? title = freezed,Object? description = freezed,Object? content = freezed,Object? attachments = null,Object? visibility = freezed,}) {
 | 
					@override @pragma('vm:prefer-inline') $Res call({Object? title = freezed,Object? description = freezed,Object? content = freezed,Object? attachments = null,Object? visibility = freezed,Object? replyingTo = freezed,Object? forwardingTo = freezed,}) {
 | 
				
			||||||
  return _then(_PostComposeInitialState(
 | 
					  return _then(_PostComposeInitialState(
 | 
				
			||||||
title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
 | 
					title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
 | 
					as String?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String?,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
 | 
					as String?,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String?,attachments: null == attachments ? _self._attachments : attachments // ignore: cast_nullable_to_non_nullable
 | 
					as String?,attachments: null == attachments ? _self._attachments : attachments // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as List<UniversalFile>,visibility: freezed == visibility ? _self.visibility : visibility // ignore: cast_nullable_to_non_nullable
 | 
					as List<UniversalFile>,visibility: freezed == visibility ? _self.visibility : visibility // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as int?,
 | 
					as int?,replyingTo: freezed == replyingTo ? _self.replyingTo : replyingTo // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as SnPost?,forwardingTo: freezed == forwardingTo ? _self.forwardingTo : forwardingTo // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as SnPost?,
 | 
				
			||||||
  ));
 | 
					  ));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Create a copy of PostComposeInitialState
 | 
				
			||||||
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					@pragma('vm:prefer-inline')
 | 
				
			||||||
 | 
					$SnPostCopyWith<$Res>? get replyingTo {
 | 
				
			||||||
 | 
					    if (_self.replyingTo == null) {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return $SnPostCopyWith<$Res>(_self.replyingTo!, (value) {
 | 
				
			||||||
 | 
					    return _then(_self.copyWith(replyingTo: value));
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}/// Create a copy of PostComposeInitialState
 | 
				
			||||||
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					@pragma('vm:prefer-inline')
 | 
				
			||||||
 | 
					$SnPostCopyWith<$Res>? get forwardingTo {
 | 
				
			||||||
 | 
					    if (_self.forwardingTo == null) {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return $SnPostCopyWith<$Res>(_self.forwardingTo!, (value) {
 | 
				
			||||||
 | 
					    return _then(_self.copyWith(forwardingTo: value));
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// dart format on
 | 
					// dart format on
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,6 +18,14 @@ _PostComposeInitialState _$PostComposeInitialStateFromJson(
 | 
				
			|||||||
          .toList() ??
 | 
					          .toList() ??
 | 
				
			||||||
      const [],
 | 
					      const [],
 | 
				
			||||||
  visibility: (json['visibility'] as num?)?.toInt(),
 | 
					  visibility: (json['visibility'] as num?)?.toInt(),
 | 
				
			||||||
 | 
					  replyingTo:
 | 
				
			||||||
 | 
					      json['replying_to'] == null
 | 
				
			||||||
 | 
					          ? null
 | 
				
			||||||
 | 
					          : SnPost.fromJson(json['replying_to'] as Map<String, dynamic>),
 | 
				
			||||||
 | 
					  forwardingTo:
 | 
				
			||||||
 | 
					      json['forwarding_to'] == null
 | 
				
			||||||
 | 
					          ? null
 | 
				
			||||||
 | 
					          : SnPost.fromJson(json['forwarding_to'] as Map<String, dynamic>),
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Map<String, dynamic> _$PostComposeInitialStateToJson(
 | 
					Map<String, dynamic> _$PostComposeInitialStateToJson(
 | 
				
			||||||
@@ -28,4 +36,6 @@ Map<String, dynamic> _$PostComposeInitialStateToJson(
 | 
				
			|||||||
  'content': instance.content,
 | 
					  'content': instance.content,
 | 
				
			||||||
  'attachments': instance.attachments.map((e) => e.toJson()).toList(),
 | 
					  'attachments': instance.attachments.map((e) => e.toJson()).toList(),
 | 
				
			||||||
  'visibility': instance.visibility,
 | 
					  'visibility': instance.visibility,
 | 
				
			||||||
 | 
					  'replying_to': instance.replyingTo?.toJson(),
 | 
				
			||||||
 | 
					  'forwarding_to': instance.forwardingTo?.toJson(),
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -238,7 +238,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
 | 
				
			|||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          // Publisher row
 | 
					          // Publisher row
 | 
				
			||||||
          Card(
 | 
					          Card(
 | 
				
			||||||
            margin: EdgeInsets.only(bottom: 8),
 | 
					            margin: EdgeInsets.only(top: 8),
 | 
				
			||||||
            elevation: 1,
 | 
					            elevation: 1,
 | 
				
			||||||
            child: Padding(
 | 
					            child: Padding(
 | 
				
			||||||
              padding: const EdgeInsets.all(12),
 | 
					              padding: const EdgeInsets.all(12),
 | 
				
			||||||
@@ -265,12 +265,22 @@ class ArticleComposeScreen extends HookConsumerWidget {
 | 
				
			|||||||
                      });
 | 
					                      });
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  const Gap(12),
 | 
					                  const Gap(16),
 | 
				
			||||||
                  Text(
 | 
					                  if (state.currentPublisher.value == null)
 | 
				
			||||||
                    state.currentPublisher.value?.name ??
 | 
					                    Text(
 | 
				
			||||||
                        'postPublisherUnselected'.tr(),
 | 
					                      'postPublisherUnselected'.tr(),
 | 
				
			||||||
                    style: theme.textTheme.bodyMedium,
 | 
					                      style: theme.textTheme.bodyMedium,
 | 
				
			||||||
                  ),
 | 
					                    )
 | 
				
			||||||
 | 
					                  else
 | 
				
			||||||
 | 
					                    Column(
 | 
				
			||||||
 | 
					                      crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					                      children: [
 | 
				
			||||||
 | 
					                        Text(state.currentPublisher.value!.nick).bold(),
 | 
				
			||||||
 | 
					                        Text(
 | 
				
			||||||
 | 
					                          '@${state.currentPublisher.value!.name}',
 | 
				
			||||||
 | 
					                        ).fontSize(12),
 | 
				
			||||||
 | 
					                      ],
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
                ],
 | 
					                ],
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
@@ -311,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, _) {
 | 
				
			||||||
@@ -322,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],
 | 
				
			||||||
@@ -348,6 +365,12 @@ class ArticleComposeScreen extends HookConsumerWidget {
 | 
				
			|||||||
                                    delta,
 | 
					                                    delta,
 | 
				
			||||||
                                  );
 | 
					                                  );
 | 
				
			||||||
                                },
 | 
					                                },
 | 
				
			||||||
 | 
					                                onInsert:
 | 
				
			||||||
 | 
					                                    () => ComposeLogic.insertAttachment(
 | 
				
			||||||
 | 
					                                      ref,
 | 
				
			||||||
 | 
					                                      state,
 | 
				
			||||||
 | 
					                                      idx,
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
                              ),
 | 
					                              ),
 | 
				
			||||||
                            ),
 | 
					                            ),
 | 
				
			||||||
                        ],
 | 
					                        ],
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,6 @@ import 'package:island/widgets/app_scaffold.dart';
 | 
				
			|||||||
import 'package:island/widgets/post/post_item.dart';
 | 
					import 'package:island/widgets/post/post_item.dart';
 | 
				
			||||||
import 'package:island/widgets/post/post_quick_reply.dart';
 | 
					import 'package:island/widgets/post/post_quick_reply.dart';
 | 
				
			||||||
import 'package:island/widgets/post/post_replies.dart';
 | 
					import 'package:island/widgets/post/post_replies.dart';
 | 
				
			||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
 | 
					 | 
				
			||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
					import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -22,9 +21,10 @@ Future<SnPost?> post(Ref ref, String id) async {
 | 
				
			|||||||
  return SnPost.fromJson(resp.data);
 | 
					  return SnPost.fromJson(resp.data);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
final postStateProvider = StateNotifierProvider.family<PostState, AsyncValue<SnPost?>, String>(
 | 
					final postStateProvider =
 | 
				
			||||||
  (ref, id) => PostState(ref, id),
 | 
					    StateNotifierProvider.family<PostState, AsyncValue<SnPost?>, String>(
 | 
				
			||||||
);
 | 
					      (ref, id) => PostState(ref, id),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PostState extends StateNotifier<AsyncValue<SnPost?>> {
 | 
					class PostState extends StateNotifier<AsyncValue<SnPost?>> {
 | 
				
			||||||
  final Ref _ref;
 | 
					  final Ref _ref;
 | 
				
			||||||
@@ -75,7 +75,9 @@ class PostDetailScreen extends HookConsumerWidget {
 | 
				
			|||||||
                          backgroundColor: isWide ? Colors.transparent : null,
 | 
					                          backgroundColor: isWide ? Colors.transparent : null,
 | 
				
			||||||
                          onUpdate: (newItem) {
 | 
					                          onUpdate: (newItem) {
 | 
				
			||||||
                            // Update the local state with the new post data
 | 
					                            // Update the local state with the new post data
 | 
				
			||||||
                            ref.read(postStateProvider(id).notifier).updatePost(newItem);
 | 
					                            ref
 | 
				
			||||||
 | 
					                                .read(postStateProvider(id).notifier)
 | 
				
			||||||
 | 
					                                .updatePost(newItem);
 | 
				
			||||||
                          },
 | 
					                          },
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                        const Divider(height: 1),
 | 
					                        const Divider(height: 1),
 | 
				
			||||||
@@ -93,20 +95,25 @@ class PostDetailScreen extends HookConsumerWidget {
 | 
				
			|||||||
                  right: 0,
 | 
					                  right: 0,
 | 
				
			||||||
                  child: Material(
 | 
					                  child: Material(
 | 
				
			||||||
                    elevation: 2,
 | 
					                    elevation: 2,
 | 
				
			||||||
                    child: postState.when(
 | 
					                    child: postState
 | 
				
			||||||
                      data: (post) => PostQuickReply(
 | 
					                        .when(
 | 
				
			||||||
                        parent: post!,
 | 
					                          data:
 | 
				
			||||||
                        onPosted: () {
 | 
					                              (post) => PostQuickReply(
 | 
				
			||||||
                          ref.invalidate(postRepliesNotifierProvider(id));
 | 
					                                parent: post!,
 | 
				
			||||||
                        },
 | 
					                                onPosted: () {
 | 
				
			||||||
                      ),
 | 
					                                  ref.invalidate(
 | 
				
			||||||
                      loading: () => const SizedBox.shrink(),
 | 
					                                    postRepliesNotifierProvider(id),
 | 
				
			||||||
                      error: (_, __) => const SizedBox.shrink(),
 | 
					                                  );
 | 
				
			||||||
                    ).padding(
 | 
					                                },
 | 
				
			||||||
                      bottom: MediaQuery.of(context).padding.bottom + 16,
 | 
					                              ),
 | 
				
			||||||
                      top: 16,
 | 
					                          loading: () => const SizedBox.shrink(),
 | 
				
			||||||
                      horizontal: 16,
 | 
					                          error: (_, _) => const SizedBox.shrink(),
 | 
				
			||||||
                    ),
 | 
					                        )
 | 
				
			||||||
 | 
					                        .padding(
 | 
				
			||||||
 | 
					                          bottom: MediaQuery.of(context).padding.bottom + 16,
 | 
				
			||||||
 | 
					                          top: 16,
 | 
				
			||||||
 | 
					                          horizontal: 16,
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -341,26 +341,6 @@ class SettingsScreen extends HookConsumerWidget {
 | 
				
			|||||||
    ];
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final behaviorSettings = [
 | 
					    final behaviorSettings = [
 | 
				
			||||||
      ListTile(
 | 
					 | 
				
			||||||
        minLeadingWidth: 48,
 | 
					 | 
				
			||||||
        title: Text('creatorHub').tr(),
 | 
					 | 
				
			||||||
        contentPadding: const EdgeInsets.only(left: 24, right: 17),
 | 
					 | 
				
			||||||
        leading: const Icon(Symbols.rocket_launch),
 | 
					 | 
				
			||||||
        trailing: const Icon(Symbols.chevron_right),
 | 
					 | 
				
			||||||
        onTap: () => context.push('/creators'),
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // Developer Hub
 | 
					 | 
				
			||||||
      ListTile(
 | 
					 | 
				
			||||||
        minLeadingWidth: 48,
 | 
					 | 
				
			||||||
        title: Text('developerHub').tr(),
 | 
					 | 
				
			||||||
        contentPadding: const EdgeInsets.only(left: 24, right: 17),
 | 
					 | 
				
			||||||
        leading: const Icon(Symbols.hub),
 | 
					 | 
				
			||||||
        trailing: const Icon(Symbols.chevron_right),
 | 
					 | 
				
			||||||
        onTap: () => context.push('/developers'),
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // Auto translate settings
 | 
					 | 
				
			||||||
      ListTile(
 | 
					      ListTile(
 | 
				
			||||||
        minLeadingWidth: 48,
 | 
					        minLeadingWidth: 48,
 | 
				
			||||||
        title: Text('settingsAutoTranslate').tr(),
 | 
					        title: Text('settingsAutoTranslate').tr(),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -63,7 +63,10 @@ StreamSubscription<WebSocketPacket> setupNotificationListener(
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Future<void> subscribePushNotification(Dio apiClient) async {
 | 
					Future<void> subscribePushNotification(
 | 
				
			||||||
 | 
					  Dio apiClient, {
 | 
				
			||||||
 | 
					  bool detailedErrors = false,
 | 
				
			||||||
 | 
					}) async {
 | 
				
			||||||
  await FirebaseMessaging.instance.requestPermission(
 | 
					  await FirebaseMessaging.instance.requestPermission(
 | 
				
			||||||
    provisional: true,
 | 
					    provisional: true,
 | 
				
			||||||
    alert: true,
 | 
					    alert: true,
 | 
				
			||||||
@@ -97,6 +100,8 @@ Future<void> subscribePushNotification(Dio apiClient) async {
 | 
				
			|||||||
      deviceToken,
 | 
					      deviceToken,
 | 
				
			||||||
      !kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1,
 | 
					      !kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					  } else if (detailedErrors) {
 | 
				
			||||||
 | 
					    throw Exception("Failed to get device token for push notifications.");
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,11 +21,23 @@ class AccountName extends StatelessWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    var nameStyle = (style ?? TextStyle());
 | 
				
			||||||
 | 
					    if (account.profile.stellarMembership != null) {
 | 
				
			||||||
 | 
					      nameStyle = nameStyle.copyWith(
 | 
				
			||||||
 | 
					        color: (switch (account.profile.stellarMembership!.identifier) {
 | 
				
			||||||
 | 
					          'solian.stellar.primary' => Colors.blueAccent,
 | 
				
			||||||
 | 
					          'solian.stellar.nova' => Colors.indigoAccent,
 | 
				
			||||||
 | 
					          'solian.stellar.supernova' => Colors.amberAccent,
 | 
				
			||||||
 | 
					          _ => null,
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Row(
 | 
					    return Row(
 | 
				
			||||||
      mainAxisSize: MainAxisSize.min,
 | 
					      mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
      spacing: 4,
 | 
					      spacing: 4,
 | 
				
			||||||
      children: [
 | 
					      children: [
 | 
				
			||||||
        Flexible(child: Text(account.nick, style: style)),
 | 
					        Flexible(child: Text(account.nick, style: nameStyle)),
 | 
				
			||||||
        if (account.profile.stellarMembership != null)
 | 
					        if (account.profile.stellarMembership != null)
 | 
				
			||||||
          StellarMembershipMark(membership: account.profile.stellarMembership!),
 | 
					          StellarMembershipMark(membership: account.profile.stellarMembership!),
 | 
				
			||||||
        if (account.profile.verification != null)
 | 
					        if (account.profile.verification != null)
 | 
				
			||||||
@@ -87,36 +99,23 @@ class StellarMembershipMark extends StatelessWidget {
 | 
				
			|||||||
  Color _getMembershipTierColor(String identifier) {
 | 
					  Color _getMembershipTierColor(String identifier) {
 | 
				
			||||||
    switch (identifier) {
 | 
					    switch (identifier) {
 | 
				
			||||||
      case 'solian.stellar.primary':
 | 
					      case 'solian.stellar.primary':
 | 
				
			||||||
        return Colors.amber;
 | 
					 | 
				
			||||||
      case 'solian.stellar.nova':
 | 
					 | 
				
			||||||
        return Colors.blue;
 | 
					        return Colors.blue;
 | 
				
			||||||
 | 
					      case 'solian.stellar.nova':
 | 
				
			||||||
 | 
					        return Colors.indigo;
 | 
				
			||||||
      case 'solian.stellar.supernova':
 | 
					      case 'solian.stellar.supernova':
 | 
				
			||||||
        return Colors.purple;
 | 
					        return Colors.amber;
 | 
				
			||||||
      default:
 | 
					      default:
 | 
				
			||||||
        return Colors.grey;
 | 
					        return Colors.grey;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  IconData _getMembershipTierIcon(String identifier) {
 | 
					 | 
				
			||||||
    switch (identifier) {
 | 
					 | 
				
			||||||
      case 'solian.stellar.primary':
 | 
					 | 
				
			||||||
        return Symbols.star;
 | 
					 | 
				
			||||||
      case 'solian.stellar.nova':
 | 
					 | 
				
			||||||
        return Symbols.auto_awesome;
 | 
					 | 
				
			||||||
      case 'solian.stellar.supernova':
 | 
					 | 
				
			||||||
        return Symbols.diamond;
 | 
					 | 
				
			||||||
      default:
 | 
					 | 
				
			||||||
        return Symbols.workspace_premium;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    if (!membership.isActive) return const SizedBox.shrink();
 | 
					    if (!membership.isActive) return const SizedBox.shrink();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final tierName = _getMembershipTierName(membership.identifier);
 | 
					    final tierName = _getMembershipTierName(membership.identifier);
 | 
				
			||||||
    final tierColor = _getMembershipTierColor(membership.identifier);
 | 
					    final tierColor = _getMembershipTierColor(membership.identifier);
 | 
				
			||||||
    final tierIcon = _getMembershipTierIcon(membership.identifier);
 | 
					    final tierIcon = Symbols.award_star;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Tooltip(
 | 
					    return Tooltip(
 | 
				
			||||||
      richMessage: TextSpan(
 | 
					      richMessage: TextSpan(
 | 
				
			||||||
@@ -124,7 +123,7 @@ class StellarMembershipMark extends StatelessWidget {
 | 
				
			|||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          TextSpan(text: '\n'),
 | 
					          TextSpan(text: '\n'),
 | 
				
			||||||
          TextSpan(
 | 
					          TextSpan(
 | 
				
			||||||
            text: 'currentMembership'.tr(args: [tierName]),
 | 
					            text: 'currentMembershipMember'.tr(args: [tierName]),
 | 
				
			||||||
            style: TextStyle(fontWeight: FontWeight.normal),
 | 
					            style: TextStyle(fontWeight: FontWeight.normal),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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();
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
                      ],
 | 
					                      ],
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -244,7 +244,7 @@ class CloudFileZoomIn extends HookConsumerWidget {
 | 
				
			|||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    String _formatFileSize(int bytes) {
 | 
					    String formatFileSize(int bytes) {
 | 
				
			||||||
      if (bytes <= 0) return '0 B';
 | 
					      if (bytes <= 0) return '0 B';
 | 
				
			||||||
      if (bytes < 1024) return '$bytes B';
 | 
					      if (bytes < 1024) return '$bytes B';
 | 
				
			||||||
      if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(2)} KB';
 | 
					      if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(2)} KB';
 | 
				
			||||||
@@ -274,7 +274,7 @@ class CloudFileZoomIn extends HookConsumerWidget {
 | 
				
			|||||||
                    buildInfoRow(
 | 
					                    buildInfoRow(
 | 
				
			||||||
                      Icons.storage,
 | 
					                      Icons.storage,
 | 
				
			||||||
                      'Size',
 | 
					                      'Size',
 | 
				
			||||||
                      _formatFileSize(item.size),
 | 
					                      formatFileSize(item.size),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                    const Divider(height: 1),
 | 
					                    const Divider(height: 1),
 | 
				
			||||||
                    buildInfoRow(
 | 
					                    buildInfoRow(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -286,7 +286,7 @@ class _PaymentContentState extends ConsumerState<_PaymentContent> {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String _formatCurrency(int amount, String currency) {
 | 
					  String _formatCurrency(int amount, String currency) {
 | 
				
			||||||
    final value = amount / 100.0;
 | 
					    final value = amount;
 | 
				
			||||||
    return '${value.toStringAsFixed(2)} $currency';
 | 
					    return '${value.toStringAsFixed(2)} $currency';
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -98,19 +98,11 @@ class ComposeLogic {
 | 
				
			|||||||
      descriptionController: TextEditingController(
 | 
					      descriptionController: TextEditingController(
 | 
				
			||||||
        text: originalPost?.description,
 | 
					        text: originalPost?.description,
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      contentController: TextEditingController(
 | 
					      contentController: TextEditingController(text: originalPost?.content),
 | 
				
			||||||
        text:
 | 
					 | 
				
			||||||
            originalPost?.content ??
 | 
					 | 
				
			||||||
            (forwardedPost != null
 | 
					 | 
				
			||||||
                ? '''> ${forwardedPost.content}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
'''
 | 
					 | 
				
			||||||
                : null),
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      visibility: ValueNotifier<int>(originalPost?.visibility ?? 0),
 | 
					      visibility: ValueNotifier<int>(originalPost?.visibility ?? 0),
 | 
				
			||||||
      submitting: ValueNotifier<bool>(false),
 | 
					      submitting: ValueNotifier<bool>(false),
 | 
				
			||||||
      attachmentProgress: ValueNotifier<Map<int, double>>({}),
 | 
					      attachmentProgress: ValueNotifier<Map<int, double>>({}),
 | 
				
			||||||
      currentPublisher: ValueNotifier<SnPublisher?>(null),
 | 
					      currentPublisher: ValueNotifier<SnPublisher?>(originalPost?.publisher),
 | 
				
			||||||
      tagsController: tagsController,
 | 
					      tagsController: tagsController,
 | 
				
			||||||
      categoriesController: categoriesController,
 | 
					      categoriesController: categoriesController,
 | 
				
			||||||
      draftId: id,
 | 
					      draftId: id,
 | 
				
			||||||
@@ -482,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,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,6 +11,7 @@ import 'package:island/models/post.dart';
 | 
				
			|||||||
import 'package:island/pods/config.dart';
 | 
					import 'package:island/pods/config.dart';
 | 
				
			||||||
import 'package:island/pods/network.dart';
 | 
					import 'package:island/pods/network.dart';
 | 
				
			||||||
import 'package:island/pods/userinfo.dart';
 | 
					import 'package:island/pods/userinfo.dart';
 | 
				
			||||||
 | 
					import 'package:island/screens/posts/compose.dart';
 | 
				
			||||||
import 'package:island/services/responsive.dart';
 | 
					import 'package:island/services/responsive.dart';
 | 
				
			||||||
import 'package:island/services/time.dart';
 | 
					import 'package:island/services/time.dart';
 | 
				
			||||||
import 'package:island/widgets/account/account_name.dart';
 | 
					import 'package:island/widgets/account/account_name.dart';
 | 
				
			||||||
@@ -55,13 +56,432 @@ 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],
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final hasBackground =
 | 
					    final hasBackground =
 | 
				
			||||||
        ref.watch(backgroundImageFileProvider).valueOrNull != null;
 | 
					        ref.watch(backgroundImageFileProvider).valueOrNull != null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Widget child;
 | 
				
			||||||
 | 
					    if (item.type == 1 && isFullPost) {
 | 
				
			||||||
 | 
					      child = Padding(
 | 
				
			||||||
 | 
					        padding: renderingPadding,
 | 
				
			||||||
 | 
					        child: Column(
 | 
				
			||||||
 | 
					          crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					          children: [
 | 
				
			||||||
 | 
					            GestureDetector(
 | 
				
			||||||
 | 
					              onTap: () {
 | 
				
			||||||
 | 
					                context.push('/publishers/${item.publisher.name}');
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              child: Row(
 | 
				
			||||||
 | 
					                crossAxisAlignment: CrossAxisAlignment.center,
 | 
				
			||||||
 | 
					                children: [
 | 
				
			||||||
 | 
					                  ProfilePictureWidget(file: item.publisher.picture),
 | 
				
			||||||
 | 
					                  const Gap(12),
 | 
				
			||||||
 | 
					                  Expanded(
 | 
				
			||||||
 | 
					                    child: Column(
 | 
				
			||||||
 | 
					                      crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					                      children: [
 | 
				
			||||||
 | 
					                        Text(item.publisher.nick).bold(),
 | 
				
			||||||
 | 
					                        if (item.publisher.verification != null)
 | 
				
			||||||
 | 
					                          VerificationMark(
 | 
				
			||||||
 | 
					                            mark: item.publisher.verification!,
 | 
				
			||||||
 | 
					                          ).padding(left: 4),
 | 
				
			||||||
 | 
					                      ],
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  Text(
 | 
				
			||||||
 | 
					                    isFullPost
 | 
				
			||||||
 | 
					                        ? item.publishedAt?.formatSystem() ?? ''
 | 
				
			||||||
 | 
					                        : item.publishedAt?.formatRelative(context) ?? '',
 | 
				
			||||||
 | 
					                  ).fontSize(11),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            if (item.visibility != 0)
 | 
				
			||||||
 | 
					              Row(
 | 
				
			||||||
 | 
					                mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
 | 
					                children: [
 | 
				
			||||||
 | 
					                  Icon(
 | 
				
			||||||
 | 
					                    _getVisibilityIcon(item.visibility),
 | 
				
			||||||
 | 
					                    size: 14,
 | 
				
			||||||
 | 
					                    color: Theme.of(context).colorScheme.secondary,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  const SizedBox(width: 4),
 | 
				
			||||||
 | 
					                  Text(
 | 
				
			||||||
 | 
					                    _getVisibilityText(item.visibility).tr(),
 | 
				
			||||||
 | 
					                    style: TextStyle(
 | 
				
			||||||
 | 
					                      fontSize: 12,
 | 
				
			||||||
 | 
					                      color: Theme.of(context).colorScheme.secondary,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					              ).padding(top: 10, bottom: 2),
 | 
				
			||||||
 | 
					            const Gap(16),
 | 
				
			||||||
 | 
					            _ArticlePostDisplay(item: item, isFullPost: isFullPost),
 | 
				
			||||||
 | 
					            if (item.tags.isNotEmpty || item.categories.isNotEmpty)
 | 
				
			||||||
 | 
					              Column(
 | 
				
			||||||
 | 
					                crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					                children: [
 | 
				
			||||||
 | 
					                  if (item.tags.isNotEmpty)
 | 
				
			||||||
 | 
					                    Wrap(
 | 
				
			||||||
 | 
					                      children: [
 | 
				
			||||||
 | 
					                        for (final tag in item.tags)
 | 
				
			||||||
 | 
					                          InkWell(
 | 
				
			||||||
 | 
					                            child: Row(
 | 
				
			||||||
 | 
					                              spacing: 4,
 | 
				
			||||||
 | 
					                              children: [
 | 
				
			||||||
 | 
					                                const Icon(Symbols.label, size: 13),
 | 
				
			||||||
 | 
					                                Text(tag.name ?? '#${tag.slug}').fontSize(13),
 | 
				
			||||||
 | 
					                              ],
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            onTap: () {},
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                      ],
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  if (item.categories.isNotEmpty)
 | 
				
			||||||
 | 
					                    Wrap(
 | 
				
			||||||
 | 
					                      children: [
 | 
				
			||||||
 | 
					                        for (final category in item.categories)
 | 
				
			||||||
 | 
					                          InkWell(
 | 
				
			||||||
 | 
					                            child: Row(
 | 
				
			||||||
 | 
					                              spacing: 4,
 | 
				
			||||||
 | 
					                              children: [
 | 
				
			||||||
 | 
					                                const Icon(Symbols.category, size: 13),
 | 
				
			||||||
 | 
					                                Text(
 | 
				
			||||||
 | 
					                                  category.name ?? '#${category.slug}',
 | 
				
			||||||
 | 
					                                ).fontSize(13),
 | 
				
			||||||
 | 
					                              ],
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            onTap: () {},
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                      ],
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            if ((item.repliedPost != null || item.forwardedPost != null) &&
 | 
				
			||||||
 | 
					                showReferencePost)
 | 
				
			||||||
 | 
					              _buildReferencePost(context, item),
 | 
				
			||||||
 | 
					            if (item.attachments.isNotEmpty && item.type != 1)
 | 
				
			||||||
 | 
					              CloudFileList(
 | 
				
			||||||
 | 
					                files: item.attachments,
 | 
				
			||||||
 | 
					                maxWidth: math.min(
 | 
				
			||||||
 | 
					                  MediaQuery.of(context).size.width,
 | 
				
			||||||
 | 
					                  kWideScreenWidth,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                minWidth: math.min(
 | 
				
			||||||
 | 
					                  MediaQuery.of(context).size.width,
 | 
				
			||||||
 | 
					                  kWideScreenWidth,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            if (item.meta?['embeds'] != null)
 | 
				
			||||||
 | 
					              ...((item.meta!['embeds'] as List<dynamic>)
 | 
				
			||||||
 | 
					                  .where((embed) => embed['Type'] == 'link')
 | 
				
			||||||
 | 
					                  .map(
 | 
				
			||||||
 | 
					                    (embedData) => EmbedLinkWidget(
 | 
				
			||||||
 | 
					                      link: SnEmbedLink.fromJson(
 | 
				
			||||||
 | 
					                        embedData as Map<String, dynamic>,
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                      maxWidth: math.min(
 | 
				
			||||||
 | 
					                        MediaQuery.of(context).size.width,
 | 
				
			||||||
 | 
					                        kWideScreenWidth,
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                      margin: EdgeInsets.only(top: 8),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  )),
 | 
				
			||||||
 | 
					            const Gap(8),
 | 
				
			||||||
 | 
					            Row(
 | 
				
			||||||
 | 
					              children: [
 | 
				
			||||||
 | 
					                Padding(
 | 
				
			||||||
 | 
					                  padding: const EdgeInsets.only(right: 12),
 | 
				
			||||||
 | 
					                  child: ActionChip(
 | 
				
			||||||
 | 
					                    avatar: Icon(Symbols.reply, size: 16),
 | 
				
			||||||
 | 
					                    label: Text(
 | 
				
			||||||
 | 
					                      (item.repliesCount > 0)
 | 
				
			||||||
 | 
					                          ? 'repliesCount'.plural(item.repliesCount)
 | 
				
			||||||
 | 
					                          : 'reply'.tr(),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    visualDensity: const VisualDensity(
 | 
				
			||||||
 | 
					                      horizontal: VisualDensity.minimumDensity,
 | 
				
			||||||
 | 
					                      vertical: VisualDensity.minimumDensity,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    onPressed: () {
 | 
				
			||||||
 | 
					                      if (isOpenable) {
 | 
				
			||||||
 | 
					                        showModalBottomSheet(
 | 
				
			||||||
 | 
					                          context: context,
 | 
				
			||||||
 | 
					                          isScrollControlled: true,
 | 
				
			||||||
 | 
					                          useRootNavigator: true,
 | 
				
			||||||
 | 
					                          builder: (context) => PostRepliesSheet(post: item),
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                      }
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                Expanded(
 | 
				
			||||||
 | 
					                  child: PostReactionList(
 | 
				
			||||||
 | 
					                    parentId: item.id,
 | 
				
			||||||
 | 
					                    reactions: item.reactionsCount,
 | 
				
			||||||
 | 
					                    padding: EdgeInsets.zero,
 | 
				
			||||||
 | 
					                    onReact: (symbol, attitude, delta) {
 | 
				
			||||||
 | 
					                      final reactionsCount = Map<String, int>.from(
 | 
				
			||||||
 | 
					                        item.reactionsCount,
 | 
				
			||||||
 | 
					                      );
 | 
				
			||||||
 | 
					                      reactionsCount[symbol] =
 | 
				
			||||||
 | 
					                          (reactionsCount[symbol] ?? 0) + delta;
 | 
				
			||||||
 | 
					                      onUpdate?.call(
 | 
				
			||||||
 | 
					                        item.copyWith(reactionsCount: reactionsCount),
 | 
				
			||||||
 | 
					                      );
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      child = Padding(
 | 
				
			||||||
 | 
					        padding: renderingPadding,
 | 
				
			||||||
 | 
					        child: Column(
 | 
				
			||||||
 | 
					          spacing: 8,
 | 
				
			||||||
 | 
					          children: [
 | 
				
			||||||
 | 
					            Row(
 | 
				
			||||||
 | 
					              mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
 | 
					              crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					              spacing: 12,
 | 
				
			||||||
 | 
					              children: [
 | 
				
			||||||
 | 
					                GestureDetector(
 | 
				
			||||||
 | 
					                  child: ProfilePictureWidget(file: item.publisher.picture),
 | 
				
			||||||
 | 
					                  onTap: () {
 | 
				
			||||||
 | 
					                    context.push('/publishers/${item.publisher.name}');
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                Expanded(
 | 
				
			||||||
 | 
					                  child: GestureDetector(
 | 
				
			||||||
 | 
					                    child: Column(
 | 
				
			||||||
 | 
					                      crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					                      children: [
 | 
				
			||||||
 | 
					                        Row(
 | 
				
			||||||
 | 
					                          children: [
 | 
				
			||||||
 | 
					                            Text(item.publisher.nick).bold(),
 | 
				
			||||||
 | 
					                            if (item.publisher.verification != null)
 | 
				
			||||||
 | 
					                              VerificationMark(
 | 
				
			||||||
 | 
					                                mark: item.publisher.verification!,
 | 
				
			||||||
 | 
					                              ).padding(left: 4),
 | 
				
			||||||
 | 
					                            Spacer(),
 | 
				
			||||||
 | 
					                            Text(
 | 
				
			||||||
 | 
					                              isFullPost
 | 
				
			||||||
 | 
					                                  ? item.publishedAt?.formatSystem() ?? ''
 | 
				
			||||||
 | 
					                                  : item.publishedAt?.formatRelative(context) ??
 | 
				
			||||||
 | 
					                                      '',
 | 
				
			||||||
 | 
					                            ).fontSize(11).alignment(Alignment.bottomRight),
 | 
				
			||||||
 | 
					                            const Gap(4),
 | 
				
			||||||
 | 
					                          ],
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                        // Add visibility indicator if not public (visibility != 0)
 | 
				
			||||||
 | 
					                        if (item.visibility != 0)
 | 
				
			||||||
 | 
					                          Row(
 | 
				
			||||||
 | 
					                            mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
 | 
					                            children: [
 | 
				
			||||||
 | 
					                              Icon(
 | 
				
			||||||
 | 
					                                _getVisibilityIcon(item.visibility),
 | 
				
			||||||
 | 
					                                size: 14,
 | 
				
			||||||
 | 
					                                color: Theme.of(context).colorScheme.secondary,
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                              const SizedBox(width: 4),
 | 
				
			||||||
 | 
					                              Text(
 | 
				
			||||||
 | 
					                                _getVisibilityText(item.visibility).tr(),
 | 
				
			||||||
 | 
					                                style: TextStyle(
 | 
				
			||||||
 | 
					                                  fontSize: 12,
 | 
				
			||||||
 | 
					                                  color:
 | 
				
			||||||
 | 
					                                      Theme.of(context).colorScheme.secondary,
 | 
				
			||||||
 | 
					                                ),
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                            ],
 | 
				
			||||||
 | 
					                          ).padding(top: 2, bottom: 2),
 | 
				
			||||||
 | 
					                        if (item.type == 1)
 | 
				
			||||||
 | 
					                          _ArticlePostDisplay(
 | 
				
			||||||
 | 
					                            item: item,
 | 
				
			||||||
 | 
					                            isFullPost: isFullPost,
 | 
				
			||||||
 | 
					                          )
 | 
				
			||||||
 | 
					                        else ...[
 | 
				
			||||||
 | 
					                          if (item.title?.isNotEmpty ?? false)
 | 
				
			||||||
 | 
					                            Text(
 | 
				
			||||||
 | 
					                              item.title!,
 | 
				
			||||||
 | 
					                              style: Theme.of(context).textTheme.titleMedium
 | 
				
			||||||
 | 
					                                  ?.copyWith(fontWeight: FontWeight.bold),
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                          if (item.description?.isNotEmpty ?? false)
 | 
				
			||||||
 | 
					                            Text(
 | 
				
			||||||
 | 
					                              item.description!,
 | 
				
			||||||
 | 
					                              style: Theme.of(
 | 
				
			||||||
 | 
					                                context,
 | 
				
			||||||
 | 
					                              ).textTheme.bodyMedium?.copyWith(
 | 
				
			||||||
 | 
					                                color:
 | 
				
			||||||
 | 
					                                    Theme.of(
 | 
				
			||||||
 | 
					                                      context,
 | 
				
			||||||
 | 
					                                    ).colorScheme.onSurfaceVariant,
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                            ).padding(bottom: 8),
 | 
				
			||||||
 | 
					                          if (item.content?.isNotEmpty ?? false)
 | 
				
			||||||
 | 
					                            MarkdownTextContent(
 | 
				
			||||||
 | 
					                              content: item.content!,
 | 
				
			||||||
 | 
					                              linesMargin:
 | 
				
			||||||
 | 
					                                  item.type == 0
 | 
				
			||||||
 | 
					                                      ? EdgeInsets.only(bottom: 8)
 | 
				
			||||||
 | 
					                                      : null,
 | 
				
			||||||
 | 
					                              attachments: item.attachments,
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                        // Render tags and categories if they exist
 | 
				
			||||||
 | 
					                        if (item.tags.isNotEmpty || item.categories.isNotEmpty)
 | 
				
			||||||
 | 
					                          Column(
 | 
				
			||||||
 | 
					                            crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					                            children: [
 | 
				
			||||||
 | 
					                              if (item.tags.isNotEmpty)
 | 
				
			||||||
 | 
					                                Wrap(
 | 
				
			||||||
 | 
					                                  children: [
 | 
				
			||||||
 | 
					                                    for (final tag in item.tags)
 | 
				
			||||||
 | 
					                                      InkWell(
 | 
				
			||||||
 | 
					                                        child: Row(
 | 
				
			||||||
 | 
					                                          spacing: 4,
 | 
				
			||||||
 | 
					                                          children: [
 | 
				
			||||||
 | 
					                                            const Icon(Symbols.label, size: 13),
 | 
				
			||||||
 | 
					                                            Text(
 | 
				
			||||||
 | 
					                                              tag.name ?? '#${tag.slug}',
 | 
				
			||||||
 | 
					                                            ).fontSize(13),
 | 
				
			||||||
 | 
					                                          ],
 | 
				
			||||||
 | 
					                                        ),
 | 
				
			||||||
 | 
					                                        onTap: () {},
 | 
				
			||||||
 | 
					                                      ),
 | 
				
			||||||
 | 
					                                  ],
 | 
				
			||||||
 | 
					                                ),
 | 
				
			||||||
 | 
					                              if (item.categories.isNotEmpty)
 | 
				
			||||||
 | 
					                                Wrap(
 | 
				
			||||||
 | 
					                                  children: [
 | 
				
			||||||
 | 
					                                    for (final category in item.categories)
 | 
				
			||||||
 | 
					                                      InkWell(
 | 
				
			||||||
 | 
					                                        child: Row(
 | 
				
			||||||
 | 
					                                          spacing: 4,
 | 
				
			||||||
 | 
					                                          children: [
 | 
				
			||||||
 | 
					                                            const Icon(
 | 
				
			||||||
 | 
					                                              Symbols.category,
 | 
				
			||||||
 | 
					                                              size: 13,
 | 
				
			||||||
 | 
					                                            ),
 | 
				
			||||||
 | 
					                                            Text(
 | 
				
			||||||
 | 
					                                              category.name ??
 | 
				
			||||||
 | 
					                                                  '#${category.slug}',
 | 
				
			||||||
 | 
					                                            ).fontSize(13),
 | 
				
			||||||
 | 
					                                          ],
 | 
				
			||||||
 | 
					                                        ),
 | 
				
			||||||
 | 
					                                        onTap: () {},
 | 
				
			||||||
 | 
					                                      ),
 | 
				
			||||||
 | 
					                                  ],
 | 
				
			||||||
 | 
					                                ),
 | 
				
			||||||
 | 
					                            ],
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                        // Show truncation hint if post is truncated
 | 
				
			||||||
 | 
					                        if (item.isTruncated && !isFullPost && item.type != 1)
 | 
				
			||||||
 | 
					                          _PostTruncateHint().padding(
 | 
				
			||||||
 | 
					                            bottom: item.attachments.isNotEmpty ? 8 : null,
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                        if ((item.repliedPost != null ||
 | 
				
			||||||
 | 
					                                item.forwardedPost != null) &&
 | 
				
			||||||
 | 
					                            showReferencePost)
 | 
				
			||||||
 | 
					                          _buildReferencePost(context, item),
 | 
				
			||||||
 | 
					                        if (item.attachments.isNotEmpty && item.type != 1)
 | 
				
			||||||
 | 
					                          CloudFileList(
 | 
				
			||||||
 | 
					                            files: item.attachments,
 | 
				
			||||||
 | 
					                            maxWidth: math.min(
 | 
				
			||||||
 | 
					                              MediaQuery.of(context).size.width * 0.85,
 | 
				
			||||||
 | 
					                              kWideScreenWidth - 160,
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            minWidth: math.min(
 | 
				
			||||||
 | 
					                              MediaQuery.of(context).size.width * 0.9,
 | 
				
			||||||
 | 
					                              kWideScreenWidth - 160,
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                        // Render embed links
 | 
				
			||||||
 | 
					                        if (item.meta?['embeds'] != null)
 | 
				
			||||||
 | 
					                          ...((item.meta!['embeds'] as List<dynamic>)
 | 
				
			||||||
 | 
					                              .where((embed) => embed['Type'] == 'link')
 | 
				
			||||||
 | 
					                              .map(
 | 
				
			||||||
 | 
					                                (embedData) => EmbedLinkWidget(
 | 
				
			||||||
 | 
					                                  link: SnEmbedLink.fromJson(
 | 
				
			||||||
 | 
					                                    embedData as Map<String, dynamic>,
 | 
				
			||||||
 | 
					                                  ),
 | 
				
			||||||
 | 
					                                  maxWidth: math.min(
 | 
				
			||||||
 | 
					                                    MediaQuery.of(context).size.width * 0.85,
 | 
				
			||||||
 | 
					                                    kWideScreenWidth - 160,
 | 
				
			||||||
 | 
					                                  ),
 | 
				
			||||||
 | 
					                                  margin: EdgeInsets.only(top: 8),
 | 
				
			||||||
 | 
					                                ),
 | 
				
			||||||
 | 
					                              )),
 | 
				
			||||||
 | 
					                      ],
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    onTap: () {
 | 
				
			||||||
 | 
					                      if (isOpenable) {
 | 
				
			||||||
 | 
					                        context.push('/posts/${item.id}');
 | 
				
			||||||
 | 
					                      }
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            Row(
 | 
				
			||||||
 | 
					              children: [
 | 
				
			||||||
 | 
					                // Replies count button
 | 
				
			||||||
 | 
					                Padding(
 | 
				
			||||||
 | 
					                  padding: const EdgeInsets.only(left: 52, right: 12),
 | 
				
			||||||
 | 
					                  child: ActionChip(
 | 
				
			||||||
 | 
					                    avatar: Icon(Symbols.reply, size: 16),
 | 
				
			||||||
 | 
					                    label: Text(
 | 
				
			||||||
 | 
					                      (item.repliesCount > 0)
 | 
				
			||||||
 | 
					                          ? 'repliesCount'.plural(item.repliesCount)
 | 
				
			||||||
 | 
					                          : 'reply'.tr(),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    visualDensity: const VisualDensity(
 | 
				
			||||||
 | 
					                      horizontal: VisualDensity.minimumDensity,
 | 
				
			||||||
 | 
					                      vertical: VisualDensity.minimumDensity,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    onPressed: () {
 | 
				
			||||||
 | 
					                      if (isOpenable) {
 | 
				
			||||||
 | 
					                        showModalBottomSheet(
 | 
				
			||||||
 | 
					                          context: context,
 | 
				
			||||||
 | 
					                          isScrollControlled: true,
 | 
				
			||||||
 | 
					                          useRootNavigator: true,
 | 
				
			||||||
 | 
					                          builder: (context) => PostRepliesSheet(post: item),
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                      }
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                // Reactions list
 | 
				
			||||||
 | 
					                Expanded(
 | 
				
			||||||
 | 
					                  child: PostReactionList(
 | 
				
			||||||
 | 
					                    parentId: item.id,
 | 
				
			||||||
 | 
					                    reactions: item.reactionsCount,
 | 
				
			||||||
 | 
					                    padding: EdgeInsets.zero,
 | 
				
			||||||
 | 
					                    onReact: (symbol, attitude, delta) {
 | 
				
			||||||
 | 
					                      final reactionsCount = Map<String, int>.from(
 | 
				
			||||||
 | 
					                        item.reactionsCount,
 | 
				
			||||||
 | 
					                      );
 | 
				
			||||||
 | 
					                      reactionsCount[symbol] =
 | 
				
			||||||
 | 
					                          (reactionsCount[symbol] ?? 0) + delta;
 | 
				
			||||||
 | 
					                      onUpdate?.call(
 | 
				
			||||||
 | 
					                        item.copyWith(reactionsCount: reactionsCount),
 | 
				
			||||||
 | 
					                      );
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return ContextMenuWidget(
 | 
					    return ContextMenuWidget(
 | 
				
			||||||
      menuProvider: (_) {
 | 
					      menuProvider: (_) {
 | 
				
			||||||
        return Menu(
 | 
					        return Menu(
 | 
				
			||||||
@@ -116,14 +536,20 @@ class PostItem extends HookConsumerWidget {
 | 
				
			|||||||
              title: 'reply'.tr(),
 | 
					              title: 'reply'.tr(),
 | 
				
			||||||
              image: MenuImage.icon(Symbols.reply),
 | 
					              image: MenuImage.icon(Symbols.reply),
 | 
				
			||||||
              callback: () {
 | 
					              callback: () {
 | 
				
			||||||
                context.push('/posts/compose', extra: {'repliedPost': item});
 | 
					                context.push(
 | 
				
			||||||
 | 
					                  '/posts/compose',
 | 
				
			||||||
 | 
					                  extra: PostComposeInitialState(replyingTo: item),
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            MenuAction(
 | 
					            MenuAction(
 | 
				
			||||||
              title: 'forward'.tr(),
 | 
					              title: 'forward'.tr(),
 | 
				
			||||||
              image: MenuImage.icon(Symbols.forward),
 | 
					              image: MenuImage.icon(Symbols.forward),
 | 
				
			||||||
              callback: () {
 | 
					              callback: () {
 | 
				
			||||||
                context.push('/posts/compose', extra: {'forwardedPost': item});
 | 
					                context.push(
 | 
				
			||||||
 | 
					                  '/posts/compose',
 | 
				
			||||||
 | 
					                  extra: PostComposeInitialState(forwardingTo: item),
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            MenuSeparator(),
 | 
					            MenuSeparator(),
 | 
				
			||||||
@@ -154,244 +580,7 @@ class PostItem extends HookConsumerWidget {
 | 
				
			|||||||
      },
 | 
					      },
 | 
				
			||||||
      child: Material(
 | 
					      child: Material(
 | 
				
			||||||
        color: hasBackground ? Colors.transparent : backgroundColor,
 | 
					        color: hasBackground ? Colors.transparent : backgroundColor,
 | 
				
			||||||
        child: Padding(
 | 
					        child: child,
 | 
				
			||||||
          padding: renderingPadding,
 | 
					 | 
				
			||||||
          child: Column(
 | 
					 | 
				
			||||||
            spacing: 8,
 | 
					 | 
				
			||||||
            children: [
 | 
					 | 
				
			||||||
              Row(
 | 
					 | 
				
			||||||
                mainAxisSize: MainAxisSize.min,
 | 
					 | 
				
			||||||
                crossAxisAlignment: CrossAxisAlignment.start,
 | 
					 | 
				
			||||||
                spacing: 12,
 | 
					 | 
				
			||||||
                children: [
 | 
					 | 
				
			||||||
                  GestureDetector(
 | 
					 | 
				
			||||||
                    child: ProfilePictureWidget(file: item.publisher.picture),
 | 
					 | 
				
			||||||
                    onTap: () {
 | 
					 | 
				
			||||||
                      context.push('/publishers/${item.publisher.name}');
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                  Expanded(
 | 
					 | 
				
			||||||
                    child: GestureDetector(
 | 
					 | 
				
			||||||
                      child: Column(
 | 
					 | 
				
			||||||
                        crossAxisAlignment: CrossAxisAlignment.start,
 | 
					 | 
				
			||||||
                        children: [
 | 
					 | 
				
			||||||
                          Row(
 | 
					 | 
				
			||||||
                            children: [
 | 
					 | 
				
			||||||
                              Text(item.publisher.nick).bold(),
 | 
					 | 
				
			||||||
                              if (item.publisher.verification != null)
 | 
					 | 
				
			||||||
                                VerificationMark(
 | 
					 | 
				
			||||||
                                  mark: item.publisher.verification!,
 | 
					 | 
				
			||||||
                                ).padding(left: 4),
 | 
					 | 
				
			||||||
                              Spacer(),
 | 
					 | 
				
			||||||
                              Text(
 | 
					 | 
				
			||||||
                                isFullPost
 | 
					 | 
				
			||||||
                                    ? item.publishedAt?.formatSystem() ?? ''
 | 
					 | 
				
			||||||
                                    : item.publishedAt?.formatRelative(
 | 
					 | 
				
			||||||
                                          context,
 | 
					 | 
				
			||||||
                                        ) ??
 | 
					 | 
				
			||||||
                                        '',
 | 
					 | 
				
			||||||
                              ).fontSize(11).alignment(Alignment.bottomRight),
 | 
					 | 
				
			||||||
                              const Gap(4),
 | 
					 | 
				
			||||||
                            ],
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                          // Add visibility indicator if not public (visibility != 0)
 | 
					 | 
				
			||||||
                          if (item.visibility != 0)
 | 
					 | 
				
			||||||
                            Row(
 | 
					 | 
				
			||||||
                              mainAxisSize: MainAxisSize.min,
 | 
					 | 
				
			||||||
                              children: [
 | 
					 | 
				
			||||||
                                Icon(
 | 
					 | 
				
			||||||
                                  _getVisibilityIcon(item.visibility),
 | 
					 | 
				
			||||||
                                  size: 14,
 | 
					 | 
				
			||||||
                                  color:
 | 
					 | 
				
			||||||
                                      Theme.of(context).colorScheme.secondary,
 | 
					 | 
				
			||||||
                                ),
 | 
					 | 
				
			||||||
                                const SizedBox(width: 4),
 | 
					 | 
				
			||||||
                                Text(
 | 
					 | 
				
			||||||
                                  _getVisibilityText(item.visibility).tr(),
 | 
					 | 
				
			||||||
                                  style: TextStyle(
 | 
					 | 
				
			||||||
                                    fontSize: 12,
 | 
					 | 
				
			||||||
                                    color:
 | 
					 | 
				
			||||||
                                        Theme.of(context).colorScheme.secondary,
 | 
					 | 
				
			||||||
                                  ),
 | 
					 | 
				
			||||||
                                ),
 | 
					 | 
				
			||||||
                              ],
 | 
					 | 
				
			||||||
                            ).padding(top: 2, bottom: 2),
 | 
					 | 
				
			||||||
                          if (item.title?.isNotEmpty ?? false)
 | 
					 | 
				
			||||||
                            Text(
 | 
					 | 
				
			||||||
                              item.title!,
 | 
					 | 
				
			||||||
                              style: Theme.of(context).textTheme.titleMedium
 | 
					 | 
				
			||||||
                                  ?.copyWith(fontWeight: FontWeight.bold),
 | 
					 | 
				
			||||||
                            ),
 | 
					 | 
				
			||||||
                          if (item.description?.isNotEmpty ?? false)
 | 
					 | 
				
			||||||
                            Text(
 | 
					 | 
				
			||||||
                              item.description!,
 | 
					 | 
				
			||||||
                              style: Theme.of(
 | 
					 | 
				
			||||||
                                context,
 | 
					 | 
				
			||||||
                              ).textTheme.bodyMedium?.copyWith(
 | 
					 | 
				
			||||||
                                color:
 | 
					 | 
				
			||||||
                                    Theme.of(
 | 
					 | 
				
			||||||
                                      context,
 | 
					 | 
				
			||||||
                                    ).colorScheme.onSurfaceVariant,
 | 
					 | 
				
			||||||
                              ),
 | 
					 | 
				
			||||||
                            ).padding(bottom: 8),
 | 
					 | 
				
			||||||
                          if (item.content?.isNotEmpty ?? false)
 | 
					 | 
				
			||||||
                            MarkdownTextContent(
 | 
					 | 
				
			||||||
                              content: item.content!,
 | 
					 | 
				
			||||||
                              linesMargin:
 | 
					 | 
				
			||||||
                                  item.type == 0
 | 
					 | 
				
			||||||
                                      ? EdgeInsets.only(bottom: 8)
 | 
					 | 
				
			||||||
                                      : null,
 | 
					 | 
				
			||||||
                            ),
 | 
					 | 
				
			||||||
                          // Render tags and categories if they exist
 | 
					 | 
				
			||||||
                          if (item.tags.isNotEmpty ||
 | 
					 | 
				
			||||||
                              item.categories.isNotEmpty)
 | 
					 | 
				
			||||||
                            Column(
 | 
					 | 
				
			||||||
                              crossAxisAlignment: CrossAxisAlignment.start,
 | 
					 | 
				
			||||||
                              children: [
 | 
					 | 
				
			||||||
                                if (item.tags.isNotEmpty)
 | 
					 | 
				
			||||||
                                  Wrap(
 | 
					 | 
				
			||||||
                                    children: [
 | 
					 | 
				
			||||||
                                      for (final tag in item.tags)
 | 
					 | 
				
			||||||
                                        InkWell(
 | 
					 | 
				
			||||||
                                          child: Row(
 | 
					 | 
				
			||||||
                                            spacing: 4,
 | 
					 | 
				
			||||||
                                            children: [
 | 
					 | 
				
			||||||
                                              const Icon(
 | 
					 | 
				
			||||||
                                                Symbols.label,
 | 
					 | 
				
			||||||
                                                size: 13,
 | 
					 | 
				
			||||||
                                              ),
 | 
					 | 
				
			||||||
                                              Text(
 | 
					 | 
				
			||||||
                                                tag.name ?? '#${tag.slug}',
 | 
					 | 
				
			||||||
                                              ).fontSize(13),
 | 
					 | 
				
			||||||
                                            ],
 | 
					 | 
				
			||||||
                                          ),
 | 
					 | 
				
			||||||
                                          onTap: () {},
 | 
					 | 
				
			||||||
                                        ),
 | 
					 | 
				
			||||||
                                    ],
 | 
					 | 
				
			||||||
                                  ),
 | 
					 | 
				
			||||||
                                if (item.categories.isNotEmpty)
 | 
					 | 
				
			||||||
                                  Wrap(
 | 
					 | 
				
			||||||
                                    children: [
 | 
					 | 
				
			||||||
                                      for (final category in item.categories)
 | 
					 | 
				
			||||||
                                        InkWell(
 | 
					 | 
				
			||||||
                                          child: Row(
 | 
					 | 
				
			||||||
                                            spacing: 4,
 | 
					 | 
				
			||||||
                                            children: [
 | 
					 | 
				
			||||||
                                              const Icon(
 | 
					 | 
				
			||||||
                                                Symbols.category,
 | 
					 | 
				
			||||||
                                                size: 13,
 | 
					 | 
				
			||||||
                                              ),
 | 
					 | 
				
			||||||
                                              Text(
 | 
					 | 
				
			||||||
                                                category.name ??
 | 
					 | 
				
			||||||
                                                    '#${category.slug}',
 | 
					 | 
				
			||||||
                                              ).fontSize(13),
 | 
					 | 
				
			||||||
                                            ],
 | 
					 | 
				
			||||||
                                          ),
 | 
					 | 
				
			||||||
                                          onTap: () {},
 | 
					 | 
				
			||||||
                                        ),
 | 
					 | 
				
			||||||
                                    ],
 | 
					 | 
				
			||||||
                                  ),
 | 
					 | 
				
			||||||
                              ],
 | 
					 | 
				
			||||||
                            ),
 | 
					 | 
				
			||||||
                          // Show truncation hint if post is truncated
 | 
					 | 
				
			||||||
                          if (item.isTruncated && !isFullPost)
 | 
					 | 
				
			||||||
                            _PostTruncateHint().padding(
 | 
					 | 
				
			||||||
                              bottom: item.attachments.isNotEmpty ? 8 : null,
 | 
					 | 
				
			||||||
                            ),
 | 
					 | 
				
			||||||
                          if ((item.repliedPost != null ||
 | 
					 | 
				
			||||||
                                  item.forwardedPost != null) &&
 | 
					 | 
				
			||||||
                              showReferencePost)
 | 
					 | 
				
			||||||
                            _buildReferencePost(context, item),
 | 
					 | 
				
			||||||
                          if (item.attachments.isNotEmpty)
 | 
					 | 
				
			||||||
                            CloudFileList(
 | 
					 | 
				
			||||||
                              files: item.attachments,
 | 
					 | 
				
			||||||
                              maxWidth: math.min(
 | 
					 | 
				
			||||||
                                MediaQuery.of(context).size.width * 0.85,
 | 
					 | 
				
			||||||
                                kWideScreenWidth - 160,
 | 
					 | 
				
			||||||
                              ),
 | 
					 | 
				
			||||||
                              minWidth: math.min(
 | 
					 | 
				
			||||||
                                MediaQuery.of(context).size.width * 0.9,
 | 
					 | 
				
			||||||
                                kWideScreenWidth - 160,
 | 
					 | 
				
			||||||
                              ),
 | 
					 | 
				
			||||||
                            ),
 | 
					 | 
				
			||||||
                          // Render embed links
 | 
					 | 
				
			||||||
                          if (item.meta?['embeds'] != null)
 | 
					 | 
				
			||||||
                            ...((item.meta!['embeds'] as List<dynamic>)
 | 
					 | 
				
			||||||
                                .where((embed) => embed['Type'] == 'link')
 | 
					 | 
				
			||||||
                                .map(
 | 
					 | 
				
			||||||
                                  (embedData) => EmbedLinkWidget(
 | 
					 | 
				
			||||||
                                    link: SnEmbedLink.fromJson(
 | 
					 | 
				
			||||||
                                      embedData as Map<String, dynamic>,
 | 
					 | 
				
			||||||
                                    ),
 | 
					 | 
				
			||||||
                                    maxWidth: math.min(
 | 
					 | 
				
			||||||
                                      MediaQuery.of(context).size.width * 0.85,
 | 
					 | 
				
			||||||
                                      kWideScreenWidth - 160,
 | 
					 | 
				
			||||||
                                    ),
 | 
					 | 
				
			||||||
                                    margin: EdgeInsets.only(top: 8),
 | 
					 | 
				
			||||||
                                  ),
 | 
					 | 
				
			||||||
                                )),
 | 
					 | 
				
			||||||
                        ],
 | 
					 | 
				
			||||||
                      ),
 | 
					 | 
				
			||||||
                      onTap: () {
 | 
					 | 
				
			||||||
                        if (isOpenable) {
 | 
					 | 
				
			||||||
                          context.push('/posts/${item.id}');
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                      },
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                ],
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              Row(
 | 
					 | 
				
			||||||
                children: [
 | 
					 | 
				
			||||||
                  // Replies count button
 | 
					 | 
				
			||||||
                  Padding(
 | 
					 | 
				
			||||||
                    padding: const EdgeInsets.only(left: 52, right: 12),
 | 
					 | 
				
			||||||
                    child: ActionChip(
 | 
					 | 
				
			||||||
                      avatar: Icon(Symbols.reply, size: 16),
 | 
					 | 
				
			||||||
                      label: Text(
 | 
					 | 
				
			||||||
                        (item.repliesCount > 0)
 | 
					 | 
				
			||||||
                            ? 'repliesCount'.plural(item.repliesCount)
 | 
					 | 
				
			||||||
                            : 'reply'.tr(),
 | 
					 | 
				
			||||||
                      ),
 | 
					 | 
				
			||||||
                      visualDensity: const VisualDensity(
 | 
					 | 
				
			||||||
                        horizontal: VisualDensity.minimumDensity,
 | 
					 | 
				
			||||||
                        vertical: VisualDensity.minimumDensity,
 | 
					 | 
				
			||||||
                      ),
 | 
					 | 
				
			||||||
                      onPressed: () {
 | 
					 | 
				
			||||||
                        if (isOpenable) {
 | 
					 | 
				
			||||||
                          showModalBottomSheet(
 | 
					 | 
				
			||||||
                            context: context,
 | 
					 | 
				
			||||||
                            isScrollControlled: true,
 | 
					 | 
				
			||||||
                            useRootNavigator: true,
 | 
					 | 
				
			||||||
                            builder: (context) => PostRepliesSheet(post: item),
 | 
					 | 
				
			||||||
                          );
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                      },
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                  // Reactions list
 | 
					 | 
				
			||||||
                  Expanded(
 | 
					 | 
				
			||||||
                    child: PostReactionList(
 | 
					 | 
				
			||||||
                      parentId: item.id,
 | 
					 | 
				
			||||||
                      reactions: item.reactionsCount,
 | 
					 | 
				
			||||||
                      padding: EdgeInsets.zero,
 | 
					 | 
				
			||||||
                      onReact: (symbol, attitude, delta) {
 | 
					 | 
				
			||||||
                        final reactionsCount = Map<String, int>.from(
 | 
					 | 
				
			||||||
                          item.reactionsCount,
 | 
					 | 
				
			||||||
                        );
 | 
					 | 
				
			||||||
                        reactionsCount[symbol] =
 | 
					 | 
				
			||||||
                            (reactionsCount[symbol] ?? 0) + delta;
 | 
					 | 
				
			||||||
                        onUpdate?.call(
 | 
					 | 
				
			||||||
                          item.copyWith(reactionsCount: reactionsCount),
 | 
					 | 
				
			||||||
                        );
 | 
					 | 
				
			||||||
                      },
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                ],
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -501,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)
 | 
				
			||||||
@@ -508,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: [
 | 
				
			||||||
@@ -805,6 +996,129 @@ class _PostTruncateHint extends StatelessWidget {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _ArticlePostDisplay extends StatelessWidget {
 | 
				
			||||||
 | 
					  final SnPost item;
 | 
				
			||||||
 | 
					  final bool isFullPost;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const _ArticlePostDisplay({required this.item, required this.isFullPost});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    if (isFullPost) {
 | 
				
			||||||
 | 
					      // Full article view
 | 
				
			||||||
 | 
					      return Column(
 | 
				
			||||||
 | 
					        crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					        children: [
 | 
				
			||||||
 | 
					          if (item.title?.isNotEmpty ?? false)
 | 
				
			||||||
 | 
					            Padding(
 | 
				
			||||||
 | 
					              padding: const EdgeInsets.only(bottom: 8.0),
 | 
				
			||||||
 | 
					              child: Text(
 | 
				
			||||||
 | 
					                item.title!,
 | 
				
			||||||
 | 
					                style: Theme.of(context).textTheme.headlineSmall?.copyWith(
 | 
				
			||||||
 | 
					                  fontWeight: FontWeight.bold,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          if (item.description?.isNotEmpty ?? false)
 | 
				
			||||||
 | 
					            Padding(
 | 
				
			||||||
 | 
					              padding: const EdgeInsets.only(bottom: 16.0),
 | 
				
			||||||
 | 
					              child: Text(
 | 
				
			||||||
 | 
					                item.description!,
 | 
				
			||||||
 | 
					                style: Theme.of(context).textTheme.bodyLarge?.copyWith(
 | 
				
			||||||
 | 
					                  color: Theme.of(context).colorScheme.onSurfaceVariant,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          if (item.content?.isNotEmpty ?? false)
 | 
				
			||||||
 | 
					            MarkdownTextContent(
 | 
				
			||||||
 | 
					              content: item.content!,
 | 
				
			||||||
 | 
					              textStyle: Theme.of(context).textTheme.bodyLarge,
 | 
				
			||||||
 | 
					              attachments: item.attachments,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // Truncated/Card view
 | 
				
			||||||
 | 
					      String? previewContent;
 | 
				
			||||||
 | 
					      if (item.description?.isNotEmpty ?? false) {
 | 
				
			||||||
 | 
					        previewContent = item.description!;
 | 
				
			||||||
 | 
					      } else if (item.content?.isNotEmpty ?? false) {
 | 
				
			||||||
 | 
					        previewContent = item.content!;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return Card(
 | 
				
			||||||
 | 
					        elevation: 0,
 | 
				
			||||||
 | 
					        margin: const EdgeInsets.only(top: 4),
 | 
				
			||||||
 | 
					        color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.3),
 | 
				
			||||||
 | 
					        shape: RoundedRectangleBorder(
 | 
				
			||||||
 | 
					          borderRadius: BorderRadius.circular(12),
 | 
				
			||||||
 | 
					          side: BorderSide(
 | 
				
			||||||
 | 
					            color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        child: Padding(
 | 
				
			||||||
 | 
					          padding: const EdgeInsets.all(16.0),
 | 
				
			||||||
 | 
					          child: Column(
 | 
				
			||||||
 | 
					            crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					            mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
 | 
					            children: [
 | 
				
			||||||
 | 
					              if (item.title?.isNotEmpty ?? false)
 | 
				
			||||||
 | 
					                Text(
 | 
				
			||||||
 | 
					                  item.title!,
 | 
				
			||||||
 | 
					                  style: Theme.of(context).textTheme.titleMedium?.copyWith(
 | 
				
			||||||
 | 
					                    fontWeight: FontWeight.bold,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  maxLines: 2,
 | 
				
			||||||
 | 
					                  overflow: TextOverflow.ellipsis,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              if (previewContent != null) ...[
 | 
				
			||||||
 | 
					                const Gap(8),
 | 
				
			||||||
 | 
					                Text(
 | 
				
			||||||
 | 
					                  previewContent,
 | 
				
			||||||
 | 
					                  style: Theme.of(context).textTheme.bodyMedium?.copyWith(
 | 
				
			||||||
 | 
					                    color: Theme.of(context).colorScheme.onSurfaceVariant,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  maxLines: 3,
 | 
				
			||||||
 | 
					                  overflow: TextOverflow.ellipsis,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
 | 
					              Container(
 | 
				
			||||||
 | 
					                margin: const EdgeInsets.only(top: 8),
 | 
				
			||||||
 | 
					                padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
 | 
				
			||||||
 | 
					                decoration: BoxDecoration(
 | 
				
			||||||
 | 
					                  color: Theme.of(
 | 
				
			||||||
 | 
					                    context,
 | 
				
			||||||
 | 
					                  ).colorScheme.surfaceContainerHighest.withOpacity(0.5),
 | 
				
			||||||
 | 
					                  borderRadius: BorderRadius.circular(20),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                child: Row(
 | 
				
			||||||
 | 
					                  mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
 | 
					                  children: [
 | 
				
			||||||
 | 
					                    Icon(
 | 
				
			||||||
 | 
					                      Symbols.article,
 | 
				
			||||||
 | 
					                      size: 16,
 | 
				
			||||||
 | 
					                      color: Theme.of(context).colorScheme.secondary,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    const SizedBox(width: 6),
 | 
				
			||||||
 | 
					                    Text(
 | 
				
			||||||
 | 
					                      'postArticle'.tr(),
 | 
				
			||||||
 | 
					                      style: TextStyle(
 | 
				
			||||||
 | 
					                        color: Theme.of(context).colorScheme.secondary,
 | 
				
			||||||
 | 
					                        fontSize: 12,
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    const SizedBox(width: 4),
 | 
				
			||||||
 | 
					                  ],
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Helper method to get the appropriate icon for each visibility status
 | 
					// Helper method to get the appropriate icon for each visibility status
 | 
				
			||||||
IconData _getVisibilityIcon(int visibility) {
 | 
					IconData _getVisibilityIcon(int visibility) {
 | 
				
			||||||
  switch (visibility) {
 | 
					  switch (visibility) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -87,7 +87,7 @@ class PostItemCreator extends HookConsumerWidget {
 | 
				
			|||||||
        );
 | 
					        );
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      child: Material(
 | 
					      child: Material(
 | 
				
			||||||
        color: Colors.transparent,
 | 
					        color: backgroundColor ?? Theme.of(context).colorScheme.surface,
 | 
				
			||||||
        borderRadius: BorderRadius.circular(12),
 | 
					        borderRadius: BorderRadius.circular(12),
 | 
				
			||||||
        elevation: 1,
 | 
					        elevation: 1,
 | 
				
			||||||
        child: InkWell(
 | 
					        child: InkWell(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,26 +24,29 @@ class PostRepliesSheet extends HookConsumerWidget {
 | 
				
			|||||||
          // Replies list
 | 
					          // Replies list
 | 
				
			||||||
          Expanded(
 | 
					          Expanded(
 | 
				
			||||||
            child: CustomScrollView(
 | 
					            child: CustomScrollView(
 | 
				
			||||||
              slivers: [PostRepliesList(
 | 
					              slivers: [
 | 
				
			||||||
                postId: post.id.toString(),
 | 
					                PostRepliesList(
 | 
				
			||||||
                backgroundColor: Colors.transparent,
 | 
					                  postId: post.id.toString(),
 | 
				
			||||||
              )],
 | 
					                  backgroundColor: Colors.transparent,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          // Quick reply section
 | 
					          // Quick reply section
 | 
				
			||||||
          Material(
 | 
					          if (user.value != null)
 | 
				
			||||||
            elevation: 2,
 | 
					            Material(
 | 
				
			||||||
            child: PostQuickReply(
 | 
					              elevation: 2,
 | 
				
			||||||
              parent: post,
 | 
					              child: PostQuickReply(
 | 
				
			||||||
              onPosted: () {
 | 
					                parent: post,
 | 
				
			||||||
                ref.invalidate(postRepliesNotifierProvider(post.id));
 | 
					                onPosted: () {
 | 
				
			||||||
              },
 | 
					                  ref.invalidate(postRepliesNotifierProvider(post.id));
 | 
				
			||||||
            ).padding(
 | 
					                },
 | 
				
			||||||
              bottom: MediaQuery.of(context).padding.bottom + 16,
 | 
					              ).padding(
 | 
				
			||||||
              top: 16,
 | 
					                bottom: MediaQuery.of(context).padding.bottom + 16,
 | 
				
			||||||
              horizontal: 16,
 | 
					                top: 16,
 | 
				
			||||||
 | 
					                horizontal: 16,
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1025,7 +1025,7 @@ packages:
 | 
				
			|||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "4.0.0"
 | 
					    version: "4.0.0"
 | 
				
			||||||
  flutter_web_plugins:
 | 
					  flutter_web_plugins:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description: flutter
 | 
					    description: flutter
 | 
				
			||||||
    source: sdk
 | 
					    source: sdk
 | 
				
			||||||
    version: "0.0.0"
 | 
					    version: "0.0.0"
 | 
				
			||||||
@@ -1097,10 +1097,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: go_router
 | 
					      name: go_router
 | 
				
			||||||
      sha256: ac294be30ba841830cfa146e5a3b22bb09f8dc5a0fdd9ca9332b04b0bde99ebf
 | 
					      sha256: c489908a54ce2131f1d1b7cc631af9c1a06fac5ca7c449e959192089f9489431
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "15.2.4"
 | 
					    version: "16.0.0"
 | 
				
			||||||
  google_fonts:
 | 
					  google_fonts:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
 | 
				
			|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
					# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
				
			||||||
# In Windows, build-name is used as the major, minor, and patch parts
 | 
					# In Windows, build-name is used as the major, minor, and patch parts
 | 
				
			||||||
# of the product and file versions while build-number is used as the build suffix.
 | 
					# of the product and file versions while build-number is used as the build suffix.
 | 
				
			||||||
version: 3.0.0+110
 | 
					version: 3.0.0+111
 | 
				
			||||||
 | 
					
 | 
				
			||||||
environment:
 | 
					environment:
 | 
				
			||||||
  sdk: ^3.7.2
 | 
					  sdk: ^3.7.2
 | 
				
			||||||
@@ -30,6 +30,8 @@ environment:
 | 
				
			|||||||
dependencies:
 | 
					dependencies:
 | 
				
			||||||
  flutter:
 | 
					  flutter:
 | 
				
			||||||
    sdk: flutter
 | 
					    sdk: flutter
 | 
				
			||||||
 | 
					  flutter_web_plugins:
 | 
				
			||||||
 | 
					    sdk: flutter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # The following adds the Cupertino Icons font to your application.
 | 
					  # The following adds the Cupertino Icons font to your application.
 | 
				
			||||||
  # Use with the CupertinoIcons class for iOS style icons.
 | 
					  # Use with the CupertinoIcons class for iOS style icons.
 | 
				
			||||||
@@ -37,7 +39,7 @@ dependencies:
 | 
				
			|||||||
  flutter_hooks: ^0.21.2
 | 
					  flutter_hooks: ^0.21.2
 | 
				
			||||||
  hooks_riverpod: ^2.6.1
 | 
					  hooks_riverpod: ^2.6.1
 | 
				
			||||||
  bitsdojo_window: ^0.1.6
 | 
					  bitsdojo_window: ^0.1.6
 | 
				
			||||||
  go_router: ^15.1.3
 | 
					  go_router: ^16.0.0
 | 
				
			||||||
  styled_widget: ^0.4.1
 | 
					  styled_widget: ^0.4.1
 | 
				
			||||||
  shared_preferences: ^2.5.3
 | 
					  shared_preferences: ^2.5.3
 | 
				
			||||||
  flutter_riverpod: ^2.6.1
 | 
					  flutter_riverpod: ^2.6.1
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user