💄 Optimize the embed view experience

This commit is contained in:
2025-09-23 21:02:14 +08:00
parent cccade763f
commit 27d478ba4f
4 changed files with 339 additions and 238 deletions

View File

@@ -1012,6 +1012,11 @@
"expandPoll": "Expand Poll", "expandPoll": "Expand Poll",
"collapsePoll": "Collapse Poll", "collapsePoll": "Collapse Poll",
"embedView": "Embed View", "embedView": "Embed View",
"auto": "Auto",
"manual": "Manual",
"iframeCode": "Iframe Code",
"iframeCodeHint": "<iframe src=\"...\" width=\"...\" height=\"...\">",
"parseIframe": "Parse Iframe",
"embedUri": "Embed URI", "embedUri": "Embed URI",
"aspectRatio": "Aspect Ratio", "aspectRatio": "Aspect Ratio",
"renderer": "Renderer", "renderer": "Renderer",
@@ -1023,5 +1028,6 @@
"noEmbed": "No embed yet", "noEmbed": "No embed yet",
"save": "Save", "save": "Save",
"webView": "Web View", "webView": "Web View",
"messageActions": "Message Actions" "messageActions": "Message Actions",
"viewEmbedLoadHint": "Tap to load"
} }

View File

@@ -30,10 +30,13 @@ class ComposeEmbedSheet extends HookConsumerWidget {
final selectedRenderer = useState<PostEmbedViewRenderer>( final selectedRenderer = useState<PostEmbedViewRenderer>(
PostEmbedViewRenderer.webView, PostEmbedViewRenderer.webView,
); );
final tabController = useTabController(initialLength: 2);
final iframeController = useTextEditingController();
void clearForm() { void clearForm() {
uriController.clear(); uriController.clear();
aspectRatioController.clear(); aspectRatioController.clear();
iframeController.clear();
selectedRenderer.value = PostEmbedViewRenderer.webView; selectedRenderer.value = PostEmbedViewRenderer.webView;
} }
@@ -77,6 +80,57 @@ class ComposeEmbedSheet extends HookConsumerWidget {
} }
} }
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( return SheetScaffold(
titleText: 'embedView'.tr(), titleText: 'embedView'.tr(),
heightFactor: 0.7, heightFactor: 0.7,
@@ -85,7 +139,7 @@ class ComposeEmbedSheet extends HookConsumerWidget {
// Header with save button when editing // Header with save button when editing
if (currentEmbedView != null) if (currentEmbedView != null)
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 6),
color: Theme.of(context).colorScheme.surfaceContainerHigh, color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: Row( child: Row(
children: [ children: [
@@ -97,15 +151,55 @@ class ComposeEmbedSheet extends HookConsumerWidget {
), ),
TextButton( TextButton(
onPressed: saveEmbedView, onPressed: saveEmbedView,
style: ButtonStyle(visualDensity: VisualDensity.compact),
child: Text('save'.tr()), child: Text('save'.tr()),
), ),
], ],
), ),
), ),
// Tab bar
TabBar(
controller: tabController,
tabs: [Tab(text: 'auto'.tr()), Tab(text: 'manual'.tr())],
),
// Content area // Content area
Expanded( Expanded(
child: SingleChildScrollView( 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), padding: const EdgeInsets.all(16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -136,7 +230,9 @@ class ComposeEmbedSheet extends HookConsumerWidget {
decimal: true, decimal: true,
), ),
inputFormatters: [ inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*$')), FilteringTextInputFormatter.allow(
RegExp(r'^\d*\.?\d*$'),
),
], ],
), ),
const Gap(16), const Gap(16),
@@ -148,11 +244,21 @@ class ComposeEmbedSheet extends HookConsumerWidget {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
), ),
selectedItemBuilder: (context) {
return PostEmbedViewRenderer.values.map((renderer) {
return Text(renderer.name).tr();
}).toList();
},
menuItemStyleData: MenuItemStyleData(
padding: EdgeInsets.zero,
),
items: items:
PostEmbedViewRenderer.values.map((renderer) { PostEmbedViewRenderer.values.map((renderer) {
return DropdownMenuItem( return DropdownMenuItem(
value: renderer, value: renderer,
child: Text(renderer.name).tr(), child: Text(
renderer.name,
).tr().padding(horizontal: 20),
); );
}).toList(), }).toList(),
onChanged: (value) { onChanged: (value) {
@@ -172,7 +278,10 @@ class ComposeEmbedSheet extends HookConsumerWidget {
const Gap(8), const Gap(8),
Card( Card(
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
color: Theme.of(context).colorScheme.surfaceContainerHigh, color:
Theme.of(
context,
).colorScheme.surfaceContainerHigh,
child: Padding( child: Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 16, left: 16,
@@ -203,43 +312,7 @@ class ComposeEmbedSheet extends HookConsumerWidget {
), ),
IconButton( IconButton(
icon: const Icon(Symbols.delete), icon: const Icon(Symbols.delete),
onPressed: () { onPressed: () => deleteEmbed(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:
colorScheme.error,
),
child: Text('delete').tr(),
),
],
),
);
},
tooltip: 'delete'.tr(), tooltip: 'delete'.tr(),
color: colorScheme.error, color: colorScheme.error,
), ),
@@ -265,7 +338,6 @@ class ComposeEmbedSheet extends HookConsumerWidget {
), ),
), ),
] else ...[ ] else ...[
// Save button for new embed
const Gap(16), const Gap(16),
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
@@ -279,6 +351,8 @@ class ComposeEmbedSheet extends HookConsumerWidget {
], ],
), ),
), ),
],
),
), ),
], ],
), ),

