362 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			362 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
import 'package:dropdown_button2/dropdown_button2.dart';
 | 
						|
import 'package:easy_localization/easy_localization.dart';
 | 
						|
import 'package:flutter/material.dart';
 | 
						|
import 'package:flutter/services.dart';
 | 
						|
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
						|
import 'package:gap/gap.dart';
 | 
						|
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
						|
import 'package:island/models/post.dart';
 | 
						|
import 'package:island/widgets/content/sheet.dart';
 | 
						|
import 'package:island/widgets/post/compose_shared.dart';
 | 
						|
import 'package:material_symbols_icons/symbols.dart';
 | 
						|
import 'package:styled_widget/styled_widget.dart';
 | 
						|
 | 
						|
class ComposeEmbedSheet extends HookConsumerWidget {
 | 
						|
  final ComposeState state;
 | 
						|
 | 
						|
  const ComposeEmbedSheet({super.key, required this.state});
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context, WidgetRef ref) {
 | 
						|
    final theme = Theme.of(context);
 | 
						|
    final colorScheme = theme.colorScheme;
 | 
						|
 | 
						|
    // Listen to embed view changes
 | 
						|
    final currentEmbedView = useValueListenable(state.embedView);
 | 
						|
 | 
						|
    // Form state
 | 
						|
    final uriController = useTextEditingController();
 | 
						|
    final aspectRatioController = useTextEditingController();
 | 
						|
    final selectedRenderer = useState<PostEmbedViewRenderer>(
 | 
						|
      PostEmbedViewRenderer.webView,
 | 
						|
    );
 | 
						|
    final tabController = useTabController(initialLength: 2);
 | 
						|
    final iframeController = useTextEditingController();
 | 
						|
 | 
						|
    void clearForm() {
 | 
						|
      uriController.clear();
 | 
						|
      aspectRatioController.clear();
 | 
						|
      iframeController.clear();
 | 
						|
      selectedRenderer.value = PostEmbedViewRenderer.webView;
 | 
						|
    }
 | 
						|
 | 
						|
    // Populate form when embed view changes
 | 
						|
    useEffect(() {
 | 
						|
      if (currentEmbedView != null) {
 | 
						|
        uriController.text = currentEmbedView.uri;
 | 
						|
        aspectRatioController.text =
 | 
						|
            currentEmbedView.aspectRatio?.toString() ?? '';
 | 
						|
        selectedRenderer.value = currentEmbedView.renderer;
 | 
						|
      } else {
 | 
						|
        clearForm();
 | 
						|
      }
 | 
						|
      return null;
 | 
						|
    }, [currentEmbedView]);
 | 
						|
 | 
						|
    void saveEmbedView() {
 | 
						|
      final uri = uriController.text.trim();
 | 
						|
      if (uri.isEmpty) {
 | 
						|
        ScaffoldMessenger.of(
 | 
						|
          context,
 | 
						|
        ).showSnackBar(SnackBar(content: Text('embedUriRequired'.tr())));
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      final aspectRatio =
 | 
						|
          aspectRatioController.text.trim().isNotEmpty
 | 
						|
              ? double.tryParse(aspectRatioController.text.trim())
 | 
						|
              : null;
 | 
						|
 | 
						|
      final embedView = SnPostEmbedView(
 | 
						|
        uri: uri,
 | 
						|
        aspectRatio: aspectRatio,
 | 
						|
        renderer: selectedRenderer.value,
 | 
						|
      );
 | 
						|
 | 
						|
      if (currentEmbedView != null) {
 | 
						|
        ComposeLogic.updateEmbedView(state, embedView);
 | 
						|
      } else {
 | 
						|
        ComposeLogic.setEmbedView(state, embedView);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    void parseIframe() {
 | 
						|
      final iframe = iframeController.text.trim();
 | 
						|
      if (iframe.isEmpty) return;
 | 
						|
 | 
						|
      final srcMatch = RegExp(r'src="([^"]*)"').firstMatch(iframe);
 | 
						|
      final widthMatch = RegExp(r'width="([^"]*)"').firstMatch(iframe);
 | 
						|
      final heightMatch = RegExp(r'height="([^"]*)"').firstMatch(iframe);
 | 
						|
 | 
						|
      if (srcMatch != null) {
 | 
						|
        uriController.text = srcMatch.group(1)!;
 | 
						|
      }
 | 
						|
 | 
						|
      if (widthMatch != null && heightMatch != null) {
 | 
						|
        final w = double.tryParse(widthMatch.group(1)!);
 | 
						|
        final h = double.tryParse(heightMatch.group(1)!);
 | 
						|
        if (w != null && h != null && h != 0) {
 | 
						|
          aspectRatioController.text = (w / h).toStringAsFixed(3);
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      tabController.animateTo(1);
 | 
						|
    }
 | 
						|
 | 
						|
    void deleteEmbed(BuildContext context) {
 | 
						|
      showDialog(
 | 
						|
        context: context,
 | 
						|
        builder:
 | 
						|
            (dialogContext) => AlertDialog(
 | 
						|
              title: Text('deleteEmbed').tr(),
 | 
						|
              content: Text('deleteEmbedConfirm').tr(),
 | 
						|
              actions: [
 | 
						|
                TextButton(
 | 
						|
                  onPressed: () => Navigator.of(dialogContext).pop(),
 | 
						|
                  child: Text('cancel').tr(),
 | 
						|
                ),
 | 
						|
                TextButton(
 | 
						|
                  onPressed: () {
 | 
						|
                    ComposeLogic.deleteEmbedView(state);
 | 
						|
                    clearForm();
 | 
						|
                    Navigator.of(dialogContext).pop();
 | 
						|
                  },
 | 
						|
                  style: TextButton.styleFrom(
 | 
						|
                    foregroundColor: Theme.of(context).colorScheme.error,
 | 
						|
                  ),
 | 
						|
                  child: Text('delete').tr(),
 | 
						|
                ),
 | 
						|
              ],
 | 
						|
            ),
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    return SheetScaffold(
 | 
						|
      titleText: 'embedView'.tr(),
 | 
						|
      heightFactor: 0.7,
 | 
						|
      child: Column(
 | 
						|
        children: [
 | 
						|
          // Header with save button when editing
 | 
						|
          if (currentEmbedView != null)
 | 
						|
            Container(
 | 
						|
              padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 6),
 | 
						|
              color: Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
						|
              child: Row(
 | 
						|
                children: [
 | 
						|
                  Expanded(
 | 
						|
                    child: Text(
 | 
						|
                      'editEmbed'.tr(),
 | 
						|
                      style: theme.textTheme.titleMedium,
 | 
						|
                    ),
 | 
						|
                  ),
 | 
						|
                  TextButton(
 | 
						|
                    onPressed: saveEmbedView,
 | 
						|
                    style: ButtonStyle(visualDensity: VisualDensity.compact),
 | 
						|
                    child: Text('save'.tr()),
 | 
						|
                  ),
 | 
						|
                ],
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
 | 
						|
          // Tab bar
 | 
						|
          TabBar(
 | 
						|
            controller: tabController,
 | 
						|
            tabs: [Tab(text: 'auto'.tr()), Tab(text: 'manual'.tr())],
 | 
						|
          ),
 | 
						|
 | 
						|
          // Content area
 | 
						|
          Expanded(
 | 
						|
            child: TabBarView(
 | 
						|
              controller: tabController,
 | 
						|
              children: [
 | 
						|
                // Auto tab
 | 
						|
                SingleChildScrollView(
 | 
						|
                  padding: const EdgeInsets.all(16),
 | 
						|
                  child: Column(
 | 
						|
                    crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
                    children: [
 | 
						|
                      TextField(
 | 
						|
                        controller: iframeController,
 | 
						|
                        decoration: InputDecoration(
 | 
						|
                          labelText: 'iframeCode'.tr(),
 | 
						|
                          hintText: 'iframeCodeHint'.tr(),
 | 
						|
                          border: OutlineInputBorder(
 | 
						|
                            borderRadius: BorderRadius.circular(8),
 | 
						|
                          ),
 | 
						|
                        ),
 | 
						|
                        maxLines: 5,
 | 
						|
                      ),
 | 
						|
                      const Gap(16),
 | 
						|
                      SizedBox(
 | 
						|
                        width: double.infinity,
 | 
						|
                        child: FilledButton.icon(
 | 
						|
                          onPressed: parseIframe,
 | 
						|
                          icon: const Icon(Symbols.auto_fix),
 | 
						|
                          label: Text('parseIframe'.tr()),
 | 
						|
                        ),
 | 
						|
                      ),
 | 
						|
                    ],
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
                // Manual tab
 | 
						|
                SingleChildScrollView(
 | 
						|
                  padding: const EdgeInsets.all(16),
 | 
						|
                  child: Column(
 | 
						|
                    crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
                    children: [
 | 
						|
                      // Form fields
 | 
						|
                      TextField(
 | 
						|
                        controller: uriController,
 | 
						|
                        decoration: InputDecoration(
 | 
						|
                          labelText: 'embedUri'.tr(),
 | 
						|
                          hintText: 'https://example.com',
 | 
						|
                          border: OutlineInputBorder(
 | 
						|
                            borderRadius: BorderRadius.circular(8),
 | 
						|
                          ),
 | 
						|
                        ),
 | 
						|
                        keyboardType: TextInputType.url,
 | 
						|
                      ),
 | 
						|
                      const Gap(16),
 | 
						|
                      TextField(
 | 
						|
                        controller: aspectRatioController,
 | 
						|
                        decoration: InputDecoration(
 | 
						|
                          labelText: 'aspectRatio'.tr(),
 | 
						|
                          hintText: '16/9 = 1.777',
 | 
						|
                          border: OutlineInputBorder(
 | 
						|
                            borderRadius: BorderRadius.circular(8),
 | 
						|
                          ),
 | 
						|
                        ),
 | 
						|
                        keyboardType: TextInputType.numberWithOptions(
 | 
						|
                          decimal: true,
 | 
						|
                        ),
 | 
						|
                        inputFormatters: [
 | 
						|
                          FilteringTextInputFormatter.allow(
 | 
						|
                            RegExp(r'^\d*\.?\d*$'),
 | 
						|
                          ),
 | 
						|
                        ],
 | 
						|
                      ),
 | 
						|
                      const Gap(16),
 | 
						|
                      DropdownButtonFormField2<PostEmbedViewRenderer>(
 | 
						|
                        value: selectedRenderer.value,
 | 
						|
                        decoration: InputDecoration(
 | 
						|
                          labelText: 'renderer'.tr(),
 | 
						|
                          border: OutlineInputBorder(
 | 
						|
                            borderRadius: BorderRadius.circular(8),
 | 
						|
                          ),
 | 
						|
                        ),
 | 
						|
                        selectedItemBuilder: (context) {
 | 
						|
                          return PostEmbedViewRenderer.values.map((renderer) {
 | 
						|
                            return Text(renderer.name).tr();
 | 
						|
                          }).toList();
 | 
						|
                        },
 | 
						|
                        menuItemStyleData: MenuItemStyleData(
 | 
						|
                          padding: EdgeInsets.zero,
 | 
						|
                        ),
 | 
						|
                        items:
 | 
						|
                            PostEmbedViewRenderer.values.map((renderer) {
 | 
						|
                              return DropdownMenuItem(
 | 
						|
                                value: renderer,
 | 
						|
                                child: Text(
 | 
						|
                                  renderer.name,
 | 
						|
                                ).tr().padding(horizontal: 20),
 | 
						|
                              );
 | 
						|
                            }).toList(),
 | 
						|
                        onChanged: (value) {
 | 
						|
                          if (value != null) {
 | 
						|
                            selectedRenderer.value = value;
 | 
						|
                          }
 | 
						|
                        },
 | 
						|
                      ),
 | 
						|
 | 
						|
                      // Current embed view display (when exists)
 | 
						|
                      if (currentEmbedView != null) ...[
 | 
						|
                        const Gap(32),
 | 
						|
                        Text(
 | 
						|
                          'currentEmbed'.tr(),
 | 
						|
                          style: theme.textTheme.titleMedium,
 | 
						|
                        ).padding(horizontal: 4),
 | 
						|
                        const Gap(8),
 | 
						|
                        Card(
 | 
						|
                          margin: EdgeInsets.zero,
 | 
						|
                          color:
 | 
						|
                              Theme.of(
 | 
						|
                                context,
 | 
						|
                              ).colorScheme.surfaceContainerHigh,
 | 
						|
                          child: Padding(
 | 
						|
                            padding: const EdgeInsets.only(
 | 
						|
                              left: 16,
 | 
						|
                              right: 16,
 | 
						|
                              bottom: 12,
 | 
						|
                              top: 4,
 | 
						|
                            ),
 | 
						|
                            child: Column(
 | 
						|
                              crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
                              children: [
 | 
						|
                                Row(
 | 
						|
                                  children: [
 | 
						|
                                    Icon(
 | 
						|
                                      currentEmbedView.renderer ==
 | 
						|
                                              PostEmbedViewRenderer.webView
 | 
						|
                                          ? Symbols.web
 | 
						|
                                          : Symbols.web,
 | 
						|
                                      color: colorScheme.primary,
 | 
						|
                                    ),
 | 
						|
                                    const Gap(12),
 | 
						|
                                    Expanded(
 | 
						|
                                      child: Text(
 | 
						|
                                        currentEmbedView.uri,
 | 
						|
                                        style: theme.textTheme.bodyMedium,
 | 
						|
                                        maxLines: 1,
 | 
						|
                                        overflow: TextOverflow.ellipsis,
 | 
						|
                                      ),
 | 
						|
                                    ),
 | 
						|
                                    IconButton(
 | 
						|
                                      icon: const Icon(Symbols.delete),
 | 
						|
                                      onPressed: () => deleteEmbed(context),
 | 
						|
                                      tooltip: 'delete'.tr(),
 | 
						|
                                      color: colorScheme.error,
 | 
						|
                                    ),
 | 
						|
                                  ],
 | 
						|
                                ),
 | 
						|
                                const Gap(12),
 | 
						|
                                Text(
 | 
						|
                                  'aspectRatio'.tr(),
 | 
						|
                                  style: theme.textTheme.labelMedium?.copyWith(
 | 
						|
                                    color: colorScheme.onSurfaceVariant,
 | 
						|
                                  ),
 | 
						|
                                ),
 | 
						|
                                const Gap(4),
 | 
						|
                                Text(
 | 
						|
                                  currentEmbedView.aspectRatio != null
 | 
						|
                                      ? currentEmbedView.aspectRatio!
 | 
						|
                                          .toStringAsFixed(2)
 | 
						|
                                      : 'notSet'.tr(),
 | 
						|
                                  style: theme.textTheme.bodyMedium,
 | 
						|
                                ),
 | 
						|
                              ],
 | 
						|
                            ),
 | 
						|
                          ),
 | 
						|
                        ),
 | 
						|
                      ] else ...[
 | 
						|
                        const Gap(16),
 | 
						|
                        SizedBox(
 | 
						|
                          width: double.infinity,
 | 
						|
                          child: FilledButton.icon(
 | 
						|
                            onPressed: saveEmbedView,
 | 
						|
                            icon: const Icon(Symbols.add),
 | 
						|
                            label: Text('addEmbed'.tr()),
 | 
						|
                          ),
 | 
						|
                        ),
 | 
						|
                      ],
 | 
						|
                    ],
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
              ],
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
        ],
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 |