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()),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|