View File

@@ -80,6 +80,11 @@ class ComposeToolbar extends HookConsumerWidget {
child: Center( child: Center(
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 560), constraints: const BoxConstraints(maxWidth: 560),
child: Row(
children: [
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row( child: Row(
children: [ children: [
IconButton( IconButton(
@@ -137,7 +142,7 @@ class ComposeToolbar extends HookConsumerWidget {
builder: (context, _) { builder: (context, _) {
return IconButton( return IconButton(
onPressed: showEmbedSheet, onPressed: showEmbedSheet,
icon: const Icon(Symbols.web), icon: const Icon(Symbols.iframe),
tooltip: 'embedView'.tr(), tooltip: 'embedView'.tr(),
color: colorScheme.primary, color: colorScheme.primary,
style: ButtonStyle( style: ButtonStyle(
@@ -150,7 +155,10 @@ class ComposeToolbar extends HookConsumerWidget {
); );
}, },
), ),
const Spacer(), ],
),
),
),
if (originalPost == null && state.isEmpty) if (originalPost == null && state.isEmpty)
IconButton( IconButton(
icon: const Icon(Symbols.draft), icon: const Icon(Symbols.draft),

View File

@@ -1,3 +1,6 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart';
@@ -58,8 +61,8 @@ class EmbedViewRenderer extends HookConsumerWidget {
children: [ children: [
Icon( Icon(
embedView.renderer == PostEmbedViewRenderer.webView embedView.renderer == PostEmbedViewRenderer.webView
? Symbols.web ? Symbols.globe
: Symbols.web, : Symbols.iframe,
size: 16, size: 16,
color: colorScheme.primary, color: colorScheme.primary,
), ),
@@ -74,13 +77,13 @@ class EmbedViewRenderer extends HookConsumerWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
IconButton( InkWell(
icon: Icon( child: Icon(
Symbols.open_in_new, Symbols.open_in_new,
size: 16, size: 16,
color: colorScheme.onSurfaceVariant, color: colorScheme.onSurfaceVariant,
), ),
onPressed: () async { onTap: () async {
final uri = Uri.parse(embedView.uri); final uri = Uri.parse(embedView.uri);
if (await canLaunchUrl(uri)) { if (await canLaunchUrl(uri)) {
await launchUrl( await launchUrl(
@@ -89,10 +92,6 @@ class EmbedViewRenderer extends HookConsumerWidget {
); );
} }
}, },
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
visualDensity: VisualDensity.compact,
tooltip: 'Open in browser',
), ),
], ],
), ),
@@ -106,6 +105,20 @@ class EmbedViewRenderer extends HookConsumerWidget {
? Stack( ? Stack(
children: [ children: [
InAppWebView( InAppWebView(
gestureRecognizers: {
Factory<VerticalDragGestureRecognizer>(
() => VerticalDragGestureRecognizer(),
),
Factory<HorizontalDragGestureRecognizer>(
() => HorizontalDragGestureRecognizer(),
),
Factory<ScaleGestureRecognizer>(
() => ScaleGestureRecognizer(),
),
Factory<TapGestureRecognizer>(
() => TapGestureRecognizer(),
),
},
initialUrlRequest: URLRequest( initialUrlRequest: URLRequest(
url: WebUri(embedView.uri), url: WebUri(embedView.uri),
), ),
@@ -256,14 +269,14 @@ class EmbedViewRenderer extends HookConsumerWidget {
children: [ children: [
Icon( Icon(
Symbols.play_arrow, Symbols.play_arrow,
fill: 1,
size: 48, size: 48,
color: colorScheme.onSurfaceVariant.withOpacity( color: colorScheme.onSurfaceVariant.withOpacity(
0.6, 0.6,
), ),
), ),
const SizedBox(height: 8),
Text( Text(
'Tap to load content', 'embedViewLoadHint'.tr(),
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant color: colorScheme.onSurfaceVariant
.withOpacity(0.6), .withOpacity(0.6),